gRPC
gRPC 是一个高性能、通用的开源 RPC 框架,其由 Google 主要面向移动应用开发并基于 HTTP/2 协议标准而设计,基于 ProtoBuf(Protocol Buffers) 序列化协议开发,且支持众多开发语言。
本篇内容演示了如何在 Midway 体系下,提供 gRPC 服务,以及调用 gRPC 服务的方法。
Midway 当前采用了最新的 gRPC 官方推荐的 @grpc/grpc-js 进行开发,并提供了一些工具包,用于快速发布服务和调用服务。
我们使用的模块为 @midwayjs/grpc
,既是一个框架(可以独立发布服务),又是一个组件(可以接入其它框架调用 gRPC 服务)。
创建示例
$ npm -v
# 如果是 npm v6
$ npm init midway --type=grpc my_midway_app
# 如果是 npm v7
$ npm init midway -- --type=grpc my_midway_app
此示例包含一个 gRPC 服务。
目录结构
.
├── package.json
├── proto ## proto 定义文件
│ └── helloworld.proto
├── src
│ ├── configuration.ts ## 入口配置文件
│ ├── interface.ts
│ └── provider ## gRPC 提供服务的文件
│ └── greeter.ts
├── test
├── bootstrap.js ## 服务启动入口
└── tsconfig.json
定义服务接口
在微服务中,定义一个服务需要特定的接口定义语言(IDL)来完成,在 gRPC 中 默认使用 Protocol Buffers 作为序列化协议。
序列化协议独立于语言和平台,提供了多种语言的实现,Java,C++,Go 等等,每一种实现都包含了相应语言的编译器和库文件。所以 gRPC 是一个提供和调用都可以跨语言的服务框架。
一个 gRPC 服务的大体架构可以用官网上的一幅图表示。
Protocol Buffers 协议的文件,默认的后缀为 .proto
。.proto 后缀的 IDL 文件,并通过其编译器生成特定语言的数据结构、服务端接口和客户端 Stub 代码。
由于 proto 文件可以跨语言使用,为了方便共享,我们一般将 proto 文件放在 src 目录外侧,方便其他工具复制分发。
下面是一个基础的 proto/helloworld.proto
文件。
syntax = "proto3";
package helloworld;
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
proto3 表示的是第三版的 protobuf 协议,是 gRPC 目前推荐的版本,“语法简单,功能更全”。
我们可以用 service
格式,定义服务体,其中可以包含方法。同时,我们可以更加细致的通过 message
描述服务具体的请求参数和响应参数。
我们可以从 Google 的官网文档 中查看更多细节。
大家会看到,这和 Java 中的 Class 非常相像,每个结构就相当于 Java 中的一个类。
编写 proto 文件
现在我们再来看之前的服务,是不是就很好理解了。
syntax = "proto3";
package helloworld;
// 服务的定义
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// 服务的请求参数
message HelloRequest {
string name = 1;
}
// 服务的响应参数
message HelloReply {
string message = 1;
}
我们定义了一个名为 Greeter
的服务,包含一个 HelloRequest
结构的请求体,以及返回 HelloReply
结构的响应体。
接下去,我们将对这个服务给大家做演示。
生成代码定义
传统的 gRPC 框架,需要用户手动编写 proto 文件,以及生成 js 服务,最后再根据 js 生成的服务再编写实现,在 Midway 体系下,我们提供了一个 grpc-helper 工具包来加速这个过程。
如果没有安装,可以先安装(脚手架示例中已带)。
$ npm i @midwayjs/grpc-helper --save-dev
grpc-helper 工具的作用,是将用户提供的 proto 文件,生成对应可读的 ts interface 文件。
我们可以添加一个脚本,方便这个过程。
{
"scripts": {
"generate": "tsproto --path proto --output src/domain"
}
}
然后执行 npm run generate
。
上述命令执行后,会在代码的 src/domain
目录中生成 proto 文件对应的服务接口定义。
不管是提供 gRPC 服务还是调用 gRPC 服务,都要先生成定义。
生成的代码如下,包含有一个命名空间(namespace),以及命名空间下的两个 TypeScript Interface, Greeter
用于编写服务端实现, GreeterClient
用于编写客户端实现。
/**
* This file is auto-generated by grpc-helper
*/
import * as grpc from '@midwayjs/grpc';
// 生成的命名空间
export namespace helloworld {
// 服务端使用的定义
export interface Greeter {
// Sends a greeting
sayHello(data: HelloRequest): Promise<HelloReply>;
}
// 客户端使用的定义
export interface GreeterClient {
// Sends a greeting
sayHello(options?: grpc.IClientOptions): grpc.IClientUnaryService<HelloRequest, HelloReply>;
}
// 请求体结构
export interface HelloRequest {
name?: string;
}
// 响应体结构
export interface HelloReply {
message?: string;
}
}
每当 proto 文件被修改时,就需要重新生成对应的服务定义,然后将对应的方法实现。
提供 gRPC 服务(Provider)
编写生产者(Provider)
在 src/provider
目录中,我们创建 greeter.ts
,内容如下。
import { MSProviderType, Provider, Provide, GrpcMethod } from '@midwayjs/decorator';
import { helloworld } from '../domain/helloworld';
/**
* 实现 helloworld.Greeter 接口的 服务
*/
@Provide()
@Provider(MSProviderType.GRPC, { package: 'helloworld' })
export class Greeter implements helloworld.Greeter {
@GrpcMethod()
async sayHello(request: helloworld.HelloRequest) {
return { message: 'Hello ' + request.name };
}
}
注意,@Provider 装饰器和 @Provide 装饰器不同,前者用于提供服务,后者用于依赖注入容器扫描标识的类。
我们使用 @Provider
暴露出一个 RPC 服务, @Provider
的第一个参数为 RPC 服务类型,这个参数是个枚举,这里选择 GRPC 类型。
@Provider
的第二个参数为 RPC 服务的元数据,这里指代的是 gRPC 服务的元数据。这里需要写入 gRPC 的 package 字段,即 proto 文件中的 package 字段(这里的字段用于和 proto 文件加载后的字段做对应)。
对于普通的 gRPC 服务接口(UnaryCall),我们只需要使用 @GrpcMethod()
装饰器修饰即可。修饰的方法即为服务定义本身,入参为 proto 中定义好的入参,return 值即为定义好的响应体。
注意,生成的 Interface 是为了更好的编写服务代码,规范结构,请务必按照定义编写。
启动 gRPC 服务
这里启动需要用到项目根目录 bootstrap.js
独立文件。代码和其他框架初始化类似,只是这里的框架包是 @midwayjs/grpc
。
内容如下:
// 获取框架
const { Framework } = require('@midwayjs/grpc');
const { join } = require('path');
// 初始化框架
const grpcService = new Framework().configure({
services: [
{
protoPath: join(__dirname, 'proto/helloworld.proto'),
package: 'helloworld',
},
],
});
// 使用 bootstrap 启动
const { Bootstrap } = require('@midwayjs/bootstrap');
Bootstrap.load(grpcService).run();
我们已经将启动命令写到了 start 脚本中,执行 npm run start
即可。
"scripts": {
"start": "NODE_ENV=production node ./bootstrap.js",
},
在部署前,需要执行 npm run build 将 ts 代码编译为 js。
框架选项
@midwayjs/grpc
作为框架启动时,可以传递的参数如下:
url | string | 可选,gRPC 服务连接字符串,默认为 localhost:6565 |
---|---|---|
services | IGRPCServiceOptions[] | 必选,数组,需要暴露的 gRPC 服务信息,每个服务对应一个 proto 文件 |
loaderOptions | object | 可选,使用 @grpc/proto-loader 加载的选项,具体参考这里,默认为 |
{ keepCase: true, longs: String, enums: String, defaults: true, oneofs: true, }
|
| credentials | ServerCredentials | 可选,服务凭证,值参考这里,默认值为 ServerCredentials.createInsecure() |
services 字段是数组,意味着 Midway 项目可以同时发布多个 gRPC 服务。每个 service 的结构为:
protoPath | string | 必选,proto 文件的绝对路径 |
---|---|---|
package | string | 必选,服务对应的 package |
编写单元测试
@midwayjs/grpc
库提供了 一个 createGRPCConsumer
方法,用于实时调用客户端,一般我们用这个方法做测试。
这个方法每次调用会实时连接,不建议将该方法用在生产环境。
在测试中写法如下。
import { createApp, close } from '@midwayjs/mock';
import { Framework, createGRPCConsumer } from '@midwayjs/grpc';
import { join } from 'path';
import { helloworld } from '../src/domain/helloworld';
describe('test/index.test.ts', () => {
it('should create multiple grpc service in one server', async () => {
const baseDir = join(__dirname, '../');
// 创建服务
const app = await createApp<Framework>(baseDir, {
services: [
{
protoPath: join(baseDir, 'proto', 'helloworld.proto'),
package: 'helloworld',
},
],
});
// 调用服务
const service = await createGRPCConsumer<helloworld.GreeterClient>({
package: 'helloworld',
protoPath: join(baseDir, 'proto', 'helloworld.proto'),
url: 'localhost:6565',
});
const result = await service.sayHello().sendMessage({
name: 'harry',
});
expect(result.message).toEqual('Hello harry');
await close(app);
});
});