gRPC客户端流创建是否为异步?双向流调用为何不阻塞等待服务端处理
问题原因
你对gRPC流RPC的调用行为理解有误,gRPC中**一元RPC(普通的请求-响应模式)**的客户端调用确实是同步阻塞的,会等服务端完整处理请求返回响应后才返回。但不管是客户端流、服务端流还是双向流,流RPC的客户端调用都不会阻塞等待服务端业务逻辑执行完成:
- 你调用
client1.Stream(ctx)的时候,gRPC客户端底层只完成了HTTP/2流的创建、请求头的发送,只要和服务端的流链路建立成功,就会立刻返回流对象sc1,不会等你服务端侧的Stream处理函数执行到把客户端加入广播列表的业务逻辑。 - 此时服务端的
Stream处理函数可能还在执行校验、初始化逻辑,还没把当前客户端注册到广播池,你立刻启动client2登录,自然会出现client1收不到广播的情况。
解决方案
要确保服务端已经完成client1的流注册逻辑再执行client2登录,有两种常用的实现方案:
方案1:业务层增加流就绪确认(推荐)
在服务端的Stream处理函数中,完成流注册逻辑后,主动给客户端发送一条流就绪的通知事件,客户端收到该事件后再执行后续逻辑:
- 先在proto的
StreamResponse中新增就绪事件类型:
message StreamResponse { oneof event { ClientLogin client_login = 1; // 新增流就绪事件 StreamReady stream_ready = 2; } } message StreamReady {}
- 服务端
Stream处理函数补充发送就绪通知的逻辑:
func (s *ChatServiceImpl) Stream(stream ChatService_StreamServer) error { // 原有逻辑:校验token、初始化流上下文 // ... // 完成流注册,加入广播列表 s.mu.Lock() s.onlineStreams = append(s.onlineStreams, stream) s.mu.Unlock() // 新增:发送流就绪通知 if err := stream.Send(&StreamResponse{ Event: &StreamResponse_StreamReady{}, }); err != nil { return err } // 原有后续流处理逻辑 // ... }
- 测试代码补充等待就绪的逻辑:
// step 3: 启动接收协程 readyc := make(chan struct{}) // 新增就绪通知通道 go func() { // 先接收就绪通知 readyRes, err := sc1.Recv() if err != nil { t.Fatalf("接收流就绪通知失败: %v", err) } if _, ok := readyRes.Event.(*StreamResponse_StreamReady); ok { close(readyc) } // 原有处理登录广播的逻辑 res, _ := sc1.Recv() t.Logf("got a response from client1 %+v", res) m := res.Event.(*StreamResponse_ClientLogin) if m.ClientLogin.Name == "test2" { loginSuccess = true } close(waitc) }() // 新增:等待流完全就绪再登录client2 <-readyc // step 4: 创建client2登录 client2 := NewChatServiceClient(conn) // ... 原有后续逻辑
方案2:测试侧临时加短时间等待(不推荐)
如果不想修改业务代码,也可以在调用Stream之后加100~500ms的睡眠,但这种方式不稳定,不同环境下可能还是会出现时序问题,仅适合临时调试用。
内容的提问来源于stack exchange,提问作者tmp dev
相关产品推荐
相关产品推荐

