From 80a14ef6187b471ba14e824767531f1ed2490998 Mon Sep 17 00:00:00 2001 From: wonder Date: Thu, 7 May 2026 18:57:24 +0800 Subject: [PATCH] vault backup: 2026-05-07 18:57:24 --- hzh/MS/06-gRPC/02-Proto设计.md | 89 +++++++++++++++++++++++++++++++++- 1 file changed, 88 insertions(+), 1 deletion(-) diff --git a/hzh/MS/06-gRPC/02-Proto设计.md b/hzh/MS/06-gRPC/02-Proto设计.md index 0f109df..d344104 100644 --- a/hzh/MS/06-gRPC/02-Proto设计.md +++ b/hzh/MS/06-gRPC/02-Proto设计.md @@ -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 文件
唯一真相源"] --> protoc["protoc / Buf"] + protoc --> Server["服务端生成代码
(Go stub)"] + protoc --> Client["客户端生成代码
(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 —— 互斥字段