双向流不卡死的核心是并发读写而非串行驱动:服务端须用IAsyncEnumerable签名并手动推送,客户端需分离读写任务,且必须正确处理生命周期、帧开销、连接配置与异常对齐。
双向流不卡死的核心不是“怎么写”,而是“不能串行驱动”——服务端必须并发读请求+推响应,客户端必须并发发数据+收状态,否则必然挂起或丢消息。
服务端方法签名必须返回
,且不能只靠
驱动
生成的双向流服务端方法签名固定为:
。这不是建议,是强制约束;若手动改成
或
,运行时直接抛
。
只负责拉取请求,它本身不触发响应发送,也不感知外部推送需求
需要主动推送(如心跳、后台事件、定时通知)时,必须另启
,并在其中调用
每次
前必须检查
,否则可能向已断开的连接写入,引发
多个任务并发调用
是安全的,但 gRPC 不保证顺序;若需严格序,加
或用
串行化写入
客户端必须并发读写,
不能早于所有写操作完成
典型错误是把双向流当同步调用用:
——这会导致后续
永远没机会执行,因为
卡在等待流结束。
正确做法是分离读写:用
单独发包
在主线程或另一任务中消费响应,
是扩展方法,要求
必须显式调用,否则服务端
永远不会返回
,流无法自然终止
若服务端返回失败状态(如
),应立即
并
,避免继续发无效块
传文件或高频小消息时,
的帧开销比
高得多
这不是语法问题,是 HTTP/2 底层机制决定的:每个
都触发一次完整 DATA 帧封装 + protobuf 序列化,而单次
是整包序列化。10KB 小消息每秒发 100 条,CPU 花费可能 70% 在帧调度上。
C知道
CSDN推出的一款AI技术问答工具
下载
高频场景(如传感器上报、聊天心跳)别用
包裹业务逻辑,改用
手动控制读取节奏
文件传输必须分块,
字段用
或
,别用
;单块建议 128KB~512KB,避开 gRPC 默认 4MB 消息上限
客户端写块后可加
缓冲,防止单次压满 TCP 窗口导致写阻塞
服务端落盘必须流式处理,别全加载进内存再
,否则大文件直接 OOM
几乎都不是服务宕机
这个错误 90% 以上源于客户端连接配置不当,和 HTTP/2 明文支持、连接复用、Kestrel 同步 IO 设置强相关。
必须全局复用,禁止每次调用都
;默认连接池不共享,短连接重建频繁会触发重置
非 TLS 场景下(如本地调试),必须提前设置:
,否则降级 HTTP/1.1,双向流直接失败
服务端 Kestrel 若配置了
,务必同时设
,否则高并发时同步读写触发连接中断
客户端超时要分层设:channel 级(底层连接)、call 级(单次流)、CancellationToken 级(应用逻辑),三者不一致容易出现“连接已断但还在等响应”的假死
真正难的不是写出能跑的双向流,而是让两个独立流的生命周期在各种异常路径下仍能对齐:网络抖动时谁先取消、服务重启时客户端如何重连、大文件传输中途失败怎么续传——这些边界条件,光看
示例代码根本覆盖不到。
IAsyncEnumerableawait foreachpublic virtual async Task BidirectionalStreamingCall(IAsyncEnumerable requestStream, IServerStreamWriter responseStream, ServerCallContext context) Task> IObservableInvalidOperationExceptionawait foreach (var req in requestStream)Task.Run(() => PushLoop(responseStream, context))responseStream.WriteAsync()WriteAsync()context.CancellationToken.IsCancellationRequestedObjectDisposedExceptionWriteAsync()lockChannelCompleteAsync()await call.RequestStream.WriteAsync(...); await foreach (var r in call.ResponseStream) { ... }WriteAsync()await foreachTask.Run(async () => { foreach (...) await call.RequestStream.WriteAsync(...); await call.RequestStream.CompleteAsync(); })await foreach (var status in call.ResponseStream.ReadAllAsync(ct))ReadAllAsync()Grpc.Net.Client >= 2.47CompleteAsync()requestStream.ReadAsync()falsestatus.Success == falsect.Cancel()await call.RequestStream.CompleteAsync()IAsyncEnumerableTaskyield returnTaskawait foreachwhile (await requestStream.MoveNext(ct))FileChunk.dataReadOnlyMemoryByteStringbyte[]await Task.Delay(1)File.WriteAllBytesRpcException: Status(StatusCode=Unavailable, Detail="Connection reset")GrpcChannelGrpcChannel.ForAddress()AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true)HttpProtocols = HttpProtocols.Http1AndHttp2AllowSynchronousIO = falseawait foreach