
gRPC服务端流怎么写才不会卡住客户端
服务端流的核心是让服务器持续发消息、客户端一次连接收多次响应,但新手常写成“发完就关”,结果客户端 Recv() 一直阻塞或提前报 EOF。关键不在发送逻辑,而在流的生命周期管理。
常见错误现象:context deadline exceeded 或客户端收不到最后几条消息;使用场景比如日志推送、实时指标下发、长周期任务状态更新。
- 必须在所有
Send()完成后显式调用stream.CloseSend()(仅服务端流不需要,但很多人误加)——服务端流只管发,不关发送端 - 别在循环里反复创建新
context.WithTimeout,用同一个ctx传入Send(),否则超时时间错乱 - 如果数据源是 channel,注意 channel 关闭后仍可能有残留值,
for range结束后要确认是否已自然退出,避免空转
示例片段:
for _, item := range items {
if err := stream.Send(&pb.Item{Value: item}); err != nil {
return err // 不要忽略 err,它可能是客户端断连
}
time.Sleep(100 * time.Millisecond)
}
// 这里不调 CloseSend() —— 服务端流没有“关闭发送”这回事
客户端流为什么总提示“stream terminated by server”
客户端流要求客户端先发完、再等服务端回一个响应,但多数人卡在“发完”的判定上:不是发完了,而是没告诉 gRPC “我不发了”。错误本质是没调 CloseSend(),导致服务端永远等下去,最终超时断开。
使用场景如文件分块上传、语音流识别、批量数据导入;参数差异上,stream.Recv() 必须在 CloseSend() 之后调,顺序错即失败。
立即学习“go语言免费学习笔记(深入)”;
-
CloseSend()必须调用,且只能调一次;调早了后续Send()会 panic,调晚了服务端卡住 - 若用
for+select从 channel 拉数据,记得在break后立刻CloseSend(),别等 defer - Go 1.21+ 中,如果服务端返回 error 早于
Recv(),客户端可能收不到,需检查服务端return err是否发生在SendAndClose()前
典型错误写法:
for data := range ch {
stream.Send(&pb.Data{Payload: data})
}
// ❌ 缺少 stream.CloseSend()
双向流中如何安全地并发读写
双向流(Bidi streaming)天然支持客户端和服务端同时收发,但 Go 的 grpc.Stream 并非线程安全——Send() 和 Recv() 不能在多个 goroutine 里裸奔调用,否则大概率触发 panic: send on closed channel 或数据错乱。
性能影响明显:单 goroutine 串行读写会成为瓶颈;兼容性上,所有 gRPC 版本都要求读写分离。
- 固定模式:起两个 goroutine,一个专责
Send()(写),一个专责Recv()(读),用sync.WaitGroup或context.Done()协同退出 - 写 goroutine 要监听
ctx.Done(),收到取消信号时主动return,避免往已关闭流继续Send() - 读 goroutine 收到
io.EOF应立即退出,不要尝试再次Recv(),否则阻塞
简写示意:
go func() {
for data := range sendCh {
if err := stream.Send(data); err != nil {
return // 不再重试
}
}
}()
go func() {
for {
resp, err := stream.Recv()
if err == io.EOF { break }
if err != nil { return }
handle(resp)
}
}()
流式 RPC 的错误处理为什么不能只看 err == nil
流式调用的 err 只反映最后一次 Send() 或 Recv() 的底层状态,但业务语义错误(比如服务端校验失败)往往藏在响应体里,或者以 Status 形式附在流结束时。只判 err != nil 会漏掉大部分真实失败。
容易踩的坑是把流当普通函数用,以为“没报错=成功”,结果数据丢了都不知道;可观察点包括 grpc.Status、响应字段中的 code、以及 stream.Context().Err() 是否提前取消。
- 每次
Recv()后,用status.FromError(err)解析真实状态码,特别关注codes.InvalidArgument或codes.FailedPrecondition - 服务端返回非 OK 状态时,
Recv()可能返回nil响应 + 非 nilerr,也可能返回有效响应 + 附带 status(取决于实现),必须统一检查 - 客户端流和双向流中,
CloseSend()本身不返回 error,但后续Recv()可能立刻返回服务端拒绝错误,别跳过
检查示例:
resp, err := stream.Recv()
if err != nil {
st := status.FromError(err)
if st.Code() == codes.PermissionDenied {
log.Println("权限不足,服务端拒绝处理")
}
}
流式 RPC 最难缠的不是语法,是状态边界——什么时候算“发完了”,什么时候算“收完了”,什么时候该取消,什么时候该重试。这些边界在文档里不写,在生成代码里也不体现,全靠对 Send()/Recv()/CloseSend() 三者协作时机的手感。写多了就会发现,出问题的从来不是逻辑,而是那行少写的 CloseSend(),或者那条没检查的 status.Code()。










