bun 实现 gRPC 服务器

cnb.cool任务集功能区中,我们使用了 bun 作为服务端,负责任务集视图的相关读写能力,积累了一定的经验。整体来说 bun 的写法和 Nodejs 几乎一致,但对于"提供 gRPC 服务"相关的知识,现网所能找到的资料较少,因此专门记录下来。

关于 bun 和 gRPC 的介绍就不在此展开了,感兴趣的同学请自行搜索。

一、初始化

参考官网的方式,首先把 bun 安装到机器上(本文开发环境为 MacOS)。

bash 复制代码
curl -fsSL https://bun.sh/install | bash

接下来就可以初始化我们的项目并安装 grpc 依赖了。

bash 复制代码
bun init -y

bun install @grpc/grpc-js @grpc/proto-loader

回头在 package.json 里面加入调试的启动命令:

json 复制代码
{
  ...
  "scripts": {
    "dev": "bun --hot index.ts"
  },
  ...
}

由于 bun 是一个能够直接运行 ts 代码的 runtime,所以也非常推荐直接使用 ts 来写我们的 server 端代码。

回到项目根目录,新建一个 index.ts,随便写入一句console.log('hello world'),执行 yarn dev,便可看到控制台输出了"hello world"字段。修改这里的代码,由于启动时加入了 --hot 的缘故,所以它会实时热更新并运行新的代码,这样就免去每次都要重新手动运行的繁琐步骤了。

二、代码实现

要学习在 bun 中架设 gRPC 服务,首先得要有一份符合要求的 .proto 文件。这里用一个最简单的 Hello World 来举个例子:

proto 复制代码
syntax = "proto3";

package demo;

message SayHelloRequest {
  string name = 1;
}

message SayHelloResponse {
  int32 code = 1;
  string message = 2;
}

service Hello {
  rpc SayHello (SayHelloRequest) returns (SayHelloResponse);
}

可以很直观地看到,我们定义了一个叫做 Hello 的服务,它提供了一个 rpc 调用函数 SayHello()。接下来我们就要开始学习如何实现这个服务。

回到根目录,按照如下的结构组织代码:

bash 复制代码
.
├── index.ts
└── src
    ├── protos
    │   └── hello.proto
    ├── server.ts
    └── services
        └── sayHello.ts

核心的代码为 src/server.ts,第一个就要去实现它。

我们的思路如下:

  1. 一个 gRPC server 就是一个实例:可以通过 new 实例化;
  2. 它提供了一个方法允许我们添加不同的服务:addService() 函数,允许传入不同的 .proto 文件和对应的实现代码;
  3. 一个启动的命令:start() 函数,允许传入 host 和 port。

因此它的雏形是这样的:

ts 复制代码
class GrpcServer {
  private server: grpc.Server

  addService(protoService: any, serviceMap: { [key: string]: any }) {}

  start(host: string; port: string | number) {}
}

在实现具体的逻辑代码之前,不得不吐槽一下官方教程真的藏得有点深。其教程最核心的代码如下:

js 复制代码
function getServer() {
  var server = new grpc.Server();
  server.addService(routeguide.RouteGuide.service, {
    getFeature: getFeature,
    listFeatures: listFeatures,
    recordRoute: recordRoute,
    routeChat: routeChat
  });
  return server;
}
var routeServer = getServer();
routeServer.bindAsync('0.0.0.0:50051', grpc.ServerCredentials.createInsecure(), () => {
  routeServer.start();
});

和我们的思路对应,它也是先通过 addService 添加服务,再通过 bindAsync 绑定 host 和 port 启动服务器。理解了官网的写法后,便可以移植到我们的实现当中来。

ts 复制代码
import grpc from '@grpc/grpc-js';

export default class GrpcServer {
  private server: grpc.Server = new grpc.Server();

  addService(protoService: any, serviceMap: { [key: string]: any }) {
    this.server.addService(protoService, serviceMap);
  }
  async start(host: string, port: string | number) {
    this.server.bindAsync(`${host}:${port}`, grpc.ServerCredentials.createInsecure(), (err, port) => {
      if (err != null) {
        return console.error(err);
      }
      console.log(`🌐 gRPC listening on ${host}:${port}`);
    });
  }
}

为了正确地提供 protoService 参数到 addService(),我们需要写一个 getProto() 方法。该方法通过 @grpc/proto-loader 加载给到的 .proto 文件,返回一个 grpc.GrpcObject

ts 复制代码
export const getProto = (name: string) => grpc.loadPackageDefinition(
  protoLoader.loadSync(
    path.join(cwd(), `src/protos/${name}.proto`),
    {
      keepCase: true,
      longs: String,
      enums: String,
      defaults: true,
      oneofs: true
    },
  )
);

