vault backup: 2026-05-07 18:57:24

This commit is contained in:
2026-05-07 18:57:24 +08:00
parent 68af73d6d8
commit 80a14ef618
+88 -1
View File
@@ -1,5 +1,5 @@
--- ---
tags: [grpc, protobuf, schema-versioning, proto3] tags: [grpc, protobuf, schema-versioning, proto3, client-server, stub-generation]
create time: 2026-05-07 16:00 create time: 2026-05-07 16:00
--- ---
@@ -13,6 +13,93 @@ create time: 2026-05-07 16:00
> >
> 你的 Service A 调用 Service B 的 `GetUser`,Proto 里定义了 20 个字段。半年后你想加第 21 个字段,但旧版客户端没有编译更新。会发生什么? > 你的 Service A 调用 Service B 的 `GetUser`,Proto 里定义了 20 个字段。半年后你想加第 21 个字段,但旧版客户端没有编译更新。会发生什么?
## Proto 是什么
Protocol Buffers(简称 **Proto**)是 Google 开源的一套**语言无关、平台无关的结构化数据序列化方案**。它的核心作用有两层:
| 层面 | 说明 |
|------|------|
| **接口契约** | 用 `.proto` 文件声明 Service(有哪些 RPC 方法)、Request / Response(传什么数据),作为服务间通信的"宪法" |
| **序列化格式** | 把内存中的对象编码成二进制字节流,跨进程 / 跨网络传输后再还原回对象 |
一句话总结:**Proto = IDL(接口定义语言)+ 二进制序列化**。
### 为什么两端都需要编译?
很多初学者会疑惑:服务端实现逻辑、客户端发起调用,两边的代码不是各写各的吗?——**不是的,.proto 文件是两端共享的唯一真相源**。
```mermaid
graph LR
Proto[".proto 文件<br/>唯一真相源"] --> protoc["protoc / Buf"]
protoc --> Server["服务端生成代码<br/>(Go stub)"]
protoc --> Client["客户端生成代码<br/>(Java / TS / …)"]
Server --> Encode["编码 Request → 发送二进制流"]
Client --> Decode["接收二进制流 → 反序列化 Response"]
Encode --- Decode
```
| 角色 | 编译产物 | 用途 |
|------|---------|------|
| **服务端** | Server Stub | 接收二进制消息 → 反序列化为参数对象 → 执行业务逻辑 → 返回结果对象 → 编码为响应 |
| **客户端** | Client Stub | 用户调用 Stub 方法 → 构造请求对象 → 编码为二进制字节流发送 → 收到响应后解码 |
> [!question] 思考
>
> 如果服务端用 Go 生成代码、客户端用 Java 生成代码,它们之间的二进制消息能正确互操作吗?
>
> > [!answer]- 答案
> > **完全可以。** Proto 的二进制编码规则是语言无关的规范,只依赖字段编号(`=` 后面的数字)。只要两边用的是同一份 `.proto`,任何语言的编译器都会按照相同的规则编解码。
#### 实际编译示意
**.proto 定义(两端共用同一文件)**
```proto
// proto/order/v1/order.proto
syntax = "proto3";
package order.v1;
service OrderService {
rpc CreateOrder (CreateOrderRequest) returns (CreateOrderResponse);
}
message CreateOrderRequest {
string user_id = 1;
repeated string item_ids = 2;
}
```
**服务端(Go):** `buf generate` → 生成 `order_service.pb.go` + `order.pb.go`
```go
// 服务端实现生成的 RPC 接口
func (s *server) CreateOrder(ctx context.Context, req *orderpb.CreateOrderRequest) (*orderpb.CreateOrderResponse, error) {
// 直接使用反序列化好的 Request 对象 ✅
fmt.Println(req.UserId) // 强类型访问
return &orderpb.CreateOrderResponse{OrderId: "ord-123"}, nil
}
```
**客户端(TypeScript):** `buf generate` → 生成 `order_pb.ts`
```typescript
// 客户端直接调用生成的 Stub 方法
const response = await orderClient.createOrder({ userId: 'u-456', itemIds: ['i-7', 'i-8'] })
console.log(response.orderId) // 直接得到结果 ✅
```
可以看到:两端都从**同一个 `.proto`** 生成了各自语言的代码,但编写的代码量极小——核心工作是写 `.proto` 和手写业务逻辑。
### 为什么不直接用 JSON / XML
| 维度 | Proto | JSON | XML |
|------|-------|------|-----|
| 体积 | 紧凑,无标签名冗余 | 冗余大(每个值都带 key 名) | 标签开销更大 |
| 性能 | 编解码速度极快(变长整数 + 字段编号编码) | 需解析字符串 | 解析最慢 |
| 类型安全 | 强类型,编译器检查 | 动态类型,运行时才暴露错误 | 动态类型 |
| 向后兼容 | 天然支持字段增删 | 无约定,全靠文档 | 部分支持 |
| 工具链 | `protoc` 生成各语言 Stub | — | — |
好消息是:Protocol Buffers 的二进制编码规则天然保证了向前兼容。坏消息是:**只有遵守规则的改动才是兼容的**,违反规则的静默破坏会让你排查整整一天。 好消息是:Protocol Buffers 的二进制编码规则天然保证了向前兼容。坏消息是:**只有遵守规则的改动才是兼容的**,违反规则的静默破坏会让你排查整整一天。
## Oneof —— 互斥字段 ## Oneof —— 互斥字段