vault backup: 2026-05-07 18:57:24
This commit is contained in:
@@ -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
|
||||
---
|
||||
|
||||
@@ -13,6 +13,93 @@ create time: 2026-05-07 16:00
|
||||
>
|
||||
> 你的 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 的二进制编码规则天然保证了向前兼容。坏消息是:**只有遵守规则的改动才是兼容的**,违反规则的静默破坏会让你排查整整一天。
|
||||
|
||||
## Oneof —— 互斥字段
|
||||
|
||||
Reference in New Issue
Block a user