前言
目前主流的微服务架构下,多数 Node.js 应用都位于网关层,作为 BFF(Backend For Frontend) 或 SSR(Server Side Render),调用后端提供的各种下游服务来完成业务逻辑。 我们就以一个简单的 Node.js Web Server 作为 Hello world,介绍如何使用 client 调用 tRPC 服务,你可以在 DevCloud 环境中跟随下述步骤。 我们已经准备了一个已经部署在 123 平台的 tRPC 服务,在北极星控制台可以查到对应的实例列表。
1. 在 Web Server 中调用 tRPC 服务
首先创建一个 ObjectProxy 实例,一个 ObjectProxy 实例代表了一个被调方服务。
在创建 ObjectProxy 实例时,我们主要关注被调方服务是谁(who):
Choose here javascripttypescripthtmlcssshellpythongolangjavacc++c#phprubyswiftkotlinscalarustdartelixirhaskellluaperlrsql
const { trpc, client } = require('@tencent/trpc-rpc');
/** callee 是被调方服务的标识 (who) */
const callee = 'trpc.trpc_go.demo.Greeter';
const prx = client.createObjectProxy(callee)然后,就可以基于 ObjectProxy 实例向被调方服务发起 RPC 请求。
发起 RPC 请求时,需要指定要调用的具体接口名,同时传入序列化后的请求数据。
我们将发送请求的过程封装为一个函数,方便调用:
Choose here javascripttypescripthtmlcssshellpythongolangjavacc++c#phprubyswiftkotlinscalarustdartelixirhaskellluaperlrsql
const { trpc, client } = require('@tencent/trpc-rpc');
async function sayHello(req) {
/* 接口名 func */
const func = '/trpc.test.helloworld.Greeter/SayHello';
/** 使用 JSON 序列化请求数据 */
const type = trpc.TrpcContentEncodeType.TRPC_JSON_ENCODE;
/** 使用 JSON 序列化请求数据 */
const data = Buffer.from(JSON.stringify(req));
const { request, response } = await prx.rpcInvoke(func, { type, data }, { timeout: 10_000 });
if (response) {
const { err, data, type } = response;
if (err && err.code !== 0) return { err };
// 使用 JSON 反序列化响应数据
const res = JSON.parse(data.toString('utf-8'));
return { res };
}
}我们从 Node.js 官方教程 中复制一段代码,实现一个简单的 HTTP Server。 Choose here javascripttypescripthtmlcssshellpythongolangjavacc++c#phprubyswiftkotlinscalarustdartelixirhaskellluaperlrsql
const http = require('http')
const server = http.createServer((req, res) => {
res.statusCode = 200
res.setHeader('Content-Type', 'text/html')
res.end('<h1>Hello, World!</h1>')
})
server.listen(3000, () => {
const address = server.address();
console.log(`Server running at ${address.port}`)
})然后把处理请求的改成调用前文中封装的 sayHello
Choose here javascripttypescripthtmlcssshellpythongolangjavacc++c#phprubyswiftkotlinscalarustdartelixirhaskellluaperlrsql
const server = http.createServer((req, res) => {
const { socket } = req;
const address = `${socket.remoteAddress}:${socket.remotePort}`;
sayHello({
msg: address,
}).then((result) => {
res.end(JSON.stringify(result));
})
.catch((error) => { // 框架错误
console.error(error);
res.end(JSON.stringify({
code: error.code,
message: error.message,
}));
});
});
server.listen(3000, () => {
const address = server.address();
console.log(`Server running at ${address.port}`);
});执行代码,启动 HTTP Server 后,使用 CURL 访问 localhost:3000 即可看到 RPC 请求的结果。
Choose here javascripttypescripthtmlcssshellpythongolangjavacc++c#phprubyswiftkotlinscalarustdartelixirhaskellluaperlrsql
curl localhost:3000Choose here javascripttypescripthtmlcssshellpythongolangjavacc++c#phprubyswiftkotlinscalarustdartelixirhaskellluaperlrsql
{"res":{"msg":"tconf kv: get tconf kv error: trpc/config: failed to load test.conf: trpc-config-tconf: config not exist, tconf: get tconf error: trpc/config: failed to load test.yaml: trpc-config-tconf: config not exist, java rsp: call java err: type:framework, code:111, msg:tcp client transport connection pool: dial tcp 9.24.159.19:18001: connect: connection refused, cost:5.535134ms, other svr: call by polaris discovery err: type:framework, code:131, msg:client Select: fail to get instances, err is Polaris-1006(ErrCodeServerError): multierrs received for GetInstances request, serviceKey: {namespace: \"Development\", service: \"trpc.misakachen111.helloworld.Greeter1\"}, cause: 2 errors occurred:\n\t* SDKError for {ServiceKey: {namespace: \"Development\", service: \"trpc.misakachen111.helloworld.Greeter1\"}, Operation: destinationInstances}, detail is Polaris-1006(ErrCodeServerError): server error from 11.181.43.17:8081: not found resource\n\t* SDKError for {ServiceKey: {namespace: \"Development\", service: \"trpc.misakachen111.helloworld.Greeter1\"}, Operation: destinationRoute}, detail is Polaris-1006(ErrCodeServerError): server error from 11.181.43.17:8081: not found service\n\n, input value: ::ffff:127.0.0.1:40059"}}完整代码如下 Choose here javascripttypescripthtmlcssshellpythongolangjavacc++c#phprubyswiftkotlinscalarustdartelixirhaskellluaperlrsql
const { trpc, client } = require('@tencent/trpc-rpc');
const http = require('http');
/** callee 是被调方服务的标识,也就是表示被调方服务是谁 (who) */
const callee = 'trpc.trpc_go.demo.Greeter';
const prx = client.createObjectProxy(callee, {});
async function sayHello(req) {
/* 接口名 func */
const func = '/trpc.test.helloworld.Greeter/SayHello';
/** 使用 JSON 序列化请求数据 */
const type = trpc.TrpcContentEncodeType.TRPC_JSON_ENCODE;
/** 使用 JSON 序列化请求数据 */
const data = Buffer.from(JSON.stringify(req));
const { request, response } = await prx.rpcInvoke(func, { type, data }, { timeout: 10_000 });
if (response) {
const { err, data, type } = response;
if (err && err.code !== 0) return { err };
// 使用 JSON 反序列化响应数据
const res = JSON.parse(data.toString('utf-8'));
return { res };
}
}
const server = http.createServer((req, res) => {
const { socket } = req;
const address = `${socket.remoteAddress}:${socket.remotePort}`;
sayHello({
msg: address,
}).then((result) => {
res.end(JSON.stringify(result));
})
.catch((error) => { // 框架错误
console.error(error);
res.end(JSON.stringify({
code: error.code,
message: error.message,
}));
});
});
server.listen(3000, () => {
const address = server.address();
console.log(`Server running at ${address.port}`);
});2. 使用 Protocol Buffers 对消息体进行序列化/反序列化
上述的示例中,我们使用 JSON 序列化/反序列化请求数据,反序列化响应数据,这在 web API 中很常见,但在 RPC 通信中不是首选。 tRPC 推荐你使用 Protocol Buffers 协议对消息体进行序列化/反序列化,以获得更高的性能。在 Node.js 中,我们通过 protobufjs 来使用 Protocol Buffers 协议。 上述示例中调用的 tRPC 服务的 Protocol Buffers 接口定义如下: Choose here javascripttypescripthtmlcssshellpythongolangjavacc++c#phprubyswiftkotlinscalarustdartelixirhaskellluaperlrsql
syntax = "proto3";
package trpc.test.helloworld;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
string msg = 1;
}
message HelloReply {
string msg = 1;
}接口定义文件中需要关注的点:
-
package关键字声明当前接口定义文件的命名空间,可以有多段,用.分隔。 -
service关键字声明了一个接口的集合,包含多个rpc声明的接口。 -
rpc关键字声明了一个具体“接口”,包含接口标识,请求数据类型和响应数据类型。
在 tRPC 中使用 Protocol Buffers 时,需要注意:
-
package标识,使用三段格式,第一段固定为trpc,后两段为业务自定义的标识,通常为 “业务” 和 “模块”。 -
service标识,与第一节中的callee有一些联系和区别。在不严格的语境下,service可能会被称为“服务”,但在 tRPC 中,“服务” 有多重理解,在介绍 tRPC Server 时会详细解释。 -
tRPC 调用时使用的接口名
func是由接口定义中的packageservicerpc的标识拼接而成,格式为/${package}.${service}/${rpc}。上述接口定义中的SayHello接口对应的func的值为/trpc.test.helloworld.Greeter/SayHello,在第一节的示例中我们已经见过了。
我们在 proto/ 目录下创建 helloworld.proto 文件,把上述接口定义内容复制到 helloworld.proto 中。然后安装 protobufjs,使用 pbjs 命令生成对应的 JavaScript 代码。
Choose here javascripttypescripthtmlcssshellpythongolangjavacc++c#phprubyswiftkotlinscalarustdartelixirhaskellluaperlrsql
npm i protobufjs
npx pbjs -t static-module -w commonjs -r helloworld -o proto/helloworld.js proto/helloworld.protopbjs 主要功能是将接口定义中的 message 定义的数据结构,生成为 JavaScript 中对应的 class 以及相关的 encode/decode 方法,供我们在 JavaScript 中使用。
我们将第一节示例代码中的 JSON 序列化/反序列化,改成使用 message 类的 encode/decode 方法。
Choose here javascripttypescripthtmlcssshellpythongolangjavacc++c#phprubyswiftkotlinscalarustdartelixirhaskellluaperlrsql
const { trpc: { test: { helloworld: { HelloRequest, HelloReply } } } } = require('./proto/helloworld');
async function sayHello(req) {
/* 接口名 func */
const func = '/trpc.test.helloworld.Greeter/SayHello';
/** 使用 Protocol Buffers 序列化请求数据 */
const type = trpc.TrpcContentEncodeType.TRPC_PROTO_ENCODE;
/** 使用 Protocol Buffers 序列化请求数据 */
const data = HelloRequest.encode(req).finish();
const { request, response } = await prx.rpcInvoke(func, { type, data }, { timeout: 10_000 });
if (response) {
const { err, data, type } = response;
conso.log(err, data);
if (err && err.code !== 0) return { err };
// 使用 Protocol Buffers 反序列化响应数据
const res = HelloReply.decode(data);
return { res };
}
}完整代码如下 Choose here javascripttypescripthtmlcssshellpythongolangjavacc++c#phprubyswiftkotlinscalarustdartelixirhaskellluaperlrsql
const { trpc, client } = require('@tencent/trpc-rpc');
const http = require('http');
const { trpc: { test: { helloworld: { HelloRequest, HelloReply } } } } = require('./proto/helloworld');
/** callee 是被调方服务的标识,也就是表示被调方服务是谁 (who) */
const callee = 'trpc.trpc_go.demo.Greeter';
const prx = client.createObjectProxy(callee, {});
async function sayHello(req) {
/* 接口名 func */
const func = '/trpc.test.helloworld.Greeter/SayHello';
/** 使用 Protocol Buffers 序列化请求数据 */
const type = trpc.TrpcContentEncodeType.TRPC_PROTO_ENCODE;
/** 使用 Protocol Buffers 序列化请求数据 */
const data = HelloRequest.encode(req).finish();
const { request, response } = await prx.rpcInvoke(func, { type, data }, { timeout: 10_000 });
if (response) {
const { err, data, type } = response;
if (err && err.code !== 0) return { err };
// 使用 Protocol Buffers 反序列化响应数据
const res = HelloReply.decode(data);
return { res };
}
}
const server = http.createServer((req, res) => {
const { socket } = req;
const address = `${socket.remoteAddress}:${socket.remotePort}`;
sayHello({
msg: address,
}).then((result) => {
res.end(JSON.stringify(result));
})
.catch((error) => { // 框架错误
console.error(error);
res.end(JSON.stringify({
code: error.code,
message: error.message,
}));
});
});
server.listen(3000, () => {
const address = server.address();
console.log(`Server running at ${address.port}`);
});3. 传输二进制数据
以上示例展示了如何使用 JSON 和 Protocol buffer 对用户数据进行序列化后通过 tRPC Node.js 传输。在一些特殊场景下,可能需要直接传输二进制数据,例如用户上传的文件等。
在 Node.js 中,二进制数据用 Buffer 实例表示。在 tRPC 协议中,使用了 TrpcContentEncodeType.TRPC_NOOP_ENCODE 标识传输内容为二进制数据。
在上述示例中,修改 type 和 data 参数即可:
Choose here javascripttypescripthtmlcssshellpythongolangjavacc++c#phprubyswiftkotlinscalarustdartelixirhaskellluaperlrsql
/** 用户自定义序列化方式的请求数据 */
const type = trpc.TrpcContentEncodeType.TRPC_NOOP_ENCODE;
/** 作为示例,读取当前文件作为请求数据 */
const data = require('fs').readFileSync(__filename);4. 处理错误
tRPC 提供了两种错误类型,框架错误和业务错误。框架错误表示通信过程本身出现了异常,比如网络不通,找不到被调方服务,调用超时等等。业务错误是被调方服务自定义的异常情况,比如一个登录接口中输入的密码错误。
在上述示例代码中,通过 rpcInvoke(...args) 方法发起 RPC 调用,返回的 response.err 即为业务。err.code 是错误码, err.message 是附带的错误信息。
发生框架错误时, rpcInvoke(...args) 会直接抛出(throw)异常,你需要通过 try catch 语句或 Promise.prototype.catch() 来捕获框架错误。捕获到的错误对象包含错误码 code 和附带的错误消息 message。
各框架错误码的含义和原因参考 错误码原因定位。
5. 使用代码生成工具
以上演示的调用流程中,构造 func data type 等请求参数和对响应数据进行解码的过程属于标准流程。换句话说,调用每一个接口都要重复这个过程。 我们提供了代码生成工具来自动生成这部分重复性的代码。代码生成工具的使用参考 trpc-node tools trpc-tools-codec - 工蜂内网版 (woa.com)
6. 初始化选项和调用时选项
在调用 client.createObjectProxy 和 prx.rpcInvoke 方法时,可以指定一些选项,例如上述示例中已经看到的 timeout。
详细的介绍参考 tRPC-Node.js Client 初始化选项 和 tRPC-Node.js Client 调用时选项