6.2 KiB
tags, create time
| tags | 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 —— 普通请求/响应
最常见的调用方式,一问一答:
// 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 —— 服务端流式
服务端一次性返回多个结果,适合批量查询或推送:
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: 客户端收到完整列表
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 —— 客户端流式
客户端连续发送多条消息,服务端最后返回聚合结果。适合大数据上传:
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}
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 —— 双向流式
双方各自独立发送流,完全异步。最适合实时场景(聊天、行情推送、协同编辑):
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
// 实战示例: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 的生命周期管理
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(实时状态变更推送)