gRPC服务端需用public async IAsyncEnumerable方法配合[EnumeratorCancellation]参数返回流式数据,禁用Task;客户端async foreach需捕获RpcException并基于游标重试,Protobuf须声明stream关键字。
IAsyncEnumerable
ASP.NET Core 6+ 的 gRPC 服务支持直接将 IAsyncEnumerable 作为流式响应类型,但必须配合 yield return 或手动构造可取消的异步枚举器;直接返回 Task 会导致客户端收不到任何消息,因为 gRPC 框架只识别裸的 IAsyncEnumerable(不是 Task 包裹的)。
public async IAsyncEnumerable StreamData(Request request, [EnumeratorCancellation] CancellationToken cancellationToken = default)
[EnumeratorCancellation] 是关键:它让 gRPC 在客户端断连时自动触发 cancellation,避免后台任务泄漏await foreach 消费另一个 IAsyncEnumerable 后再 yield —— 这会阻塞流式推送;应直接 yield return 或使用 Channel.Reader.ReadAllAsync()
Channel + 后台生产者,而非拼接多个 IAsyncEnumerable
IAsyncEnumerable 流时的生命周期陷阱客户端 C# 使用 async foreach 消费服务端流时,GrpcChannel 不会自动重连或重试;一旦底层 HTTP/2 连接中断(如超时、网络抖动),MoveNextAsync() 会抛出 RpcException 并终止循环 —— 不会自动恢复流。
RpcException 并检查 Status.StatusCode == StatusCode.Unavailable 才考虑重试async foreach 外层套 try/catch 后简单重进循环:这会丢失已消费的项,且可能重复请求lastSeenId 参数),客户端在异常前记录最后处理的 IDCancellationToken 传给 foreach 仅控制当前迭代,不影响连接本身;连接级超时由 CallOptions 中的 Deadline 控制IAsyncEnumerable 和传统 IServerStreamWriter 的性能与调试差异两者都走 gRPC Server Streaming,但底层行为不同:IAsyncEnumerable 由框架自动管理写入节奏和背压,而 IServerStreamWriter 要求你手动调用 WriteAsync,并自行处理 HttpContext.RequestAborted。
IAsyncEnumerable 的异常堆栈更干净,错误直接出现在 yield return 行;IServerStreamWriter 的异常可能被吞掉或延迟抛出IAsyncEnumerable 默认使用 Channel 缓冲,缓冲区大小影响内存占用;可通过 Channel.CreateBounded(new BoundedChannelOptions(100)) 显式控制IServerStreamWriter;IAsyncEnumerable 是“fire-and-forget”模型IAsyncEnumerable 方法更简单:直接 await foreach + Assert,无需模拟 ServerStreamWriter
Protobuf 定义中必须声明 stream 关键字,否则 dotnet-grpc 工具不会为服务端生成 IAsyncEnumerable 返回类型,而是退化为单次响应。
service DataStreamer {
rpc StreamUpdates (StreamRequest) returns (stream StreamResponse); // ✅ 必须有 stream
}
Grpc.AspNetCore(IAsyncEnumerable 支持不完整,需更新 NuGet 包Grpc.Net.Client ≥ 2.46.0,否则 CallInvoker.AsyncStreamingCall 可能无法正确包装 IAsyncEnumerable
*.Grpc.cs 文件里,服务端接口方法返回类型应为 IAsyncEnumerable;若仍是 Task,检查 .proto 是否漏了 stream 或是否启用了 grpc_use_deprecated_api
实际用起来最易忽略的是:服务端 IAsyncEnumerable 方法里的 cancellationToken 必须加 [EnumeratorCancellation] 属性,否则客户端断开时,你的 while 循环根本收不到通知,协程就卡在那儿了。