跳转到主内容
趣航编程网 - 趣学编程,启航技术之路!

C#怎么使用gRPC双向流_C#实现高效的实时数据推送【高级】

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

相关文章