跳到主要内容
版本:1.2.0

gRPC 支持

Fory 可以为包含 service 定义的 schema 生成 JavaScript service companion。生成的 service code 使用普通 gRPC transport,但 request 和 response 对象使用 Fory 序列化,而不是 protobuf。

当 RPC 两端都由同一份 Fory IDL、protobuf IDL 或 FlatBuffers IDL 生成,并且两端都期望 Fory 编码的 message body 时,可以使用这种模式。如果 API 必须被通用 protobuf client、 reflection 工具或期望 protobuf message bytes 的组件消费,请使用标准 protobuf gRPC 代码生成。

使用 --grpc 生成 Node.js server/client 代码;使用 --grpc-web 生成调用 gRPC-Web 兼容 server 或 proxy 的浏览器 client。

添加依赖

生成的 model 文件依赖 @apache-fory/core

Node.js gRPC companion 导入 @grpc/grpc-js

npm install @apache-fory/core @grpc/grpc-js

浏览器 gRPC-Web companion 导入 grpc-web

npm install @apache-fory/core grpc-web

Fory 不会把 gRPC package 作为硬依赖。只需添加应用实际使用的 transport package。

定义 Service

Service 定义可以来自 Fory IDL、protobuf IDL 或 FlatBuffers rpc_service。Fory IDL service 示例如下:

package demo.greeter;

message HelloRequest {
string name = 1;
}

message HelloReply {
string reply = 1;
}

service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
}

生成 Node.js gRPC binding:

foryc service.fdl --javascript_out=./generated/javascript --grpc

生成浏览器 gRPC-Web binding:

foryc service.fdl --javascript_out=./generated/javascript --grpc-web

也可以同时生成:

foryc service.fdl --javascript_out=./generated/javascript --grpc --grpc-web

输出包含:

文件用途
service.tsinterface、enum、union 和 schema helper
service_grpc.tsNode.js @grpc/grpc-js server/client
service_grpc_web.ts浏览器 grpc-web client

实现 Node.js Server

import * as grpc from "@grpc/grpc-js";
import {
GreeterHandlers,
addGreeterService,
} from "./generated/javascript/service_grpc";

const greeter: GreeterHandlers = {
sayHello(call, callback) {
callback(null, {
reply: `Hello, ${call.request.name}`,
});
},
};

const server = new grpc.Server();
addGreeterService(server, greeter);
server.bindAsync(
"0.0.0.0:50051",
grpc.ServerCredentials.createInsecure(),
(error, port) => {
if (error) {
throw error;
}
server.start();
console.log(`listening on ${port}`);
},
);

创建 Node.js Client

import * as grpc from "@grpc/grpc-js";
import { createGreeterClient } from "./generated/javascript/service_grpc";

const client = createGreeterClient(
"localhost:50051",
grpc.credentials.createInsecure(),
);

client.sayHello({ name: "Fory" }, (error, reply) => {
if (error) {
throw error;
}
console.log(reply.reply);
});

可以继续使用普通 @grpc/grpc-js metadata、call option、credential、deadline 和 interceptor。

创建 Browser Client

import { createGreeterWebClient } from "./generated/javascript/service_grpc_web";

const client = createGreeterWebClient("https://api.example.com", {
wireFormat: "grpcweb",
});

client.sayHello({ name: "Fory" }, null, (error, reply) => {
if (error) {
console.error(error.message);
return;
}
console.log(reply.reply);
});

Unary 调用也可以使用生成的 promise client:

import { createGreeterWebPromiseClient } from "./generated/javascript/service_grpc_web";

const client = createGreeterWebPromiseClient("https://api.example.com");
const reply = await client.sayHello({ name: "Fory" });
console.log(reply.reply);

Streaming RPC

Node.js companion 支持所有 gRPC streaming shape。浏览器 gRPC-Web companion 支持 unary 和 server-streaming;gRPC-Web 不支持 client-streaming 或 bidirectional streaming,compiler 会拒绝 这些 shape 的 --grpc-web 生成。

service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
rpc LotsOfReplies (HelloRequest) returns (stream HelloReply);
rpc LotsOfGreetings (stream HelloRequest) returns (HelloReply);
rpc Chat (stream HelloRequest) returns (stream HelloReply);
}

Node.js server 实现使用普通 @grpc/grpc-js streaming call object。生成 companion 只负责把 Fory payload 接入 gRPC,deadline、credential、metadata、interceptor 和错误语义仍遵循 gRPC 库行为。

const greeter: GreeterHandlers = {
sayHello(call, callback) {
callback(null, { reply: `Hello, ${call.request.name}` });
},

lotsOfReplies(call) {
call.write({ reply: `Hello, ${call.request.name}` });
call.write({ reply: `Welcome, ${call.request.name}` });
call.end();
},

lotsOfGreetings(call, callback) {
const names: string[] = [];
call.on("data", (request) => {
names.push(request.name);
});
call.on("end", () => {
callback(null, { reply: `Hello, ${names.join(", ")}` });
});
},

chat(call) {
call.on("data", (request) => {
call.write({ reply: `Hello, ${request.name}` });
});
call.on("end", () => {
call.end();
});
},
};

Node.js client 使用与 RPC shape 对应的生成方法:

const replies = client.lotsOfReplies({ name: "Fory" });
replies.on("data", (reply) => {
console.log(reply.reply);
});

const greetings = client.lotsOfGreetings((error, reply) => {
if (error) {
throw error;
}
console.log(reply.reply);
});
greetings.write({ name: "Alice" });
greetings.write({ name: "Bob" });
greetings.end();

const chat = client.chat();
chat.on("data", (reply) => {
console.log(reply.reply);
});
chat.write({ name: "Alice" });
chat.write({ name: "Bob" });
chat.end();

包含 server-streaming 方法的 service 中,生成的 gRPC-Web companion 默认使用 grpcwebtext 编码格式。只有 unary 方法的 service 默认使用 grpcweb。也可以显式指定:

const client = createGreeterWebClient("https://api.example.com", {
wireFormat: "grpcwebtext",
});

浏览器 client 可以用 callback client 消费 server-streaming RPC:

const stream = client.lotsOfReplies({ name: "Fory" });

stream.on("data", (reply) => {
console.log(reply.reply);
});
stream.on("error", (error) => {
console.error(error.message);
});
stream.on("end", () => {
console.log("stream ended");
});

Service 行为

生成的 service code 只替换 request/response 序列化。常规 gRPC service 行为仍由 transport package 提供:

  • TLS 和 credential
  • Metadata 和 status code
  • Deadline 和取消
  • Client/server interceptor
  • 负载均衡和部署相关 proxy 配置

故障排查

缺少 gRPC Package

Node.js 添加 @grpc/grpc-js,浏览器添加 grpc-web。生成 model 仍需要 @apache-fory/core

gRPC-Web Client-Streaming 或 Bidirectional RPC 被拒绝

gRPC-Web 不支持 client-streaming 或 bidirectional streaming。对于这些 shape,请使用 --grpc 生成 Node.js companion,或只向浏览器 client 暴露 unary 和 server-streaming 方法。

Protobuf Client 无法读取响应

Fory gRPC 使用 Fory 二进制协议 payload,不是 protobuf wire-format message。请在两端使用同一份 schema 生成的 Fory gRPC companion。