gRPC在Nest中的尝试

简要列举grpc相对于传统restful api的优势

  • 性能高效:通过 HTTP/2 和 Protobuf,大大降低了数据传输的延迟和带宽消耗。
  • 多种通信模式:支持双向流式、单向流式等复杂的通信模式。
  • 跨语言支持:适合构建跨语言的分布式系统。
  • 自动化代码生成:基于接口定义自动生成客户端和服务端代码,减少重复工作。
  • 内建的负载均衡和安全机制:让微服务架构中的通信更加高效、可靠和安全。

关于grpc的gui github.com/bloomrpc/bl... 导入proto文件即可,方法会自动引入,配置参数和地址即可

实现

在一个目录下创建两个项目,一个作为client,一个作为server,proto来管理公共proto

公共目录创建proto文件

proto 复制代码
syntax = "proto3";

package person;

// Generated according to https://cloud.google.com/apis/design/standard_methods
service PersonService {
  rpc FindOne (PersonById) returns(Person) {}
  rpc FindAll (Empty) returns(People) {}
}

message PersonById {
  int32 id = 1;
}

message Person {
  int32 id = 1;
  string name = 2;
  string power = 3;
}

message Empty {

}

message People {
  repeated Person people = 1;
}

实现server端

ts 复制代码
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { join } from 'path';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.connectMicroservice<MicroserviceOptions>({
    transport: Transport.GRPC,
    options: {
      package: 'person',
      protoPath: join(__dirname, '../../proto/person.proto'),
      url: 'localhost:5000',
    },
  });

  await app.startAllMicroservices();

  await app.init();
}
bootstrap();

配置脚本

添加运行依赖

bash 复制代码
pnpm add @nestjs/microservices

添加开发依赖

bash 复制代码
pnpm add ts-proto @grpc/grpc-js @grpc/proto-loader -D

在package.json scripts中添加脚本

bash 复制代码
"proto:gen": "protoc --plugin=protoc-gen-ts_proto=./node_modules/.bin/protoc-gen-ts_proto --ts_proto_out=./proto/ --ts_proto_opt=nestJs=true src/proto/*.proto"

执行脚本

bash 复制代码
pnpm run proto:gen

会自动生成一个ts文件,包括实现方法的grpcmethod装饰器

ts 复制代码
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions:
//   protoc-gen-ts_proto  v2.6.1
//   protoc               v5.28.3
// source: person.proto

/* eslint-disable */
import { GrpcMethod, GrpcStreamMethod } from "@nestjs/microservices";
import { Observable } from "rxjs";

export const protobufPackage = "person";

export interface PersonById {
  id: number;
}

export interface Person {
  id: number;
  name: string;
  power: string;
}

export interface Empty {
}

export interface People {
  people: Person[];
}

export const PERSON_PACKAGE_NAME = "person";

/** Generated according to https://cloud.google.com/apis/design/standard_methods */

export interface PersonServiceClient {
  findOne(request: PersonById): Observable<Person>;

  findAll(request: Empty): Observable<People>;
}

/** Generated according to https://cloud.google.com/apis/design/standard_methods */

export interface PersonServiceController {
  findOne(request: PersonById): Promise<Person> | Observable<Person> | Person;

  findAll(request: Empty): Promise<People> | Observable<People> | People;
}

export function PersonServiceControllerMethods() {
  return function (constructor: Function) {
    const grpcMethods: string[] = ["findOne", "findAll"];
    for (const method of grpcMethods) {
      const descriptor: any = Reflect.getOwnPropertyDescriptor(constructor.prototype, method);
      GrpcMethod("PersonService", method)(constructor.prototype[method], method, descriptor);
    }
    const grpcStreamMethods: string[] = [];
    for (const method of grpcStreamMethods) {
      const descriptor: any = Reflect.getOwnPropertyDescriptor(constructor.prototype, method);
      GrpcStreamMethod("PersonService", method)(constructor.prototype[method], method, descriptor);
    }
  };
}

export const PERSON_SERVICE_NAME = "PersonService";

那么我们可以修改脚本,启动时生成和更新

bash 复制代码
"start:dev": "npm run proto:gen && nest start --watch",

这里可以利用文件监听工具chokidar监听proto的变化重新生成对应的ts文件

person模块文件

ts 复制代码
import { Module } from '@nestjs/common';
import { PersonController } from './person.controller';
import { PersonService } from './person.service';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { GrpcLoggingInterceptor } from 'src/test.interceptor';

@Module({
  controllers: [PersonController],
  providers: [
    PersonService,
    {
      provide: APP_INTERCEPTOR,
      useClass: GrpcLoggingInterceptor,
    },
  ],
})
export class PersonModule {}

