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 调用的生命周期
  • 添加日志、监控等横切关注点

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

相关推荐
uzong23 分钟前
大模型给我的开发提效入门篇
人工智能·后端
dgiij1 小时前
node.js的进程保活
后端·node.js·bash
蒜蓉大猩猩1 小时前
Node.js - Express框架
后端·架构·node.js·express
蒜蓉大猩猩2 小时前
Node.js --- 详解MongoDB与Mongoose
数据库·后端·mongodb·node.js
昔我往昔2 小时前
Spring Boot中如何处理跨域请求(CORS)
java·spring boot·后端
昔我往昔2 小时前
Spring Boot中的配置文件有哪些类型
java·spring boot·后端
西岸风1662 小时前
【全套】基于Springboot的房屋租赁网站的设计与实现
java·spring boot·后端
ben19874 小时前
二、使用Spring Boot构建AI应用程序
后端
ben19874 小时前
三、ChatClient&Chat Model简化与AI模型的交互
后端
hlvy4 小时前
如何使用策略模式并让spring管理
java·开发语言·后端·spring·策略模式