198 lines
6.2 KiB
Markdown
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 保活
|