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

6.2 KiB
Raw Blame History

tags, create time
tags create time
grpc
rpc-patterns
streaming
bidirectional
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(实时状态变更推送)

关联笔记

  • 01-协议与架构 — 流式建立在 HTTP/2 Stream 的多路复用之上
  • 04-拦截器 — 流式调用同样可以通过 StreamInterceptor 进行切面处理
  • 05-错误处理 — 流式调用的错误处理与普通模式有差异
  • 06-连接管理 — 长时间存活的流式连接需要 Keepalive 保活