bigywater.tech
gRPC · 9

客户端流与双向流:什么时候 Recv 会返回 EOF

客户端流和双向流里,什么时候 Recv 返回 io.EOF?CloseSend 在线上是什么?服务端先结束了客户端还能 Send 吗?两边都在发时会不会死锁?这篇逐个验证。

长度 0 的 DATA
CloseSend 在线上:带 END_STREAM 的空 DATA 帧
5 条
双向流里客户端只 Send 不 Recv,每条 256 KiB 时 Send 成功的条数(之后死锁)
io.EOF
服务端提前结束时,客户端后续每次 Send 的返回值(无论服务端返回 nil 还是错误)

CloseSend

实测 · 本机执行输出
  客户端发了 3 条、还没 CloseAndRecv:服务端 (无记录)(没有 EOF,在等更多消息)
  CloseAndRecv 后:resp.N=3 err=<nil>;服务端 Recv 收到 io.EOF(共 3 条),SendAndClose
     C→S HEADERS  长 25  标志 END_HEADERS
     C→S DATA     长 9   标志 
     C→S DATA     长 9   标志 
     C→S DATA     长 9   标志 
     C→S DATA     长 0   标志 END_STREAM

CloseSend 就是一个长度为 0 的 DATA 帧加 END_STREAM,表示客户端到服务端这个方向结束。服务端 Recv 得到 io.EOF 就是它。

半关闭

实测 · 本机执行输出
  客户端依次收到 [late-0 late-1 late-2],最后 Recv 返回 EOF;服务端收到 EOF 之后又发了 3 条

客户端 CloseSend 之后服务端还可以继续 Send,客户端 Recv 到服务端 handler 返回才得到 io.EOF。两个方向独立关闭。

Send 返回 io.EOF

实测 · 本机执行输出
  early-ok  服务端读了 1 条就 return nil:后续 3 次 Send 返回 [EOF EOF EOF];Recv 返回 io.EOF(状态为 OK)
  early-err 服务端读了 1 条就 return PermissionDenied:后续 3 次 Send 返回 [EOF EOF EOF];Recv 返回 code=PermissionDenied message="不让你继续"

服务端提前结束后,客户端 Send 返回 io.EOF,无论服务端返回的是 nil 还是错误。真正的状态要调 Recv 才能拿到。

死锁

实测 · 本机执行输出
  1 秒后客户端只 Send 成功了 5 条(共 1280 KiB),之后卡住;服务端状态:(服务端 handler 还没返回,阻塞在 Send 上,没有日志)
  deadline 到后:客户端 Send 退出;服务端:服务端 Send 错误:rpc error: code = Canceled desc = context canceled
  对照:客户端用另一个 goroutine 同时 Recv:100 条(25 MiB)Send 完,用时 261.9 ms;服务端:服务端 Recv 收到 io.EOF(客户端已 CloseSend)

双向流里服务端同一个 goroutine 里收一条回一条,客户端只管 Send 不 Recv,两边的窗口都会满,双方互相等待。收发要放在不同的 goroutine 里。

不 CloseSend

实测 · 本机执行输出
  500 ms 后服务端:(服务端还没返回)
  deadline 到了之后客户端 Recv 返回 code=DeadlineExceeded;服务端:服务端 Recv 错误:rpc error: code = Canceled desc = context canceled

客户端不 CloseSend,服务端的 Recv 永远等不到 EOF,只能等到 deadline 或取消。

结束方式一览

实测 · 本机执行输出
  服务端 return nil 客户端 Recv 最终:io.EOF(状态为 OK)           服务端记录:服务端读了 1 条就 return nil
  服务端 return 错误  客户端 Recv 最终:code=PermissionDenied    服务端记录:服务端读了 1 条就 return PermissionDenied
  客户端 CloseSend  客户端 Recv 最终:io.EOF(状态为 OK)           服务端记录:服务端 Recv 收到 io.EOF(客户端已 CloseSend)
  客户端 cancel     客户端 Recv 最终:code=Canceled            服务端记录:服务端 Recv 错误:rpc error: code = Canceled desc = cont

io.EOF 只出现在状态为 OK 的时候;非 OK 的状态,Recv 返回带 code 的错误;取消得到 Canceled。

分步演示

客户端流与双向流
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1 / grpc-go v1.84.0 / protobuf-go v1.36.12 / golang.org/x/net v0.57.0 / protoc 36.2(Homebrew);服务端和客户端都是本机起的 Go 进程(127.0.0.1,端口 9800 以上,不访问任何外部服务);需要看线上字节时用我自己写的 TCP 抓包代理加 golang.org/x/net/http2 的 Framer 解析 HTTP/2 帧;计时用 time.Now 或 perf_counter,数字每次运行会有小幅抖动。线上帧来自抓包代理;死锁实验用 2 秒 deadline 终止;所有结果是确定的,耗时为单次测量
    • 没有验证:服务端流与客户端流同时存在时的更多状态组合;grpc-go 其它版本对 Send 返回 io.EOF 的行为;非 grpc-go 实现的半关闭语义;死锁时窗口的具体占用(只数了 Send 成功的条数)
    京ICP备2026067502号-1京公网安备11010502063487号