双向流通信需严格遵循grpc契约:.proto中必须双写stream关键字,代码生成需--go-grpc_out插件,服务端方法签名固定且须用stream.context(),客户端须双goroutine收发并正确处理io.eof与上下文取消。

双向流通信不是“配个选项就能跑”,而是从 .proto 定义、代码生成、服务端实现到客户端协程调度,每一步都必须严格对齐 gRPC 的契约。错一个 stream 关键字,或漏掉一个插件,运行时就卡死、类型不匹配、根本调不到函数。
proto 文件里两个 stream 关键字缺一不可
gRPC 双向流的语法是硬性约束:请求参数和返回类型前都必须加 stream。写成 rpc Chat(stream Message) returns (stream Message) 才是合法双向流;少一个就变成单向流,生成的 Go 接口类型完全不对。
-
rpc Chat(Message) returns (stream Message)→ 服务端流,客户端只能发一次,ChatClient类型不带Send方法 -
rpc Chat(stream Message) returns (Message)→ 客户端流,服务端只能回一次,ChatServer类型没有Send能力 - 漏掉任意一个,编译能过,但客户端调用后服务端函数永远不会被触发——因为生成的接口类型不匹配,gRPC 调度器直接跳过
服务端方法签名不能手动加 context.Context
生成的服务端方法签名是固定的,例如 func (s *server) Chat(stream pb.ChatService_ChatServer) error。你不能自己往参数里塞 context.Context,也不能在函数体里直接用外部传入的上下文控制流生命周期。
- 正确做法:从
stream.Context()拿上下文,它已自动绑定连接生命周期和 HTTP/2 流状态 - 错误现象:自己调
context.WithTimeout后传进去,会导致stream.Recv()突然返回context.Canceled,但服务端逻辑没感知连接已断,资源泄漏 -
Recv()返回io.EOF表示客户端调了CloseSend(),不是错误,要主动退出循环;其他 err(如网络断开、status.Error)才该return让 gRPC 清理连接
客户端必须用两个 goroutine 分别处理收发
Send() 和 Recv() 是完全异步的,混在同一个 goroutine 里交替调用极易卡死:一次 Recv() 阻塞,后续所有 Send() 就永远发不出去。
- 发数据的 goroutine 必须监听
stream.Context().Done(),一旦上下文取消(比如超时或连接断开),立刻停止Send(),避免 panic - 收数据的 goroutine 要持续
for { res, err := stream.Recv(); ... },不能只调一次 - 初始化流时绝不能用
context.Background(),必须用context.WithCancel或context.WithTimeout,否则流 hang 住就会永久泄漏 goroutine - 不要在
Send()后立刻调CloseSend(),除非你确定客户端不会再发任何消息;否则服务端Recv()会提前收到io.EOF,中断接收逻辑
生成代码时漏掉 --go-grpc_out 插件就等于没生成
protoc --go_out=. 只生成消息结构体,不生成流式接口所需的 XXXServer 和 XXXClient 类型。没这个,编译会报 undefined: pb.XXXServer。
- 必须同时安装两个插件:
protoc-gen-go和protoc-gen-go-grpc,命令分别是:go install google.golang.org/protobuf/cmd/protoc-gen-go@latest和go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest - 生成命令必须包含
--go-grpc_out=.,推荐完整写法:protoc --go_out=. --go-grpc_out=. --go-grpc_opt=paths=source_relative streaming.proto - 检查生成结果:服务端流对应的是
XXX_YYYServer类型(如StreamService_ServerStreamServer),不是*grpc.ServerStream——手动传错类型,函数根本不会被调用
最常被忽略的其实是流的“半关闭”语义:客户端调 CloseSend() 不代表流结束,只是停止发送;服务端仍可继续 Send(),直到自己也完成。这个边界不厘清,就容易在错误处理里提前退出或死等。











