本文将简单介绍RPC,以及通过Eggjs框架搭建RPC客户端和服务端。
过程遇到的问题记录在这:Node.js Eggjs使用RPC模块 egg-sofa-rpc 踩坑记录
RPC ( Remote Procedure Call ) 即 远程过程调用,就是像调用本地的函数一样去调用远程的函数。简单讲,就是本地调用的逻辑处理的过程放在的远程的机器上,而不是本地服务代理来处理。
也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。
在HTTP和RPC的选择上,可能有些人是迷惑的,主要是因为,有些RPC框架配置复杂,如果走HTTP也能完成同样的功能,那么为什么要选择RPC,而不是更容易上手的HTTP来实现了。
以下是阐述HTTP和RPC的异同。
总结:
RPC主要用于公司内部的服务调用,性能消耗低,传输效率高,服务治理方便。HTTP主要用于对外的异构环境,浏览器接口调用,APP接口调用,第三方接口调用等。
HTTP接口是在接口不多、系统与系统交互较少的情况下,解决信息孤岛初期常使用的一种通信手段;优点就是简单、直接、开发方便。利用现成的HTTP协议进行传输。但是如果是一个大型的网站,内部子系统较多、接口非常多的情况下,RPC框架的好处就显示出来了,首先就是长链接,不必每次通信都要像HTTP一样去3次握手什么的,减少了网络开销;其次就是RPC框架一般都有注册中心,有丰富的监控管理;发布、下线接口、动态扩展等,对调用方来说是无感知、统一化的操作。第三个来说就是安全性。最后就是最近流行的服务化架构、服务化治理,RPC框架是一个强力的支撑
本文以windows为例,其他操作系统的安装、使用方法请自行 google。
要求版本 >= 8.0.0
zookeeper
下载安装包:http://zookeeper.apache.org/releases.html
如果是最新版本则下载带有bin的包,不带bin是源码,需要自行编译
启动 zookeeper
服务
复制 conf/zoo_sample.cfg
为 conf/zoo.cfg
在bin目录打开cmd,执行命令运行服务
bin/zkServer.sh start
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 boilerplate
ts - Simple egg && typescript app boilerplate
empty - Empty egg app boilerplate
plugin - egg plugin boilerplate
framework - 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
配置 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
。
通过 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
在单元测试中,我们可以通过 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 服务的方法
执行单元测试需要先配置要调用的接口
在 ${app_root}/config/config.${env}.js
做一些全局性的配置
// ${app_root}config/config.${env}.js
exports.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 consumer
return;
}
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 插件
https://github.com/eggjs/egg-sofa-rpc
egg-rpc-generator 工具
https://github.com/eggjs/egg-rpc-generator
zookeeper
http://zookeeper.apache.org/