本文通过一个最基础的示例,来演示 grpc 通信的简单使用。示例中采用最基础的请求 + 应答方式完成通信,类似于最简单的 http 客户端请求 + 服务端响应返回结果的形式。
grpc 通信示例
- Proto 定义 worker.proto
首先定义 protocol buffer 的相关协议的调用方法与参数结构,可以理解为 Typescript 中的类型申明,这一步可以申明 rpc 方法的参数与返回值结构。
按照文档规范的格式,提供 package, service, message 等结构的定义。
syntax = "proto3";
package worker;
service Task {
rpc runTask(TaskConf) returns (TaskResult) {}
}
message TaskConf {
string id = 1;
string job = 2;
}
message TaskResult {
string status = 1;
string result = 2;
}
- Proto 对象加载模块 proto.ts
在定义 server 与 client 之前,需要从 proto 文件中加载 proto buffer 的模块信息。
import grpc from 'grpc';
import * as protoLoader from '@grpc/proto-loader';
const pack = protoLoader.loadSync(
'./resources/proto/worker.proto',
{
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true
}
);
export const proto: any = grpc.loadPackageDefinition(pack).worker;
注意到这部分对于返回的 proto 对象,强制使用了 any 的类型申明,而不是一个 GrpcObject.主要原因是目前 grpc 本身对于使用预编译模块集成的接口定义上比较含糊,GrpcObject 对象本身使用了如下所示的过多类型定义,行为上本身已经有了很大的不确定性。如要使用相关的方法,则需要做更进一步的类型规约,使用上会相对繁琐不少。同时,官方示例中,也是使用了目前演示的方法。
GrpcObject 的类型定义:
export interface GrpcObject {
[name: string]: GrpcObject | typeof Client | Message;
}
在 proto 的加载过程中,loader 本身会对定义文件进行配置的检查,防止错误的定义产生使用上的异常。
关于 proto-loader 加载过程中的参数文档可参考: https://www.npmjs.com/package/@grpc/proto-loader
- ts 类型申明 task.types.ts
在 typescript 环境下,将 rpc 通信的参数与返回结构都进行申明。
export interface TaskConf {
id: string,
job: string
}
export interface TaskResult {
status: string,
result: string
}
- 服务端 server.ts
完成必要的结构申明之后,开始注册服务端的 rpc 处理方法。如下表所示,grpc 提供了四种服务端的处理方式,对应四种不同的通信方式协议。
方法 | 用途 |
---|---|
handleUnaryCall | 处理常规请求调用(单一参数形式) |
handleClientStreamingCall | 处理客户端流式传输调用 |
handleServerStreamingCall | 处理服务端流式传输调用 |
handleBidiStreamingCall | 处理双向流式传输调用 |
本次演示的即常规请求的 rpc 通信调用形式,而未使用流式处理,所以采用基础的单参数调用形式即可,handle 方法中会提供一个 call 参数用于传递请求信息。
grpc 的通信建立在 TPC 的 HTTP/2 协议上,所以也需要在服务端初始化监听端口,且建立服务时还需指定安全规则,作为内部系统的微服务,可以仅使用无校验规则策略。对于公共网络下的 rpc 通信,则可以考虑开启 ssl 加密甚至是组合验证方式,以保证通信的安全性,例如在移动端与服务器通信,或者是通过公网进行跨服务器通信的场景下,建议开启加密策略。
import _ from 'lodash';
import grpc, { handleUnaryCall } from 'grpc';
import { TaskConf, TaskResult } from './task.types';
import { proto } from './proto';
const runTask: handleUnaryCall<TaskConf, TaskResult> = (call, callback) => {
const { id, job } = call.request;
console.log(`run task: #${ id } ${ job }`);
return callback(null, {
status: 'ok',
result: job
});
};
const server = new grpc.Server();
server.addService(proto.Task.service, {
runTask, runTaskProgress
});
server.bind('0.0.0.0:9050', grpc.ServerCredentials.createInsecure());
server.start();
- 客户端 client.ts
import grpc from 'grpc';
import { TaskResult } from './task.types';
import { proto } from './proto';
const client = new proto.Task('localhost:9050', grpc.credentials.createInsecure());
client.runTask({
id: '001',
job: 'test'
}, (err: Error, results: TaskResult) => {
if (err) {
console.error(err.stack);
} else {
console.log(`[${ results.status }] ${ results.result }`);
}
});
注意到 client 上使用的 runTask 方法是由 proto buffer 客户端模块初始化过程中动态注册的,如需使用严格的强类型调用方法 makeUnaryRequest,需要使用前面所说到的处理方式,定义 proto 对象的类型规约,并需要显示申明 client 的序列化与反序列化方法。
运行示例
分别启动服务端与客户端,可以看到客户端提交的任务信息,在服务端被正确执行,并返回状态给客户端输出。
遇到的问题
- 目前 grpc 对于 typescript 的支持不是很友好,其提供的 grpc 对象原生调用方法,使用上不如官方示例采用的动态注册 + 调用的方式来的便捷,且从 Github 上可以看到经常会因为类型规约维护不及时出现的 issue。
- grpc 提供的预编译模块与原生 js 模块之间的类型申明也存在不少的差异,不便于在纯 js 模式下开发与原生预编译模块之间切换。虽然情况下都会优先使用预编译的 C 模块进行对接,以获得最佳性能。
参考文档:
- https://grpc.io/docs/tutorials/basic/node.html
- https://www.npmjs.com/package/@grpc/proto-loader
- https://developers.google.com/protocol-buffers/docs/overview
最后修改于 2019-04-15