## gRPC
[gRPC](https://github.com/grpc/grpc-node) 是一個現代的、高性能RPC框架,可以運行在任何環境下。它可以有效在數據中心之間連接服務,并通過插件支持負載均衡、跟蹤、健康診斷和授權。
和很多RPC系統一樣,gRPC基于可以定義遠程調用的函數(方法)的概念。針對每個方法,定義一個參數并返回類型。服務、參數和返回類型在`.proto`文件中定義,使用谷歌的開源語言——中性[協議緩存(protocol buffers)](https://developers.google.com/protocol-buffers)機制。
使用gRPC傳輸器,Nest使用`.proto`文件來動態綁定客戶端和服務以簡化遠程調用并自動序列化和反序列化結構數據。
### 安裝
在開始之前,我們必須安裝所需的軟件包:
```
$ npm i --save @grpc/grpc-js @grpc/proto-loader
```
### 概述
類似其他微服務傳輸器層的實現,要使用gRPC傳輸器機制,你需要像下面的示例一樣給`createMicroservice()`方法傳遞指定傳輸器`transport`屬性和可選的`options`屬性。
> main.ts
```typescript
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` 屬性以便于部署非ts文件,添加 `watchAssets` 來對assets文件們進行監聽,就grpc而言,我們希望 `.proto` 文件自動復制到 `dist` 文件夾下
```typescript
{
"compilerOptions": {
"assets": ["**/*.proto"],
"watchAssets": true
}
}
```
### 選項
**gRPC**傳輸器選項暴露了以下屬性。
|||
|---|---|
package|`Protobuf`包名稱(與.proto文件定義的相匹配)。必須的
protoPath|`.proto`文件的絕對或相對路徑,必須的。
url|連接url,字符串,格式為`ip address/dns name:port` (例如, 'localhost:50051') 定義傳輸器連接的地址/端口,可選的。默認為'localhost:5000'
protoLoader|用來調用`.proto`文件的NPM包名,可選的,默認為'@grpc/proto-loader'
loader| `@grpc/proto-loader`選項。可以控制`.proto`文件更多行為細節,可選的。[參見這里](https://github.com/grpc/grpc-node/blob/master/packages/proto-loader/README.md)。
credentials|服務器憑證,可選的。([參見更多](https://grpc.io/grpc/node/grpc.ServerCredentials.html))
### 示例`gRPC`服務
我們定義`HeroesService`示例gRPC服務。在上述的`options`對象中, `protoPath` 是設置`.proto`定義文件`hero.proto`的路徑。`hero.proto` 文件是使用協議緩沖區語言構建的。
> hero.proto
```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;
}
```
在上面的示例中,我們定義了一個 `HeroService`,它暴露了一個 `FindOne()` gRPC處理程序,該處理程序期望 `HeroById` 作為輸入并返回一個 `Hero` 消息(協議緩沖語言使用`message`元素定義參數類型和返回類型)。
接下來,需要實現這個服務。如下在控制器中使用`@GrpcMethod()`裝飾器來定義一個滿足要求的處理程序。這個裝飾器提供了要聲明gRPC服務方法的元數據。
> 之前章節中介紹的`@MessagePattern()`裝飾器([閱讀更多](https://docs.nestjs.com/microservices/basics#request-response))在基于gRPC的微服務中不適用。基于gPRC的微服務使用`@GrpcMethod()`裝飾器。
> hero.controller.ts
```typescript
@Controller()
export class HeroesController {
@GrpcMethod('HeroesService', 'FindOne')
findOne(data: HeroById, metadata: Metadata, call: ServerUnaryCall<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()`方法。
`findone()`處理程序方法有三個參數,`data`從調用者中傳遞,`metadata`保存了gRPC需要的元數據,`call`用于獲取`GrpcCall`對象屬性,例如`sendMetadata`以像客戶端發送元數據。
`@GrpcMethod()`裝飾器兩個參數都是可選的,如果不指定第二個參數(例如`FindOne`),Nest會自動將`.proto`文件中的rpc方法與處理程序相關聯,并將rpc處理程序名稱轉換為大寫駱駝格式(例如,`findOne`處理器與`FindOne`rpc調用定義關聯),如下:
> hero.controller.ts
```typescript
@Controller()
export class HeroesController {
@GrpcMethod('HeroesService')
findOne(data: HeroById, metadata: Metadata, call: ServerUnaryCall<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`服務的處理器方法相關聯,以`HeroesService`名稱相匹配。
> hero.controller.ts
```typescript
@Controller()
export class HeroesService {
@GrpcMethod()
findOne(data: HeroById, metadata: Metadata, call: ServerUnaryCall<any>): Hero {
const items = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Doe' },
];
return items.find(({ id }) => id === data.id);
}
}
```
### 客戶端
Nest應用可以作為gRPC客戶端,消費`.proto`文件定義的服務。你可以使用`ClientGrpc`對象調用遠程服務。可以通過幾種方式調用`ClientGrpc`對象。
推薦的技術是導入`ClientModule`,使用`register()` 方法綁定一個在`.proto`文件中定義的服務包以注入token并配置服務。`name`屬性是注入的token。在gRPC服務中,使用`transport:Transport.GRPC`,`options`屬性和前節相同。
```typescript
imports: [
ClientsModule.register([
{
name: 'HERO_PACKAGE',
transport: Transport.GRPC,
options: {
package: 'hero',
protoPath: join(__dirname, 'hero/hero.proto'),
},
},
]),
];
```
> `register()`方法包含一個對象數組。通過逗號分隔注冊對象以注冊多個對象。
注冊后,可以使用`@Inject()`注入配置的`ClientGrpc`對象。然后使用`ClientGrpc`對象的`getService()`方法來獲取服務實例,如下:
```typescript
@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客戶端不會發送名稱包含下劃線`_`的字段,除非`keepCase`選項在`proto`裝載配置中(`options.loader.keepcase`在微服務傳輸器配置中)被配置為`true`。
注意,和其他微服務傳輸器方法相比,這里的技術有一點細微的區別。使用`ClientGrpc`代替`ClientProxy`類,提供`getService()`方法,使用一個服務名稱作為參數并返回它的實例(如果存在)。
也可以使用 `@Client()` 裝飾器來初始化`ClientGrpc`對象,如下:
```typescript
@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屏蔽了網絡通訊的細節,`herosService`看上去和本地服務一樣。
注意,所有這些都是 **小寫** (為了遵循自然慣例)。基本上,我們的`.proto`文件 `HeroService` 定義包含 `FindOne()` 函數。這意味著 `heroService` 實例將提供 `findOne()` 方法。
```typescript
interface HeroService {
findOne(data: { id: number }): Observable<any>;
}
```
消息處理程序也可以返回一個`Observable`,在流完成之后其結果值會被發出。
> hero.controller.ts
```typescript
@Get()
call(): Observable<any> {
return this.heroService.findOne({ id: 1 });
}
```
要發送gRPC元數據(隨請求),可以像如下這樣傳遞第二個參數:
```typescript
call(): Observable<any> {
const metadata = new Metadata();
metadata.add('Set-Cookie', 'yummy_cookie=choco');
return this.heroesService.findOne({ id: 1 }, metadata);
}
```
> `Metadata`類從`grpc`包中導入。
注意,這可能需要更新我們在之前步驟中定義的`HeroesService`接口。
#### 示例
[這里](https://github.com/nestjs/nest/tree/master/sample/04-grpc) 提供了一個完整的示例。
### gRPC流
`GRPC` 本身支持長期的實時連接(稱為流)。 對于諸如聊天,熱評或數據塊傳輸之類的服務案例,流可以是非常有用的工具。 您可以在官方文檔([此處](https://grpc.io/docs/guides/concepts/))中找到更多詳細信息。
`Nest` 通過兩種可能的方式支持 `GRPC`流處理程序:
- `RxJS Subject + Observable` 處理程序:可用于在`Controller` 內部編寫響應或將其傳遞給 `Subject / Observable`使用者。
- `Pure GRPC` 調用流處理程序:將其傳遞給某個執行程序非常有用,后者將處理節點標準雙工流處理程序的其余分派。
### 流示例
定義一個示例的gRPC服務,名為`HelloService`。`hello.proto`文件使用協議緩沖語言組織,如下:
```typescript
// 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`接口。
```typescript
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包自動生成,在](https://github.com/stephenh/ts-proto)[這里](https://github.com/stephenh/ts-proto/blob/main/NESTJS.markdown)了解更多。
### 主題策略
`@GrpcStreamMethod()` 裝飾器提供`RxJS Observable`的函數參數,也就是說,我們可以接收和處理多個消息。
```typescript
@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()` 裝飾器的全雙工交互,需要從`Controller` 方法中返回 `RxJS Observable`。
> `Metadata`和`ServerUnaryCall`類/接口從`grpc`包中導入。
依據服務定義(在`.proto`文件中),`BidiHello`方法需要向服務發送流請求。要從客戶端發送多個異步消息到流,需要暴露一個RxJS的`ReplySubject`類。
```typescript
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()`調用)。
### 調用流處理程序
當返回值被定義為`stream`時,`@GrpcStreamCall()`裝飾器提供了一個`grpc.ServerDuplexStream`作為函數參數,支持標準的 `.on('data', callback)`、`.write(message)`或 `.cancel()`方法,有關可用方法的完整文檔可在[此處](https://grpc.github.io/grpc/node/grpc-ClientDuplexStream.html)找到。
可選的,當方法返回值不是`stream`時,`@GrpcStreamCall()`裝飾器提供兩個函數參數,分別為`grpc.ServerReadableStream` ([參見這里](https://grpc.github.io/grpc/node/grpc-ServerReadableStream.html)) 和`callback`。
接下來開始應用`BidiHello`,它應該支持全雙工交互。
```typescript
@GrpcStreamCall()
bidiHello(requestStream: any) {
requestStream.on('data', message => {
console.log(message);
requestStream.write({
reply: 'Hello, world!'
});
});
}
```
> 此裝飾器不需要提供任何特定的返回參數。 可以像對待任何其他標準流類型一樣處理流。
在上述示例中,使用`write()`方法將對象寫入響應流。將回調信息作為第二個參數傳遞給`.on()`方法,當服務每次收到收據塊時會進行調用。
應用`LotsOfGreetings`方法:
```typescript
@GrpcStreamCall()
lotsOfGreetings(requestStream: any, callback: (err: unknown, value: HelloResponse) => void) {
requestStream.on('data', message => {
console.log(message);
});
requestStream.on('end', () => callback(null, { reply: 'Hello, world!' }));
}
```
這里使用`callback`函數在`requestStream`完成時來發送響應。
### gRPC 元數據
元數據是一系列反應特定RPC調用信息的鍵值對,鍵是字符串格式,值通常是字符串,但也可以是二進制數據。元數據對gRPC自身而言是不透明的,客戶端向服務器發送信息時攜帶元數據信息,反之亦然。元數據包含認證token,請求指示器和監控用途的標簽,以及數據信息例如數據集中的記錄數量。
要在`@GrpcMethod()`處理程序中讀取元數據,使用第二個參數(元數據),類型為`Metadata`(從`grpc`包中導入)。
要從處理程序中發回元數據,使用`ServerUnaryCall#sendMetadata()`方法(第三個處理程序參數)。
>heroes.controller.ts
```typescript
@Controller()
export class HeroesService {
@GrpcMethod()
findOne(data: HeroById, metadata: Metadata, call: ServerUnaryCall<any>): Hero {
const serverMetadata = new Metadata();
const items = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Doe' },
];
serverMetadata.add('Set-Cookie', 'yummy_cookie=choco');
call.sendMetadata(serverMetadata);
return items.find(({ id }) => id === data.id);
}
}
```
類似地,要使用`@GrpcStreamMethod()`處理程序(主題策略)在處理程序注釋中讀取元數據,使用第二個參數(元數據),類型為`Metadata`(從`grpc`包中導入)。
要從處理程序中發回元數據,使用`ServerDuplexStream#sendMetadata()`方法(第三個處理程序參數)。
要從[call stream handlers](https://docs.nestjs.com/microservices/grpc#call-stream-handler)(使用`@GrpcStreamCall()`裝飾器注釋的處理程序)中讀取元數據,監聽`requestStream`引用中的`metadata`事件。
```typescript
requestStream.on('metadata', (metadata: Metadata) => {
const meta = metadata.get('X-Meta');
});
```
- 介紹
- 概述
- 第一步
- 控制器
- 提供者
- 模塊
- 中間件
- 異常過濾器
- 管道
- 守衛
- 攔截器
- 自定義裝飾器
- 基礎知識
- 自定義提供者
- 異步提供者
- 動態模塊
- 注入作用域
- 循環依賴
- 模塊參考
- 懶加載模塊
- 應用上下文
- 生命周期事件
- 跨平臺
- 測試
- 技術
- 數據庫
- Mongo
- 配置
- 驗證
- 緩存
- 序列化
- 版本控制
- 定時任務
- 隊列
- 日志
- Cookies
- 事件
- 壓縮
- 文件上傳
- 流式處理文件
- HTTP模塊
- Session(會話)
- MVC
- 性能(Fastify)
- 服務器端事件發送
- 安全
- 認證(Authentication)
- 授權(Authorization)
- 加密和散列
- Helmet
- CORS(跨域請求)
- CSRF保護
- 限速
- GraphQL
- 快速開始
- 解析器(resolvers)
- 變更(Mutations)
- 訂閱(Subscriptions)
- 標量(Scalars)
- 指令(directives)
- 接口(Interfaces)
- 聯合類型
- 枚舉(Enums)
- 字段中間件
- 映射類型
- 插件
- 復雜性
- 擴展
- CLI插件
- 生成SDL
- 其他功能
- 聯合服務
- 遷移指南
- Websocket
- 網關
- 異常過濾器
- 管道
- 守衛
- 攔截器
- 適配器
- 微服務
- 概述
- Redis
- MQTT
- NATS
- RabbitMQ
- Kafka
- gRPC
- 自定義傳輸器
- 異常過濾器
- 管道
- 守衛
- 攔截器
- 獨立應用
- Cli
- 概述
- 工作空間
- 庫
- 用法
- 腳本
- Openapi
- 介紹
- 類型和參數
- 操作
- 安全
- 映射類型
- 裝飾器
- CLI插件
- 其他特性
- 遷移指南
- 秘籍
- CRUD 生成器
- 熱重載
- MikroORM
- TypeORM
- Mongoose
- 序列化
- 路由模塊
- Swagger
- 健康檢查
- CQRS
- 文檔
- Prisma
- 靜態服務
- Nest Commander
- 問答
- Serverless
- HTTP 適配器
- 全局路由前綴
- 混合應用
- HTTPS 和多服務器
- 請求生命周期
- 常見錯誤
- 實例
- 遷移指南
- 發現
- 誰在使用Nest?