gRPC双向流要求.proto中RPC方法的请求和响应类型前必须同时声明stream关键字,缺一不可;否则生成的接口仅为单向流,无法实现全双工通信。

proto 文件里必须同时写两个 stream 关键字
双向流不是配置开关,是 .proto 语法强制约定:请求类型和响应类型前都得有 stream。漏掉任意一个,生成的 Go 接口就不是双向流——编译能过,但运行时根本不会被 gRPC 调用。
常见错误现象:
-
rpc Chat(Message) returns (stream Message)→ 服务端流,客户端只能发一次,ChatClient类型不带Send方法 -
rpc Chat(stream Message) returns (Message)→ 客户端流,服务端只能回一次,ChatServer类型没有Send能力 - 你写了
rpc Chat(stream Message) returns (stream Message),但 protoc 没装protoc-gen-go-grpc插件 → 生成的是旧版接口,Send/Recv方法缺失或签名错乱
服务端方法签名不能手动加 context.Context
生成的服务端方法签名是固定的,例如 func (s *server) Chat(stream pb.ChatService_ChatServer) error。你不能在参数里硬塞一个 ctx context.Context,也不能在函数体内直接用外部传入的 ctx 控制流生命周期。
真正该用的上下文来自流本身:stream.Context()。它自动绑定连接生命周期,且在客户端断连、超时或调用 CloseSend() 时同步取消。
立即学习“go语言免费学习笔记(深入)”;
错误做法后果:
- 自己传
context.WithTimeout(context.Background(), 30*time.Second)给方法 → 超时后stream.Recv()突然返回context.Canceled,但服务端逻辑没清理资源,连接卡在半关闭状态 - 忽略
io.EOF:当Recv()返回io.EOF,表示客户端已调用CloseSend(),不是错误,要主动退出接收循环,否则继续Recv()会 panic - 在
Send()后立刻CloseSend()→ 服务端还没来得及处理完消息就收到io.EOF,中断后续接收逻辑
客户端必须用两个 goroutine 分别处理 Send 和 Recv
双向流本质是全双工异步通道。Send() 和 Recv() 互不阻塞,但放在同一个 goroutine 里交替调用,极易因某次 Recv() 阻塞导致整个流卡死——后续 Send() 永远发不出去。
正确结构:
- 发数据 goroutine:持续读取用户输入或业务事件,调用
stream.Send();必须监听stream.Context().Done(),一旦触发就停止发送,避免往已失效 stream 写数据引发 panic - 收数据 goroutine:
for { res, err := stream.Recv(); if err != nil { break } ... };遇到io.EOF是正常结束,其他err(如网络断开、transport is closing)才该退出并上报 - 初始化流时绝不能用
context.Background();必须用context.WithTimeout或context.WithCancel,否则流 hang 住就会永久泄漏 goroutine
stream.Send() 失败通常意味着连接已不可用
Send() 返回非 nil error,基本可判定底层 HTTP/2 连接已断开或对端静默关闭。这不是重试就能解决的问题——gRPC 默认不重试流方法,出错即终止。
典型错误信息:rpc error: code = Unavailable desc = transport is closing。常见诱因:
- 服务端处理慢,客户端发太快,触发 HTTP/2 流控或窗口耗尽
- NAT 或代理设备超时踢掉长连接
- TLS 握手失败或证书过期(尤其在容器重启后未及时更新证书)
此时应立即 return 错误,让 gRPC 自动清理资源;不要尝试重连或重发,除非上层业务做了幂等设计。长期存活的双向流,内存泄漏风险比短连接高得多,message 字段务必精简,避免嵌套深或含大二进制字段。


















