gRPC 四种流模式与双向流实战
更新时间:2026-09-18。gRPC 不只有「一问一答」。借助 HTTP/2 多路复用,它支持四种调用形态:Unary(一元)、服务端流、客户端流、双向流。选型的关键不是「流很酷」,而是数据在时间轴上如何产生、谁先谁后、是否需要边算边传。本文给出四种模式的 proto 与写法、双向流时序和坑。基础见 gRPC 与 Protobuf,连接层见 gRPC keepalive 与多路复用。
本文要回答的问题
- 四种流模式的 proto 关键字
stream写在请求还是响应侧? - 服务端流适合什么(大数据集、进度推送)?客户端流适合什么(聚合上报)?
- 双向流在一条 HTTP/2 流上是怎么交互的?请求和响应是否一一对应?
- 流的背压、并发、顺序、取消怎么处理?
- 什么时候该用流、什么时候反而该用分页 unary 或 WebSocket/SSE?
一、四种模式总览
syntax = "proto3";
service Demo {
// 1) Unary:一请求一响应
rpc Get(UserReq) returns (UserResp);
// 2) 服务端流:一请求,响应是流
rpc List(PageReq) returns (stream Item);
// 3) 客户端流:请求是流,最后一个响应汇总
rpc Upload(stream Chunk) returns (UploadAck);
// 4) 双向流:双方都是流
rpc Chat(stream ChatMsg) returns (stream ChatMsg);
}| 模式 | 请求 | 响应 | 典型场景 |
|---|---|---|---|
| Unary | 单个 | 单个 | 普通 RPC、查询/命令 |
| 服务端流 | 单个 | 多个 | 大列表分批、实时日志、进度/订阅推送 |
| 客户端流 | 多个 | 单个 | 文件分片上传、批量聚合、指标上报 |
| 双向流 | 多个 | 多个 | 聊天、协同编辑、交互式数据流 |
二、服务端流与客户端流
// 服务端流:边查边发,避免一次性组超大响应
@Override
public void list(PageReq req, StreamObserver<Item> out) {
try (var rows = db.scan(req)) {
while (rows.hasNext()) {
out.onNext(rows.next()); // 分批下发,受 HTTP/2 flow control 背压
}
out.onCompleted();
} catch (Exception e) {
out.onError(Status.INTERNAL.withCause(e).asException());
}
}// 客户端流(Go):循环收完客户端所有分片,最后一次性 ack
func (s *server) Upload(stream Demo_UploadServer) error {
var size int64
for {
chunk, err := stream.Recv() // io.EOF 表示客户端发完
if err == io.EOF {
return stream.SendAndClose(&pb.UploadAck{Size: size})
}
if err != nil { return err }
size += int64(len(chunk.Data))
}
}服务端流解决「一次返回太大/结果逐步产出」;客户端流解决「客户端数据逐步产生、服务端聚合」。两者都复用同一条 HTTP/2 stream,不需要为每批数据新建连接。
三、双向流:一条流上的两路独立序列
双向流最容易误解:请求消息序列和响应消息序列是两条逻辑流,跑在同一条 HTTP/2 stream 上,并不要求一请一答。服务端可以收到任意条请求后、以任意顺序(但每一侧各自有序)回任意条响应。

// 双向流服务端:onNext 里收,异步地往 responseObserver 发
@Override
public StreamObserver<ChatMsg> chat(StreamObserver<ChatMsg> out) {
return new StreamObserver<>() {
public void onNext(ChatMsg m) {
ChatMsg reply = bot.handle(m);
if (reply != null) out.onNext(reply); // 可回 0..N 条
}
public void onError(Throwable t) { cleanup(); }
public void onCompleted() { out.onCompleted(); }
};
}顺序语义:单条 stream 上消息按发送顺序到达(HTTP/2 保证字节流有序);但服务端若用多线程处理并直接 out.onNext,响应顺序由线程调度决定。需要严格保序要么单线程处理,要么在响应里带序号由客户端重排,或为每个会话建独立 stream。
四、背压、取消与并发
- 背压:HTTP/2 有 flow control,发送快、对端收得慢时写会阻塞/待发送缓冲堆积。不要无上限往
onNext灌;用响应式(gRPC-Java 与 Reactive Streams/Context cancellation 集成)或在客户端及时Recv消费。 - 取消与超时:客户端
onCancel/context.cancel()应让服务端及时停止无意义计算;务必设置deadline,服务端检查Context.current().isCancelled()或 ctx.Done()。 - 一条 stream 不是多条并发通道:要在一个连接上并发多路,应开多个 RPC(多个 stream),由同一条 TCP/HTTP/2 连接多路复用,而不是在单条双向流里自己实现多路复用(除非加 correlationId 自管)。
- 错误:流式中用
onError(status)/ 返回 gRPC status 终止;HTTP/2 是单条 stream 出错通常只结束该 stream,不影响连接上其他 stream。
五、选型与坑
- 别为了「实时」一律双向流。 只需服务端推(行情、通知)用服务端流更简单;浏览器直连场景考虑 gRPC-Web/SSE/WebSocket(见 SSE 与 WebSocket 怎么选)。
- 把双向流当请求-响应配对。 它不保证配对,要关联请用消息内带 requestId。
- 服务端多线程回写导致乱序。 需要顺序就串行化或加序号。
- 忽略背压把内存打爆。 慢消费者 + 快生产者会堆积,要限流和及时消费。
- 长流被中间设备掐断。 需要 keepalive 保活,见 keepalive 与连接复用;鉴权/埋点见 gRPC 拦截器。
REST/JSON 与 gRPC 的整体取舍见 gRPC 与 Protobuf。
总结
gRPC 四种流模式:用 proto 里 stream 加在请求/响应侧区分——Unary 一对一、服务端流把大数据集或进度/订阅分批下发、客户端流用于分片上传与批量聚合、双向流在一条 HTTP/2 stream 上跑两条各自有序但不强制配对的消息序列,适合聊天协同;关键坑是单条 stream 内消息保序但多线程处理会乱序(需串行化或带序号)、要靠 HTTP/2 flow control 做背压而非无脑 onNext、必须设 deadline 并响应取消,长流配 keepalive,只需服务端推送就别上双向流。