接下来我们便可编写 SayHello 的具体实现代码了:

ts 复制代码
export default function SayHello(call: { request: any }, callback: any) {
  const { name } = call.request;
  callback(null, {
    code: 0,
    message: `Hello ${name}`,
  })
}

注意,这里的 call: { request: any } 对应着 hello.proto 中的 message SayHelloRequest,这里定义了需要传入一个类型为 string 的参数 name

callback 的第一个参数是 Error 对象,在出现错误的时候可以把错误传递进去,如果没有错误则填入 null 即可。第二参数则对应了 hello.proto 中的 message SayHelloResponse

最后回到 index.ts,我们便可以直接启动一个最简单的 gRPC 服务了:

ts 复制代码
import GrpcServer, { getProto } from './src/server';
import SayHello from './src/services/sayHello';

const server = new GrpcServer();
const proto = (getProto('hello').demo as any).Hello.service; // 注意这里的写法。对照 `hello.proto`,找到具体的那个 service

server.addService(proto, { SayHello });
server.start('0.0.0.0', 50051)

执行启动命令后,控制台将会输出

csharp 复制代码
🌐 gRPC listening on 0.0.0.0:50051

使用BloomRPC调试工具,可以验证到该服务已经正常运行。

三、开发模式下热更新能力的提供

在实际的工作开发中,我们肯定会不断地修改代码,细心的同学肯定会发现,上述的代码无法使用 bun 提供的热更新指令 --hot。一旦修改代码,一定会报错:

bash 复制代码
E No address added out of total 1 resolved
462 |                     return bindResult.port;
463 |                 }
464 |                 else {
465 |                     const errorString = `No address added out of total ${addressList.length} resolved`;
466 |                     logging.log(constants_1.LogVerbosity.ERROR, errorString);
467 |                     throw new Error(`${errorString} errors: [${bindResult.errors.join(',')}]`);
                                    ^
error: No address added out of total 1 resolved errors: [Failed to listen at 0.0.0.0]
      at /Users/jrainlau/Desktop/bun-grpc-server-demo/node_modules/@grpc/grpc-js/build/src/server.js:467:31
      at processTicksAndRejections (native:7:39)

该错误的原因是在热更新的时候,并没有杀掉上一次的 gRPC 服务,导致热更新后无法再使用同样的 host 和 port。查遍了官网和 Google 都没有找到对应的解法,最后愣是在源码 @grpc/grpc-js/build/src/server.js 中找到了一个方法 forceShutdown(),强行终止服务。

diff 复制代码
async start(host: string, port: string | number) {
+    if ((globalThis as any).grpcServer) {
+      (globalThis as any).grpcServer.forceShutdown();
+    }
    this.server.bindAsync(`${host}:${port}`, grpc.ServerCredentials.createInsecure(), (err, port) => {
      if (err != null) {
        return console.error(err);
      }
      console.log(`🌐 gRPC listening on ${host}:${port}`);

+      (globalThis as any).grpcServer = this.server;
    });
  }

实现方式也很简单,在每次调用 start() 进行启动的时候,判断全局底下是否仍有残留的实例,如果有就调用 forceShutdown() 方法杀掉它。


最后,本文有关的代码都在仓库 bun-grpc-server-demo 中,可自行下载尝试。

相关推荐
Ling_suu13 分钟前
SpringBoot3——Web开发
java·服务器·前端
Yvemil719 分钟前
《开启微服务之旅:Spring Boot Web开发》(二)
前端·spring boot·微服务
hanglove_lucky22 分钟前
本地摄像头视频流在html中打开
前端·后端·html
维李设论25 分钟前
Node.js的Web服务在Nacos中的实践
前端·spring cloud·微服务·eureka·nacos·node.js·express
2401_8576009531 分钟前
基于 SSM 框架 Vue 电脑测评系统:赋能电脑品质鉴定
前端·javascript·vue.js
天之涯上上35 分钟前
Pinia 是一个专为 Vue.js 3 设计的状态管理库
前端·javascript·vue.js
@大迁世界43 分钟前
摆脱 `<div>`!7 种更语义化的 HTML 标签替代方案
前端·html
高山我梦口香糖2 小时前
[react] <NavLink>自带激活属性
前端·javascript·react.js
撸码到无法自拔2 小时前
React:组件、状态与事件处理的完整指南
前端·javascript·react.js·前端框架·ecmascript
高山我梦口香糖2 小时前
[react]不能将类型“string | undefined”分配给类型“To”。 不能将类型“undefined”分配给类型“To”
前端·javascript·react.js