gRPC
gRPC 是一个现代、开源、高性能的 RPC 框架, 可以在任何环境中运行。 它可以有效地连接数据中心内外的服务,并支持可插拔的负载均衡、跟踪、健康检查和身份验证。
与许多 RPC 系统一样,gRPC 基于在术语上定义服务的概念,这些服务可以远程调用。
对于每个方法,您定义参数和返回类型。
服务、参数和返回类型是使用 Google 的开源语言中立协议缓冲区
机制在 .proto
文件中定义的。
使用 gRPC 传输器,Nest 使用 .proto
文件动态绑定客户端和服务器,
以便轻松实现远程过程调用,并自动对结构化数据进行序列化和反序列化。
安装
要开始构建基于 gRPC 的微服务,请首先安装所需的包:
npm i --save @grpc/grpc-js @grpc/proto-loader
概述
与其他 Nest 微服务传输层实现一样,
您可以使用传递给 createMicroservice()
方法的 options
对象的 transport
属性选择 gRPC 传输机制。
在下面的示例中,我们将设置一个英雄服务。
options
属性提供了关于该服务的元数据;其属性如下所述。
const app = await NestFactory.createMicroservice<MicroserviceOptions>(AppModule, {
transport: Transport.GRPC,
options: {
package: 'hero',
protoPath: join(__dirname, 'hero/hero.proto'),
},
});
join()
函数是从 path
包中导入的;
Transport
枚举是从 @nestjs/microservices
包中导入的。
在 nest-cli.json
文件中,我们添加了 assets
属性,
该属性允许我们分发非 TypeScript 文件,
以及 watchAssets
用于打开对所有非 TypeScript 资源的监视。
在我们的情况下,我们希望将 .proto
文件自动复制到 dist
文件夹。
{
"compilerOptions": {
"assets": ["**/*.proto"],
"watchAssets": true
}
}
选项
gRPC 传输器选项对象公开了以下描述的属性。
package
:Protobuf 包名称(与.proto
文件中的package
设置相匹配)。必需。protoPath
:.proto
文件的绝对路径(或相对于根目录)。必需。url
:连接 URL。字符串的格式为ip address/dns:port
(例如,'localhost:50051'
),定义传输器在哪个地址/端口上建立连接。可选。默认为'localhost:5000'
。protoLoader
:用于加载.proto
文件的 NPM 包名称。可选。默认为'@grpc/proto-loader'
。loader
:@grpc/proto-loader
的选项。这些选项可以详细控制.proto
文件的行为。可选。有关更多详细信息,请参阅这里。credentials
:服务器凭据。可选。了解更多。
示例 gRPC 服务
让我们定义一个名为 HeroesService
的示例 gRPC 服务。
在上述选项对象中,protoPath
属性设置了一个路径,
指向 .proto
定义文件 hero.proto
。hero.proto
文件使用
Protocol Buffers 进行结构化。
以下是它的样子:
// hero/hero.proto
syntax = "proto3";
package hero;
service HeroesService {
rpc FindOne (HeroById) returns (Hero) {}
}
message HeroById {
int32 id = 1;
}
message Hero {
int32 id = 1;
string name = 2;
}
我们的 HeroesService
公开了一个 FindOne()
方法。
该方法期望一个类型为 HeroById
的输入参数,
并返回一个 Hero
消息(Protocol Buffers 使用消息元素来定义参数类型和返回类型)。
接下来,我们需要实现这个服务。
为了定义一个满足这一定义的处理程序,我们在控制器中使用 @GrpcMethod()
装饰器,
如下所示。该装饰器提供将方法声明为 gRPC 服务方法所需的元数据。
前面的微服务章节中介绍的 @MessagePattern()
装饰器(了解更多)
不适用于基于 gRPC 的微服务。
@GrpcMethod() 装饰器有效地取代了基于 gRPC 的微服务。
@Controller()
export class HeroesController {
@GrpcMethod('HeroesService', 'FindOne')
findOne(data: HeroById, metadata: Metadata, call: ServerUnaryCall<any, any>): Hero {
const items = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Doe' },
];
return items.find(({ id }) => id === data.id);
}
}
@GrpcMethod()
装饰器是从 @nestjs/microservices
包中导入的,
Metadata
和 ServerUnaryCall
是从 grpc
包中导入的。
上述装饰器接受两个参数。第一个是服务名称(例如,'HeroesService'
),
对应于 hero.proto
文件中的 HeroesService
服务定义。
第二个参数(字符串 'FindOne'
)对应于在
hero.proto
文件中的 HeroesService
中定义的 FindOne()
rpc 方法。
findOne()
处理程序方法接受三个参数:
来自调用者的data
、存储 gRPC 请求元数据的 metadata
,
以及用于获取 GrpcCall
对象属性的 call
。
(例如用于将元数据发送到客户端的 sendMetadata
)
这两个 @GrpcMethod()
装饰器的参数都是可选的。
如果没有第二个参数调用(例如 'FindOne'
),
Nest 将根据将处理程序名称转换为大驼峰形式来自动将
.proto
文件的 rpc 方法与处理程序关联起来(例如,
findOne
处理程序将与 FindOne
rpc 调用定义相关联)。
以下是示例:
@Controller()
export class HeroesController {
@GrpcMethod('HeroesService')
findOne(data: HeroById, metadata: Metadata, call: ServerUnaryCall<any, any>): Hero {
const items = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Doe' },
];
return items.find(({ id }) => id === data.id);
}
}
你还可以省略第一个 @GrpcMethod()
装饰器的参数。
在这种情况下,Nest 将根据处理程序定义的类名自动将处理程序与 proto 定义文件中的服务定义相关联。
例如,以下代码中,HeroesService
类将其处理程序方法与 hero.proto
文件中的 HeroesService
服务定义相关联:
@Controller()
export class HeroesService {
@GrpcMethod()
findOne(data: HeroById, metadata: Metadata, call: ServerUnaryCall<any, any>): Hero {
const items = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Doe' },
];
return items.find(({ id }) => id === data.id);
}
}
客户端
Nest 应用程序可以作为 gRPC 客户端,消耗在 .proto
文件中定义的服务。
您可以通过 ClientGrpc
对象访问远程服务。
可以通过几种方式获得 ClientGrpc
对象。
首选的技术是导入 ClientsModule
。
使用 register()
方法将在 .proto
文件中定义的一组服务绑定到一个注入令牌,
并配置服务。name
属性是注入令牌。
对于 gRPC 服务,请使用 transport: Transport.GRPC
。
options
属性是具有上述相同属性的对象。
imports: [
ClientsModule.register([
{
name: 'HERO_PACKAGE',
transport: Transport.GRPC,
options: {
package: 'hero',
protoPath: join(__dirname, 'hero/hero.proto'),
},
},
]),
];
register()
方法接受一个对象数组。通过提供一个以逗号分隔的注册对象列表,可以注册多个包。
注册后,我们可以使用 @Inject()
注入配置的 ClientGrpc
对象。
然后,我们使用 ClientGrpc
对象的 getService()
方法检索服务实例,
如下所示。
@Injectable()
export class AppService implements OnModuleInit {
private heroesService: HeroesService;
constructor(@Inject('HERO_PACKAGE') private client: ClientGrpc) {}
onModuleInit() {
this.heroesService = this.client.getService<HeroesService>('HeroesService');
}
getHero(): Observable<string> {
return this.heroesService.findOne({ id: 1 });
}
}
gRPC 客户端不会发送字段名称中包含下划线 _
的字段,
除非在 proto 加载器配置(options.loader.keepcase
在微服务传输器配置中)
中设置 keepCase
选项为 true
。
请注意,与其他微服务传输方法使用的技术相比,这里有一个小差异。
我们不使用 ClientProxy
类,
而是使用提供了 getService()
方法的 ClientGrpc
类。
getService()
泛型方法接受服务名称作为参数,并返回其实例(如果可用)。
或者,您可以使用 @Client()
装饰器来实例化 ClientGrpc
对象,如下所示:
@Injectable()
export class AppService implements OnModuleInit {
@Client({
transport: Transport.GRPC,
options: {
package: 'hero',
protoPath: join(__dirname, 'hero/hero.proto'),
},
})
client: ClientGrpc;
private heroesService: HeroesService;
onModuleInit() {
this.heroesService = this.client.getService<HeroesService>('HeroesService');
}
getHero(): Observable<string> {
return this.heroesService.findOne({ id: 1 });
}
}
最后,对于更复杂的场景,我们可以使用 ClientProxyFactory
类注入一个动态配置的客户端,
详情请参阅这里。
无论哪种方式,我们最终都获得了对我们的 HeroesService
代理对象的引用,
该对象公开了与 .proto
文件中定义的一组方法相同的方法。
现在,当我们访问此代理对象(即 heroesService
)时,
gRPC 系统将自动序列化请求,将其转发到远程系统,返回响应并反序列化响应。
由于 gRPC 为我们屏蔽了这些网络通信细节,
heroesService
看起来和操作起来就像是一个本地提供者。
请注意,所有服务方法都是小驼峰式的(为了遵循语言的自然约定)。
因此,例如,虽然我们的 .proto
文件 HeroesService
定义包含了 FindOne()
函数,
但 heroesService
实例将提供 findOne()
方法。
interface HeroesService {
findOne(data: { id: number }): Observable<any>;
}
消息处理程序还可以返回 Observable
,在这种情况下,结果值将在流完成之前发出。
@Get()
call(): Observable<any> {
return this.heroesService.findOne({ id: 1 });
}
要发送 gRPC 元数据(与请求一起),可以传递第二个参数,如下所示:
call(): Observable<any> {
const metadata = new Metadata();
metadata.add('Set-Cookie', 'yummy_cookie=choco');
return this.heroesService.findOne({ id: 1 }, metadata);
}
Metadata
类是从 grpc
包中导入的。
请注意,这将要求更新我们前面定义的 HeroesService
接口。
示例
一个可运行的示例可在这里找到。
gRPC 流
gRPC 本身支持长期的实时连接,传统上称为流。 流对于聊天、观测或分块数据传输等情况非常有用。 在官方文档中可以找到更多详细信息。
Nest 支持两种方式的 GRPC 流处理:
RxJS Subject + Observable
处理程序: 可以用于直接在控制器方法中编写响应,或传递给Subject/Observable
消费者。- 纯 GRPC 调用流处理程序:
可以用于传递给一些执行器,该执行器将处理 Node 标准的双工流(
Duplex
)处理程序的其余部分。
流式示例
让我们定义一个名为 HelloService
的新示例 gRPC 服务。
hello.proto
文件使用协议缓冲区进行结构化。以下是其内容:
// hello/hello.proto
syntax = "proto3";
package hello;
service HelloService {
rpc BidiHello(stream HelloRequest) returns (stream HelloResponse);
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse);
}
message HelloRequest {
string greeting = 1;
}
message HelloResponse {
string reply = 1;
}
LotsOfGreetings
方法可以简单地使用 @GrpcMethod
装饰器实现(如上面的示例),因为返回的流可以发射多个值。
基于此 .proto
文件,让我们定义 HelloService
接口:
interface HelloService {
bidiHello(upstream: Observable<HelloRequest>): Observable<HelloResponse>;
lotsOfGreetings(
upstream: Observable<HelloRequest>,
): Observable<HelloResponse>;
}
interface HelloRequest {
greeting: string;
}
interface HelloResponse {
reply: string;
}
proto
接口可以通过 ts-proto
包自动生成,详情请参阅这里。
Subject 策略
@GrpcStreamMethod()
装饰器将函数参数提供为 RxJS Observable
。
因此,我们可以接收和处理多个消息。
@GrpcStreamMethod()
bidiHello(messages: Observable<any>, metadata: Metadata, call: ServerDuplexStream<any, any>): Observable<any> {
const subject = new Subject();
const onNext = message => {
console.log(message);
subject.next({
reply: 'Hello, world!'
});
};
const onComplete = () => subject.complete();
messages.subscribe({
next: onNext,
complete: onComplete,
});
return subject.asObservable();
}
为了支持 @GrpcStreamMethod()
装饰器的全双工交互,控制器方法必须返回 RxJS Observable。
Metadata
和 ServerUnaryCall
类/接口是从 grpc
包中导入的。
根据服务定义(在 .proto
文件中),BidiHello
方法应将请求流式传输到服务。
要从客户端向流发送多个异步消息,我们利用了 RxJS 的 ReplaySubject
类。
const helloService = this.client.getService<HelloService>('HelloService');
const helloRequest$ = new ReplaySubject<HelloRequest>();
helloRequest$.next({ greeting: 'Hello (1)!' });
helloRequest$.next({ greeting: 'Hello (2)!' });
helloRequest$.complete();
return helloService.bidiHello(helloRequest$);
在上面的示例中,我们向流写入了两条消息(next()
调用),
并通知服务我们已经完成了数据的发送(complete()
调用)。