person控制器

ts 复制代码
import { Controller } from '@nestjs/common';
import { PersonService } from './person.service';
import { Call, Metadata } from '@grpc/grpc-js';
import {
  People,
  Person,
  PersonById,
  PersonServiceControllerMethods,
} from 'src/proto/person';

@Controller('person')
@PersonServiceControllerMethods()
export class PersonController {
  constructor(private readonly personService: PersonService) {}

  findOne(data: PersonById, metadata: Metadata, call: Call): Person {
    console.log('client access server controller', metadata, call);
    return this.personService.findOne(data);
  }

  findAll(): People {
    return this.personService.findAll();
  }
}

可以看到这里直接使用导出的PersonServiceControllerMethods装饰器,否则的话每个方法都需要自己手动加上@GrpcMethod

person服务

ts 复制代码
import { Injectable } from '@nestjs/common';
import {
  People,
  Person,
  PersonById,
  PersonServiceController,
} from 'src/proto/person';

@Injectable()
export class PersonService implements PersonServiceController {
  private readonly people: Person[] = [
    { id: 1, name: 'Iron Man', power: 'Technology' },
    { id: 2, name: 'Spider Man', power: 'Spider Powers' },
    { id: 3, name: 'Thor', power: 'Thunder' },
  ];

  findOne(request: PersonById): Person {
    const person = this.people.find(({ id }) => id === request.id);
    console.log('client calls server findOne function');

    if (!person) {
      throw new Error(`Person with id ${request.id} not found`);
    }
    return person;
  }

  findAll(): People {
    return {
      people: this.people,
    };
  }
}

注册在app module

最后在app.moudule中注册

ts 复制代码
import { Module } from '@nestjs/common';
import { PersonModule } from './person/person.module';

@Module({
  imports: [PersonModule],
})
export class AppModule {}

在bloomrpc填入地址和参数

运行截图

顺利运行服务端

实现client端

配置脚本

添加运行依赖

bash 复制代码
pnpm add @nestjs/microservices

添加开发依赖

bash 复制代码
pnpm add ts-proto -D

在package.json scripts中添加脚本

bash 复制代码
"proto:gen": "protoc --plugin=protoc-gen-ts_proto=./node_modules/.bin/protoc-gen-ts_proto --ts_proto_out=./proto/ --ts_proto_opt=nestJs=true src/proto/*.proto"

执行脚本

bash 复制代码
pnpm run proto:gen
ts 复制代码
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3001);
}
bootstrap();

为了简化,直接在app.module.ts中写入模块

这里的url填的是服务端的grpc地址

client模块注册在app module

ts 复制代码
// app.module.ts
import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';

import { join } from 'path';
import { GrpcClientService } from './client/client.service';
import { PersonController } from './client/client.controller';

@Module({
  imports: [
    ClientsModule.register([
      {
        name: 'PERSON_PACKAGE',
        transport: Transport.GRPC,
        options: {
          package: 'person', // proto 文件中的包名
          protoPath: join(__dirname, '../../proto/person.proto'),
          url: 'localhost:5000',
        },
      },
    ]),
  ],
  controllers: [PersonController],
  providers: [GrpcClientService],
})
export class AppModule {}

创建client控制器

ts 复制代码
import { Controller, Get, Param } from '@nestjs/common';
import { GrpcClientService } from './client.service';

@Controller('person')
export class PersonController {
  constructor(private readonly grpcClientService: GrpcClientService) {}

  @Get(':id')
  async getPersonById(@Param('id') id: string) {
    const person = await this.grpcClientService
      .findPersonById({ id: Number(id) })
      .toPromise();
    return person;
  }

  @Get()
  async getAllPeople() {
    const people = await this.grpcClientService.findAllPeople().toPromise();
    return people;
  }
}

创建client服务

重点来了,可以关注下是怎么调用服务端方法的

ts 复制代码
// grpc-client.service.ts
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
import { ClientGrpc } from '@nestjs/microservices';
import { Observable } from 'rxjs';
import {
  People,
  Person,
  PersonById,
  PersonServiceClient,
} from 'src/proto/person';

@Injectable()
export class GrpcClientService implements OnModuleInit {
  private personService: PersonServiceClient;

  // 注入 gRPC 客户端服务
  constructor(@Inject('PERSON_PACKAGE') private client: ClientGrpc) {}

  onModuleInit() {
    // 在模块初始化时创建 gRPC 客户端
    this.personService = this.client.getService('PersonService');
  }

  // 调用 gRPC 服务端的 findOne 方法
  findPersonById(id: PersonById): Observable<Person> {
    return this.personService.findOne(id);
  }

