Files
cs-note/hzh/MS/06-gRPC/03-RPC模式.md
T
2026-05-24 11:42:38 +08:00

198 lines
6.2 KiB
Markdown

---
tags: [grpc, rpc-patterns, streaming, bidirectional]
create time: 2026-05-07 16:00
---
# RPC 模式详解
## 概述
gRPC 支持 **四种 RPC 调用模式**,每种对应不同的客户端/服务端消息交互时序。理解它们不仅仅是记住名词,更要掌握「什么时候该用哪种」以及「各种模式的坑在哪里」。
> [!question] 先看一个实际场景
>
> 你需要实现一个订单列表功能:用户在前端点击按钮 → 后端查询数据库 → 一次返回 100 条订单记录。
>
> 你会选择哪种 RPC 模式?如果用 Unary 一行行发回来会怎样?
Unary 模式下这 100 条记录会变成 100 次独立的 HTTP/2 Stream,虽然 HTTP/2 支持多路复用,但这依然浪费了宝贵的 Stream 标识符空间。更好的方式是用 **Server Streaming** 在一个 Stream 内批量发送。
## Unary —— 普通请求/响应
最常见的调用方式,一问一答:
```go
// client side
resp, err := client.GetOrder(ctx, &pb.GetOrderRequest{Id: "ORD-001"})
if err != nil {
// handle error
}
// server side
func (s *orderServer) GetOrder(ctx context.Context, req *pb.GetOrderRequest) (*pb.Order, error) {
return s.store.Get(req.GetId())
}
```
适用场景:CRUD 常规操作、单次计算任务、短平快的查询。这是占 gRPC 流量 **90%+** 的模式。
## Server Streaming —— 服务端流式
服务端一次性返回多个结果,适合批量查询或推送:
```mermaid
sequenceDiagram
participant C as Client
participant S as Server
C->>S: ListOrders(request)
S-->>C: Order[1]
S-->>C: Order[2]
S-->>C: Order[3]
S-->>C: EOF
Note right of C: 客户端收到完整列表
```
```go
func (s *orderServer) ListOrders(req *pb.ListOrdersRequest, stream pb.OrderService_ListOrdersServer) error {
orders := s.store.ListAll()
for _, o := range orders {
if err := stream.Send(o); err != nil {
return err
}
}
return nil
}
```
> [!warning] 服务端必须主动关闭流
>
> 如果服务端 `Send` 循环中没有遇到错误或主动返回 `nil`,Stream 永远不会结束,客户端将永远阻塞在 `Recv()`。务必确保所有路径都会退出循环或返回错误。
## Client Streaming —— 客户端流式
客户端连续发送多条消息,服务端最后返回聚合结果。适合大数据上传:
```mermaid
sequenceDiagram
participant C as Client
participant S as Server
C->>S: BatchRecord[1]
C->>S: BatchRecord[2]
C->>S: BatchRecord[3]
C->>S: CloseSend()
Note right of S: 收集所有数据
S-->>C: BatchResult{count: 3}
```
```go
func (s *orderServer) UploadLogs(stream pb.OrderService_UploadLogsServer) error {
var count int32
for {
log, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
return err
}
s.store.Log(log.Message)
count++
}
return stream.SendAndClose(&pb.UploadResult{Count: count})
}
```
适用场景:批量写入日志、文件分块上传、批量创建记录。注意要在客户端控制流速,避免一次性塞爆缓冲区。
## Bidi Streaming —— 双向流式
双方各自独立发送流,完全异步。**最适合实时场景**(聊天、行情推送、协同编辑):
```mermaid
sequenceDiagram
participant CS as Client Stream
participant SS as Server Stream
CS->>SS: Msg[1]: "Hello"
SS->>CS: Reply[1]: "Hi!"
CS->>SS: Msg[2]: "What's up?"
SS->>CS: Reply[2]: "Not much"
CS->>SS: Msg[3]...
SS->>CS: Reply[3]...
CS--)SS: Cancel
SS--)CS: Cancel
```
```go
// 实战示例:WebSocket-like 的实时通知推送
func (s *server) SubscribeNotifications(
req *pb.SubscribeRequest,
stream pb.NotificationService_SubscribeServer,
) error {
// 注册订阅
notifier.Register(stream.Context().Done(), func() {
stream.Send(&pb.Notification{})
})
<-stream.Context().Done()
return stream.Context().Err()
}
```
> [!question] 选型思考
>
> 假设你要实现一个实时订单状态推送功能(前端监听订单从"待支付"→"已发货"→"已完成"的变化),你会选哪种流式模式?为什么不用 WebSocket?
gRPC 双向流是**强类型 + 全双工**的替代方案,不需要额外建 WebSocket 通道,Proto 定义即契约,代码自动生成即客户端 SDK。对于纯内部服务链路,gRPC 双向流通常优于 WebSocket。
### Bidi Streaming 的生命周期管理
```mermaid
flowchart TD
Start["建立双向连接"] --> Idle["空闲等待"]
Idle --> SendMsg["客户端 send"]
Idle --> SendReply["服务端 send"]
SendMsg --> Idle
SendReply --> Idle
SendMsg --> ClientCancel{"client cancel?"}
SendReply --> ServerCancel{"server cancel?"}
ClientCancel -->|yes| Cleanup["释放资源"]
ServerCancel -->|yes| Cleanup
Cleanup --> End["断开连接"]
style Start fill:#e3f2fd
style End fill:#ffcdd2
style Cleanup fill:#fff3e0
```
> [!keypoint] 黄金法则
>
> 双向流的**任何一方都可以随时单方面关闭发送端**(`CloseSend` 或 `context.Done`)。另一方应检测到 EOF 并及时清理资源,而不是无限等待。
## 四种模式速查
| 模式 | 客户端消息数 | 服务端消息数 | 典型场景 |
|------|------------|------------|---------|
| Unary | 1 | 1 | CRUD 常规操作 |
| Server Stream | 1 | N | 列表查询、日志流拉取 |
| Client Stream | N | 1 | 批量写入、大文件分块上传 |
| Bidi Stream | N | M | 聊天室、实时协作、行情推送 |
> [!note] 组合使用
>
> 同一个 Service 中可以混合使用四种模式。比如 `OrderService` 里:
> - `CreateOrder` 用 Unary(创建后立即返回结果)
> - `ListOrders` 用 Server Stream(大量数据分批返回)
> - `ImportRecords` 用 Client Stream(批量导入)
> - `NotifyOrderStatus` 用 Bidi Stream(实时状态变更推送)
## 关联笔记
- [[01-协议与架构]] — 流式建立在 HTTP/2 Stream 的多路复用之上
- [[04-拦截器]] — 流式调用同样可以通过 StreamInterceptor 进行切面处理
- [[05-错误处理]] — 流式调用的错误处理与普通模式有差异
- [[06-连接管理]] — 长时间存活的流式连接需要 Keepalive 保活