前言

目前主流的微服务架构下,多数 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:3000

       Choose 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 是由接口定义中的 package service rpc 的标识拼接而成,格式为 /${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.proto

pbjs 主要功能是将接口定义中的 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 调用时选项