gRPC流式响应需严格遵循proto语法定义,stream位置错误将导致生成接口不匹配;必须在每次Send()/Recv()后立即检查错误,显式配置缓冲和超时参数,否则易卡死或丢数据。

gRPC流式响应不是“加个stream就能自动跑通”,写错位置、漏查错误、不设缓冲,服务端发三条就卡死,客户端永远收不到io.EOF。
proto里stream关键字写在哪,直接决定Go接口长什么样
gRPC只认语法位置,不看注释或业务场景。写错一个词,生成的Go代码根本没法调Send()或Recv():
- 服务端流必须是
rpc Subscribe(Request) returns (stream Response)——客户端调一次Subscribe(),服务端可多次Send() - 客户端流必须是
rpc Upload(stream Chunk) returns (Result)——客户端循环Send(),服务端最后return单个结果 - 双向流必须是
rpc Sync(stream Req) returns (stream Resp)——双方各自独立Recv()/Send(),不能混用 - 写成
rpc Bad(stream Req) returns (Resp)→ 生成的是客户端流接口,但你当服务端流用,Recv()永远阻塞 - 写成
rpc Bad(Req) returns (Resp)(漏stream)→ 生成的是unary接口,压根没Send()方法
Send()和Recv()每次都要检查错误,不能只在循环末尾判一次
Send()是同步阻塞调用,失败后不处理,下一次调用会panic或静默丢数据;Recv()遇到io.EOF不及时退出,连接就一直挂着:
- 每轮
stream.Send(&msg)后必须跟if err != nil { return err },不能只在for循环外统一判断 -
stream.Recv()返回err == io.EOF时要立即return nil,否则客户端断连后服务端还在往已关闭流写,触发transport is closing - 复用同一个
*pb.Response实例反复改字段再Send()——proto内部buffer被覆盖,客户端收到空或乱码 - 从文件或数据库读数据时,
io.EOF到来前没完成最后一次Send(),客户端永远等不到结束信号
默认配置在生产环境基本不可用,必须显式调大缓冲和超时
HTTP/2默认窗口太小,中间件(如Envoy/Nginx)默认限制又严,不调参,大消息必OOM或被断连:
立即学习“go语言免费学习笔记(深入)”;
- 服务端启动时加
grpc.MaxRecvMsgSize(64 和<code>grpc.MaxSendMsgSize(64 ,不然单条消息超4MB直接报错 - 客户端连接时加
grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(64,否则即使服务端开了,客户端仍受默认4MB限制 - 所有流操作必须绑定带超时的
context,比如ctx, cancel := context.WithTimeout(stream.Context(), 30*time.Second),否则NAT或防火墙静默断连后goroutine永久泄漏 - 高吞吐场景下,用
bufio.NewReaderSize(conn, 1包装底层连接,减少系统调用次数
背压不是自动生效的,应用层得自己控速
HTTP/2流控窗口只管TCP层,应用层消费不过来照样OOM。尤其日志推送、传感器数据这类上游快下游慢的场景:
- 服务端别直接
for range eventCh往流里塞,要用带缓冲的channel(如make(chan *pb.Log, 100))做第一道限流 - 客户端
Recv()前先select监听stream.Context().Done()和超时,避免一次卡住整个goroutine - 大文件分块传输时,别用
os.ReadFile()一次性加载——改成io.CopyN()分片读,每片Send()后检查错误并sleep微秒级让出调度权 - 双向流中,服务端
Send()前最好select判断stream.Context().Err() == nil,防止往已断流写
最常被跳过的点:stream.Context().Done()必须监听,哪怕只是打一行log;Send()错误必须当场处理,不能攒着;proto里stream的位置不是风格问题,是编译期契约。


















