Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bun 实现 gRPC 服务器 #40

Open
jrainlau opened this issue Dec 9, 2024 · 0 comments
Open

bun 实现 gRPC 服务器 #40

jrainlau opened this issue Dec 9, 2024 · 0 comments
Labels

Comments

@jrainlau
Copy link
Owner

jrainlau commented Dec 9, 2024

What-is-gRPC_

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

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

一、初始化

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

curl -fsSL https://bun.sh/install | bash

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

bun init -y

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

回头在 package.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 来举个例子:

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()。接下来我们就要开始学习如何实现这个服务。

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

.
├── 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。

因此它的雏形是这样的:

class GrpcServer {
  private server: grpc.Server

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

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

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

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 启动服务器。理解了官网的写法后,便可以移植到我们的实现当中来。

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

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 的具体实现代码了:

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 服务了:

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)

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

🌐 gRPC listening on 0.0.0.0:50051

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

Clipboard_Screenshot_1733749902

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

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

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(),强行终止服务。

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 中,可自行下载尝试。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant