前言
本文将简单介绍RPC,以及通过Eggjs框架搭建RPC客户端和服务端。
过程遇到的问题记录在这:Node.js Eggjs使用RPC模块 egg-sofa-rpc 踩坑记录
RPC介绍
RPC ( Remote Procedure Call ) 即 远程过程调用,就是像调用本地的函数一样去调用远程的函数。简单讲,就是本地调用的逻辑处理的过程放在的远程的机器上,而不是本地服务代理来处理。
也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。
HTTP和RPC的对比
在HTTP和RPC的选择上,可能有些人是迷惑的,主要是因为,有些RPC框架配置复杂,如果走HTTP也能完成同样的功能,那么为什么要选择RPC,而不是更容易上手的HTTP来实现了。
以下是阐述HTTP和RPC的异同。
传输协议 RPC,可以基于TCP协议,也可以基于HTTP协议HTTP,基于HTTP协议 传输效率 RPC,使用自定义的TCP协议,可以让请求报文体积更小,或者使用HTTP2协议,也可以很好的减少报文的体积,提高传输效率HTTP,如果是基于HTTP1.1的协议,请求中会包含很多无用的内容,如果是基于HTTP2.0,那么简单的封装以下是可以作为一个RPC来使用的,这时标准RPC框架更多的是服务治理 性能消耗,主要在于序列化和反序列化的耗时 RPC,可以基于thrift实现高效的二进制传输HTTP,大部分是通过json来实现的,字节大小和序列化耗时都比thrift要更消耗性能 负载均衡 RPC,基本都自带了负载均衡策略HTTP,需要配置Nginx,HAProxy来实现 服务治理(下游服务新增,重启,下线时如何不影响上游调用者) RPC,能做到自动通知,不影响上游HTTP,需要事先通知,修改Nginx/HAProxy配置
总结:
RPC主要用于公司内部的服务调用,性能消耗低,传输效率高,服务治理方便。HTTP主要用于对外的异构环境,浏览器接口调用,APP接口调用,第三方接口调用等。
HTTP接口是在接口不多、系统与系统交互较少的情况下,解决信息孤岛初期常使用的一种通信手段;优点就是简单、直接、开发方便。利用现成的HTTP协议进行传输。但是如果是一个大型的网站,内部子系统较多、接口非常多的情况下,RPC框架的好处就显示出来了,首先就是长链接,不必每次通信都要像HTTP一样去3次握手什么的,减少了网络开销;其次就是RPC框架一般都有注册中心,有丰富的监控管理;发布、下线接口、动态扩展等,对调用方来说是无感知、统一化的操作。第三个来说就是安全性。最后就是最近流行的服务化架构、服务化治理,RPC框架是一个强力的支撑
准备工作
本文以windows为例,其他操作系统的安装、使用方法请自行 google。
安装 nodejs
要求版本 >= 8.0.0
下载安装包:/zh-cn/执行安装
安装zookeeper
下载安装包:/releases.html
如果是最新版本则下载带有bin的包,不带bin是源码,需要自行编译
启动zookeeper
服务
复制conf/zoo_sample.cfg
为conf/zoo.cfg
在bin目录打开cmd,执行命令运行服务
Linux:bin/zkServer.sh start
Windows:bin/zkServer.cmd
全局安装egg-init
$ npm i egg-init -g
初始化工程
通过egg-init
初始化项目脚手架,选择 simple 模板,接下来根据实际情况填写必要信息$ egg-init? Please select a boilerplate type (Use arrow keys)──────────────❯ simple - Simple egg app boilerplatets - Simple egg && typescript app boilerplateempty - Empty egg app boilerplateplugin - egg plugin boilerplateframework - egg framework boilerplate
进入生成好的项目目录,并安装依赖
$ cd /rpc-demo$ npm i
安装sofa-node
框架,egg-sofa-rpc
插件和egg-rpc-generator
工具
$ npm i sofa-node --save$ npm i egg-sofa-rpc --save$ npm i egg-rpc-generator --save-dev
配置RPC
配置
配置package.json
的 scripts 节点,增加一个命令 rpc 如下
{"scripts": {"rpc": "egg-rpc-generator -p protobuf"}}
在使用
egg-rpc-generator
生成代理文件时,会同时进行ProtoRPCPlugin
,jsdoc2jar
,Jar2ProxyPlugin
这三种模式,其中会编译生成jar包,在目前的实际应用中,需要用的是egg之间调用,不涉及跨平台,那也就没必要生成相关jar包,解决方式是添加参数-p
配置package.json
的 egg 节点,增加"framework": "sofa-node"
{"egg": {"framework": "sofa-node","declarations": true},}
配置config/plugin.js
开启egg-sofa-rpc
插件
// config/plugin.js'use strict';module.exports = {sofaRpc: {enable: true,package: 'egg-sofa-rpc',},};
默认的服务发现依赖于 zookeeper,所以需要配置一个 zk 的地址。在config/config.{env}.js
中配置rpc
如下:
'use strict';module.exports = appInfo => {const config = exports = {};config.rpc = {registry: {address: '127.0.0.1:2181', // zk 地址指向本地 2181 端口},};return config;};
定义接口文件
这个proto文件是作为通信转换,在客户端或服务端都需要。
在 egg 项目根目录下创建 proto 目录,创建 ProtoService.proto 文件
.├── app│ ├── controller│ │ └── home.js│ └── router.js├── config│ ├── config.default.js│ └── plugin.js├── package.json└── proto└── ProtoService.proto
protobuf 有自己的接口定义语言,详细可以参考官方文档。
# ProtoService.proto syntax = "proto3";package com.nodejs.rpc;option java_multiple_files = false; // 可选option java_outer_classname = "ProtoServiceModels"; // 可选service ProtoService {rpc echoObj (EchoRequest) returns (EchoResponse) {}}message EchoRequest {string name = 1;Group group = 2;}message EchoResponse {int32 code = 1;string message = 2;}enum Group {A = 0;B = 1;}
上面这个 ProtoService.proto 文件定义了一个服务:com.nodejs.rpc.ProtoService
,它有一个叫echoObj
的方法,入口参数类型是EchoRequest
,返回值类型是EchoResponse
。
RPC服务端
配置 RPC 服务端的参数
通过config/config.{env}.js
配置 RPC 服务端server
的参数
// config/config.default.js'use strict';module.exports = appInfo => {const config = exports = {};config.rpc = {registry: {address: '127.0.0.1:2181', // zk 地址指向本地 2181 端口},server: {namespace: 'com.nodejs.rpc',},};return config;};
其中最主要的配置就是 namespace,其他配置都可以缺省:
namespace
(必选): 接口的命名空间,所有的暴露的接口默认都在该命名空间下selfPublish
(可选): 是否每个 worker 进程独立暴露服务。nodejs 多进程模式下,如果多个进程共享一个端口,在 RPC 这种场景可能造成负载不均,所以 selfPublish 默认为 true,代表每个进程独立监听端口和发布服务port
(可选): 服务监听的端口(注意:在 selfPublish=true 时,监听的端口是基于这个配置生成的)maxIdleTime
(可选): 客户端连接如果在该配置时长内没有任何流量,则主动断开连接responseTimeout
(可选): 服务端建议的超时时长,具体的超时还是以客户端配置为准codecType
(可选): 推荐的序列化方式,默认为 protobuf
实现接口逻辑
在app/rpc
目录下创建 ProtoService.js 文件,用于实现接口逻辑
'use strict';exports.echoObj = async function(req) {return {code: 200,message: 'hello ' + req.name + ', you are in ' + req.group,};};
发布服务
运行命令npm run dev
测试 RPC 接口
在单元测试中,我们可以通过app.rpcRequest
接口来方便的测试我们自己暴露的 RPC 服务,例如:
'use strict';const { app, assert } = require('egg-mock/bootstrap');describe('test/app/rpc/ProtoService.test.js', () => {it('should assert', () => {const pkg = require('../../../package.json');assert(app.config.keys.startsWith(pkg.name));// const ctx = app.mockContext({});// yield ctx.service.xx();});it('should invoke ProtoService', done => {app.rpcRequest('com.nodejs.rpc.ProtoService').invoke('echoObj').send({ name: 'test', group: 'A' }).expect({ code: 200, message: 'hello test, you are in 0' }, done);});});
详细app.rpcRequest
的 api 可以参考:单元测试 RPC 服务的方法
执行单元测试需要先配置要调用的接口
RPC客户端
配置 RPC 客户端的参数
在${app_root}/config/config.${env}.js
做一些全局性的配置
// ${app_root}config/config.${env}.jsexports.rpc = {client: {responseTimeout: 3000,},};
responseTimeout
(可选): RPC 的超时时长,默认为 3 秒
配置要调用的接口
RPC 客户端还有一个重要的配置文件是:${app_root}/config/proxy.js
,你需要把你调用的服务配置到里面,然后通过egg-rpc-generator
工具帮你生成本地调用代码。
让我们看一个最简单的配置,它的基本含义是:我需要调用sofarpc
应用暴露的com.nodejs.rpc.ProtoService
这个服务。
'use strict';module.exports = {services: [{appName: 'sofarpc',api: {ProtoService: 'com.nodejs.rpc.ProtoService',},}],};
appName
(必选): 服务提供方的应用名,如果没有可以任意起一个api
(必选): 接口列表,是一个 key-value 键值对,key 是生成的 proxy 文件名,value 是接口名(如果要更精细的配置也可以是一个对象)
详细的配置可以参考 RPC 代理(Proxy)配置
生成本地调用代理
在根目录下运行npm run rpc
,生成调用的 proxy 文件
$ npm run rpc> rpc_server@1.0.0 rpc /egg-rpc-demo> egg-rpc-generator -p protobuf[EggRpcGenerator] framework: /egg-rpc-demo/node_modules/sofa-node, baseDir: /egg-rpc-demo[ProtoRPCPlugin] found "com.nodejs.rpc.ProtoService" in proto file[ProtoRPCPlugin] save all proto info into "/egg-rpc-demo/run/proto.json"
运行成功以后,会发现生成了两个文件
app/proxy/ProtoService.js
- 调用服务的代理文件run/proto.json
- 从 .proto 文件中导出的接口信息,是一个 json 格式文件
.├── app│ ├── controller│ │ └── home.js│ ├── proxy│ │ └── ProtoService.js│ └── router.js├── config│ ├── config.default.js│ ├── plugin.js│ └── proxy.js├── package.json├── proto│ └── ProtoService.proto└── run└── proto.json
生成的app/proxy/ProtoService.js
文件内容如下(注意:不要手动去改这个文件):
// Don't modified this file, it's auto created by egg-rpc-generator'use strict';const path = require('path');/* eslint-disable *//* istanbul ignore next */module.exports = app => {const consumer = app.rpcClient.createConsumer({interfaceName: 'com.nodejs.rpc.ProtoService',targetAppName: 'sofarpc',version: '1.0',group: 'SOFA',proxyName: 'ProtoService',});if (!consumer) {// `app.config['sofarpc.rpc.service.enable'] = false` will disable this consumerreturn;}app.beforeStart(async() => {await consumer.ready();});class ProtoService extends app.Proxy {constructor(ctx) {super(ctx, consumer);}async echoObj(req) {return await consumer.invoke('echoObj', [ req ], { ctx: this.ctx,});}}return ProtoService;};/* eslint-enable */
调用服务
上面定义的这个 ProtoService 这个类,会挂载在 app.proxyClasses 上。
通过ctx.proxy.proxyName
(注意这里是小驼峰)来访问生成的 proxy 代码,proxyName 就是上面 proxy.js 配置的 api 键值对中的 key。例如:上面配置的 ProtoService,但是需要特别注意的是 proxyName 会自动转成小驼峰形式,所以就是ctx.proxy.protoService
。
下面我们在 home controller 调用 ProtoService 的 echoObj 方法
// app/controller/home.js'use strict';const Controller = require('egg').Controller;class HomeController extends Controller {async index() {const { ctx } = this;const res = await ctx.proxy.protoService.echoObj({name: 'gxcsoccer',group: 'A',});ctx.body = res;}}module.exports = HomeController;
和调用本地方法体验一模一样。
相关资源链接
egg-sofa-rpc 插件
/eggjs/egg-sofa-rpc
egg-rpc-generator 工具
/eggjs/egg-rpc-generator
zookeeper
/