grpc流式传输需严格遵循proto语法和错误处理规范:stream关键字位置决定流类型,send()/recv()必须逐次检查错误,配置缓冲区与超时参数,应用层实现背压,否则易卡死或oom。

gRPC 流式传输不是开个 stream 就能跑通,写错一个关键字位置、漏查一次 Send() 错误、不设缓冲区大小,流就会卡死或 OOM。
proto 中 stream 关键字写错位置,生成的 Go 接口就完全不能用
服务端流、客户端流、双向流的语义,只由 stream 在 rpc 声明中的位置决定,不是修饰符,是语法硬约束:
- 服务端流必须写成
rpc Subscribe(Request) returns (stream Response)—— 客户端发一次,服务端可多次Send() - 客户端流必须写成
rpc Upload(stream Chunk) returns (Result)—— 客户端多次Send(),服务端最后Return - 双向流必须写成
rpc Sync(stream Req) returns (stream Resp)—— 双方各自独立Send()/Recv() - 写成
rpc Bad(stream Req) returns (Resp)→ 生成的是客户端流接口,但开发者误当服务端流用,调Recv()却收不到数据 - 写成
rpc Bad(Req) returns (Resp)(漏stream)→ 生成 unary 接口,根本没Send()方法
stream.Send() 卡住或只发几条就停,90% 是错误没检查
Send() 是同步阻塞调用,失败后不处理,下一次调用会 panic 或静默失败。常见卡点:
- 每次
stream.Send(&msg)后必须加if err != nil { return err },不能只在循环末尾判断 - 复用同一个
*pb.Response实例反复改字段再Send()—— proto 序列化复用内部 buffer,后一次覆盖前一次,客户端收到空或乱码 - 数据源是数据库游标或文件读取时,
io.EOF到来前没完成最后一次Send(),客户端永远等不到io.EOF,连接挂起 - 没监听
stream.Context().Done(),客户端断连后服务端还在往已关闭 stream 写,触发transport is closing
大消息或高吞吐场景下内存暴涨、连接被中间件断开
默认配置在真实生产环境几乎不可用,必须显式调整:
-
grpc.MaxRecvMsgSize(64 * 1024 * 1024)和grpc.MaxSendMsgSize(64 * 1024 * 1024)必须两端都设,否则超过 4MB 默认限制直接报rpc error: code = ResourceExhausted desc = grpc: received message larger than max - 避免在 proto 中定义
repeated bytes data或超大bytes字段 —— protoc 生成代码会尝试一次性分配整块内存,低内存容器必崩 - Nginx/Envoy 默认 60s 超时,需配
grpc.KeepaliveParams+grpc.WithKeepaliveParams发心跳帧保活 - 服务端用带缓冲 channel(如
make(chan *pb.Event, 100))做应用层背压,防止慢消费者拖垮上游
客户端正确读流:别用 for range,要手动判 io.EOF
stream.Recv() 不是 channel,不能 for range;标准读法是显式循环 + 错误分支:
for {
res, err := stream.Recv()
if err != nil {
if err == io.EOF {
break
}
log.Printf("recv error: %v", err)
return err
}
handle(res)
}
漏掉 err == io.EOF 分支,会导致客户端把连接异常当成业务错误处理;固定次数循环(比如 for i := 0; i )更危险 —— 数据没发完就退出,流提前终止。
真正难的不是启动流,而是让流在各种网络抖动、消费延迟、OOM 边界下持续稳住 —— 背压靠应用层 channel 控制,可靠性靠每次 Send()/Recv() 的错误检查,而这一切的前提,是 stream 关键字写对位置。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











