Cell - gRPC 组件
Cell 上的 gRPC 服务器和客户端,基于 node:http2,内置 .proto 解析器和 protobuf 编解码:无第三方依赖,无代码生成。一元、服务器流、客户端流和双向方法;与其他 gRPC 实现互操作(双向对 @grpc/grpc-js 测试过)。
定义服务
将 .proto 文件保留在源中并解析一次。使用 Vite(cell build、cell dev、createTestApp()),?raw 导入捆绑其文本:
// src/greeter.ts
import { Proto, type UnaryCall, type ServerStreamCall } from '@celljs/grpc';
import source from './greeter.proto?raw'; // 声明 module '*?raw' 一次,见 examples/grpc-app/src/raw.d.ts
export const Greeter = Proto.parse(source).service('helloworld.Greeter');Proto.parse(...sources) 接受服务需要的每个文件(导入按类型名跨源解析);google/protobuf 众所周知的类型(empty、timestamp、duration、wrappers、struct、any、field_mask)是内置的。名字是完整的(helloworld.Greeter)或明确时短的。proto.message('HelloRequest') 给出一个有 encode() / decode() 的消息类型,用于 protobuf 但不用 gRPC。
提供服务
import { GrpcError, GrpcService, GrpcStatus, type ServerCall } from '@celljs/grpc';
import { Greeter } from './greeter';
@GrpcService(Greeter)
export class GreeterService {
@Autowired(UserService) // 像任何其他组件一样的组件
protected readonly users: UserService;
// rpc SayHello (HelloRequest) returns (HelloReply)
sayHello({ name }: HelloRequest, call: ServerCall): HelloReply {
if (!name) {
throw new GrpcError(GrpcStatus.INVALID_ARGUMENT, 'name is required');
}
return { message: `Hello ${name}` };
}
// rpc SayHelloStream (HelloRequest) returns (stream HelloReply)
async *sayHelloStream({ name }: HelloRequest): AsyncIterable<HelloReply> {
yield { message: `Hello ${name}` };
}
// rpc SayHelloToAll (stream HelloRequest) returns (HelloReply)
async sayHelloToAll(requests: AsyncIterable<HelloRequest>): Promise<HelloReply> { ... }
// rpc Chat (stream HelloRequest) returns (stream HelloReply)
async *chat(requests: AsyncIterable<HelloRequest>): AsyncIterable<HelloReply> { ... }
}从 DI 模块导入文件(src/module.ts)。应用启动时,如果至少有一个 @GrpcService,GrpcServerLifecycle 启动 gRPC 服务器(日志中 gRPC serve 127.0.0.1:50051);应用停止时关闭。方法是 .proto 方法的小驼峰(SayHello → sayHello);没有实现的方法答 UNIMPLEMENTED。类型 UnaryHandler、ServerStreamHandler、ClientStreamHandler 和 BidiStreamHandler 描述四个签名。
最后一个参数,call: ServerCall,有请求 metadata(小写键)、当客户端取消或截止时中止的 signal、deadline、setHeader(key, value)(在第一条响应前)和 setTrailer(key, value)。
错误:抛出 GrpcError(code, details, metadata?) 以答该状态。其他任何错误答 UNKNOWN 带错误信息并被记录。
grpc:
server:
enabled: true # false:即使有 @GrpcService 组件也无服务器
port: 50051 # CELL_GRPC__SERVER__PORT;0 取自由端口
host: 127.0.0.1 # CELL_GRPC__SERVER__HOST=0.0.0.0 在容器中
maxReceiveMessageSize: 4194304gRPC 服务器是 @celljs/express-adapter 的 HTTP 一旁的第二端口;没有 HTTP 的应用不需适配器(cell build 则警告没有组件提供 http 入口,这是预期的)。Cell 应用外,new GrpcServer(options).addService(Greeter, impl).listen(port, host) 做同样的事,server.handleStream(stream, headers) 从你自己的 HTTP/2 服务器的流提供(例如 http2.createSecureServer() 用于 TLS)。
调用它
import { GrpcChannel } from '@celljs/grpc';
export interface GreeterClient {
sayHello: UnaryCall<Partial<HelloRequest>, HelloReply>;
sayHelloStream: ServerStreamCall<Partial<HelloRequest>, HelloReply>;
}
const channel = new GrpcChannel('localhost:50051', { metadata: { authorization: 'Bearer ...' }, timeout: 5000 });
const greeter = channel.createClient<GreeterClient>(Greeter);
const { message } = await greeter.sayHello({ name: 'Cell' });
for await (const reply of greeter.sayHelloStream({ name: 'Cell' })) { ... } // break 取消调用
await greeter.sayHelloToAll([{ name: 'a' }, { name: 'b' }]); // 任何(异步)可迭代对象
await greeter.sayHello({ name: 'Cell' }, { timeout: 1000, signal, metadata: { 'x-trace-bin': bytes } });
channel.close();一个通道是一条 HTTP/2 连接,在第一次调用时打开,丢失时重新打开;在同一服务器的客户端之间共享。空闲通道不让进程活着。失败的调用拒绝时返回 GrpcError(code、details、尾部 metadata):当服务器无法到达时 UNAVAILABLE、DEADLINE_EXCEEDED、CANCELLED 等。无法编码的请求在发送任何东西前拒绝,返回命名字段的 TypeError。
值
| Protobuf | JavaScript |
|---|---|
字段 user_id | 属性 userId(编码也接受 user_id) |
int32、uint32、sint32、fixed32、sfixed32、float、double | number |
int64、uint64、sint64、fixed64、sfixed64 | 十进制 string(编码也接受 number 和 bigint) |
bool、string | boolean、string |
bytes | Uint8Array(编码也接受 base64) |
| 枚举 | 值名(编码也接受数字;未知数字解码为数字) |
| 消息 | 对象;google.protobuf.Timestamp 是 { seconds: '…', nanos: 0 } |
repeated / map<K, V> | 数组 / 带字符串键的普通对象 |
oneof kind | 集合成员,和 kind: 'memberName' |
解码的消息在每个没有明确存在的字段处其默认值(0、''、false、第一个枚举值、[]、{});消息字段、optional 字段和 oneof 成员未设置时被忽略。未知字段被跳过。
测试
在 cell.test.yml 中,backend.grpc.server.port: 0 给测试应用一个自由端口;从应用加载的生命周期读它:
const app = await createTestApp();
const { GrpcServerLifecycle } = await app.import('@celljs/grpc');
const channel = new GrpcChannel(`127.0.0.1:${app.get(GrpcServerLifecycle).port}`);限制
- 运行时:Node.js >= 18 和 Deno 2 用于服务器和客户端;Bun 作为客户端,1.4 及以后作为服务器(更早版本的 HTTP/2 明文服务器仅答 Bun 客户端)。不在 Cloudflare Workers 或 FaaS 预设上:服务器需要一个有自己端口的长期运行进程。
- 服务器说明文 HTTP/2(h2c);在其前终止 TLS,或从你自己的 TLS 服务器提供
handleStream()。 - 接收的消息可能用 gzip 或 deflate 压缩;发送的消息不压缩。
.proto:proto2 和 proto3。选项、reserved、扩展和extend被读和忽略;组和版本被拒绝。无服务器反射、健康服务或 gRPC-Web。