2.1 RPC 与序列化契约:远程调用不是本地函数
RPC 用类似函数调用的接口封装网络通信,但它不能继承本地调用的可靠性。远程调用有延迟、序列化、独立部署、版本并存和结果未知,API 必须把这些差异暴露出来。
IDL 是跨进程契约
下面是一份完整的 scoring.proto,后文的 Player 定义也可以追加到这个文件。
syntax = "proto3";
package atlas.scoring.v1;
service ScoringService {
rpc GetScore(GetScoreRequest) returns (GetScoreResponse);
rpc WatchRanking(WatchRankingRequest) returns (stream RankingUpdate);
}
message GetScoreRequest {
string player_id = 1;
}
message GetScoreResponse {
string player_id = 1;
int64 points = 2;
int64 version = 3;
}
message WatchRankingRequest {
string tournament_id = 1;
optional int64 after_version = 2;
}
message RankingUpdate {
int64 ranking_version = 1;
repeated GetScoreResponse scores = 2;
}消息生成器和 gRPC 插件可以生成消息类与 stub,但类型检查不会替你约定业务含义。这里约定 points 是整数积分,GetScoreResponse.version 是该选手积分版本;ranking_version 则是赛事榜单版本,两种版本不能互相比较。
WatchRanking 每条响应是该赛事的一份完整榜单快照,版本严格递增。不传 after_version 表示先取当前快照再订阅;传入时只返回大于该版本的快照。历史已过保留期时返回 FAILED_PRECONDITION,客户端重新请求完整快照。optional 让“没给续传位置”与“明确从版本 0 之后开始”能够区分。字段单位、错误、deadline、幂等和兼容政策都需要这样的文字契约。
Protobuf 编码依赖字段号和 wire type
在 Protobuf 二进制格式中,字段由编号识别,不传输源码中的字段名;ProtoJSON 使用字段名,因此改名仍可能破坏 JSON 客户端。整数常使用 varint,字符串和嵌套消息使用 length-delimited 编码。
这通常带来紧凑表示和快速解析,但不能脱离 payload、语言实现和压缩方式宣称固定倍率。JSON 的可读性、浏览器生态和调试便利仍可能更适合公开 API。
Schema 演进规则
- 已发布字段号不能改变含义;
- 删除字段后
reserved编号和名称; - 不把标量字段随意改成 wire/语义不兼容的类型;
- 枚举保留明确的零值
UNSPECIFIED; - 新字段要让旧客户端忽略后仍得到安全行为;
- 不依赖接收方理解未知字段完成关键业务逻辑;二进制解析通常会保留未知字段,但转成 JSON 或逐字段复制可能将其丢弃。
enum PlayerStatus {
PLAYER_STATUS_UNSPECIFIED = 0;
PLAYER_STATUS_ACTIVE = 1;
PLAYER_STATUS_SUSPENDED = 2;
}
message Player {
reserved 4;
reserved "legacy_rank";
string player_id = 1;
string display_name = 2;
PlayerStatus status = 3;
}新增字段或枚举值也可能造成语义破坏,例如旧客户端把不认识的状态错误地当作“允许参赛”。兼容测试要覆盖真实生成代码和业务分支。proto3 未声明 optional 的标量通常无法区分未传值与显式传入默认值;把缺失积分默认为 0 是否合理,应由业务明确决定。
四种 RPC 形态
| 形态 | 请求 | 响应 | 适合 |
|---|---|---|---|
| Unary | 1 | 1 | 普通查询与命令 |
| Server streaming | 1 | 多 | 订阅更新、逐批返回 |
| Client streaming | 多 | 1 | 批量上传、聚合 |
| Bidirectional streaming | 多 | 多 | 双向独立流、交互会话 |
不要因为框架支持流就把所有分页查询改成长流。流式 RPC 需要定义背压、取消、断线续传、顺序、checkpoint 和最大消息大小。
边界模型不要复用内部实体
把 ORM Entity 直接作为 RPC 消息会耦合数据库结构、延迟加载和公开契约。Protobuf 消息是传输模型,应在适配器边界映射到领域或应用模型。
同理,一个巨大的 Common.proto 会让所有服务共享同一演进节奏。只共享真正稳定的标识和公共协议,业务模型由拥有它的上下文发布。
协议选择
| 维度 | JSON/HTTP | gRPC/Protobuf |
|---|---|---|
| 人类调试 | 直接 | 专用工具配合反射服务或本地 schema |
| 浏览器/公共生态 | 成熟 | 常需代理或 Web 支持 |
| 强 IDL 与代码生成 | 可由 OpenAPI 提供 | 原生工作流成熟 |
| 流式 RPC | 可实现但形式多 | 四种形态明确 |
| 二进制效率 | 依赖 JSON 与压缩 | 通常更紧凑 |
| 版本风险 | 字段与语义 | 字段号、schema 与语义 |
下一课处理 RPC 的运行语义:deadline、取消、重试和背压。
参考资料
- Protocol Buffers, Language Guide
- gRPC, Core concepts