  // 调用 gRPC 服务端的 findMany 方法
  findAllPeople(): Observable<People> {
    return this.personService.findAll({});
  }
}

this.client.getService() 是在创建一个代理对象,这个代理对象会:

  • 映射到服务端定义的 service(在 .proto 文件中定义的 service)
  • 自动处理 gRPC 的通信细节
  • 提供与服务端相同的方法接口

当你执行 getService('PersonService') 时:

  • 它会创建一个包含 FindOneFindAll 方法的客户端对象
  • 这些方法可以直接调用,就像调用本地方法一样
  • 底层会自动处理 gRPC 的请求和响应

测试

访问http://localhost:3001/person/1

同时我们在controller service中加的log也被打印了

注意

细心点可能会发现我在服务端的方法中写的还有两个参数 类型来自于@grpc/grpc-js

ts 复制代码
findOne(data: PersonById, metadata: Metadata, call: Call): Person {
    console.log('client access server controller');
    return this.personService.findOne(data);
  }

这里的metadata和call有什么作用呢

metadata 参数:

这是一个 Metadata 类型的对象,用于传递请求的元数据信息

常见用途:

  1. 传递认证信息,如 token:
ts 复制代码
@GrpcMethod('PersonService', 'FindOne')
findOne(data: PersonById, metadata: Metadata) {
  const token = metadata.get('authorization')[0];  // 获取认证token
  // ... 验证逻辑
}
  1. 传递认证信息,如 token:
ts 复制代码
const traceId = metadata.get('x-trace-id')[0];
  1. 设置自定义头信息:
ts 复制代码
metadata.set('custom-header', 'value');

call 参数:

代表当前 gRPC 调用的上下文信息

主要功能:

  1. 获取调用状态:
ts 复制代码
@GrpcMethod()
findOne(data: PersonById, metadata: Metadata, call: ServerUnaryCall<any>) {
  console.log(call.getPeer());  // 获取客户端地址
  console.log(call.cancelled);  // 检查调用是否被取消
}
  1. 处理流式调用中的事件:
ts 复制代码
@GrpcStreamMethod()
streamPeople(data$: Observable<PersonById>, metadata: Metadata, call: ServerDuplexStream<any, any>) {
  call.on('cancelled', () => {
    // 处理调用取消
    console.log('Stream was cancelled');
  });
}
  1. 获取请求相关的配置信息:
ts 复制代码
const settings = call.getSettings();  // 获取gRPC调用设置

典型使用示例:

ts 复制代码
@Controller()
export class PersonController {
  @GrpcMethod('PersonService')
  async findOne(
    data: PersonById,
    metadata: Metadata,
    call: ServerUnaryCall<any>
  ) {
    // 获取认证信息
    const token = metadata.get('authorization')[0];
    
    // 记录调用来源
    const clientAddress = call.getPeer();
    
    // 检查调用是否被取消
    if (call.cancelled) {
      throw new Error('Call was cancelled');
    }
    
    // 添加响应头
    metadata.set('response-time', Date.now().toString());
    
    return { /* people data */ };
  }
}

那么就可以用于这些场景:

  • 处理请求的元数据(headers、认证信息等)
  • 控制和监控 gRPC 调用的生命周期
  • 添加日志、监控等横切关注点

需要注意的是,不是所有方法都需要使用这两个参数。如果不需要处理元数据或调用上下文可以省略它们。

相关推荐
AAA修煤气灶刘哥13 分钟前
ES 聚合爽到飞起!从分桶到 Java 实操,再也不用翻烂文档
后端·elasticsearch·面试
爱读源码的大都督24 分钟前
Java已死?别慌,看我如何用Java手写一个Qwen Code Agent,拯救Java
java·人工智能·后端
星辰大海的精灵1 小时前
SpringBoot与Quartz整合,实现订单自动取消功能
java·后端·算法
天天摸鱼的java工程师1 小时前
RestTemplate 如何优化连接池?—— 八年 Java 开发的踩坑与优化指南
java·后端
一乐小哥1 小时前
一口气同步10年豆瓣记录———豆瓣书影音同步 Notion分享 🚀
后端·python
LSTM971 小时前
如何使用C#实现Excel和CSV互转:基于Spire.XLS for .NET的专业指南
后端
三十_1 小时前
【NestJS】构建可复用的数据存储模块 - 动态模块
前端·后端·nestjs
武子康1 小时前
大数据-91 Spark广播变量:高效共享只读数据的最佳实践 RDD+Scala编程
大数据·后端·spark
努力的小郑1 小时前
MySQL索引(二):覆盖索引、最左前缀原则与索引下推详解
后端·mysql
阿拉伦1 小时前
智能交通拥堵治理柔性设计实践复盘小结
后端