.NET 10 SSE实战:别再为了进度条强上SignalR了
很多人习惯性地认为 JSON 响应在服务端会被缓存,客户端必须等整个请求结束才能收到数据,所以只要做实时进度条,第一反应就是部署 SignalR。但实际上,.NET 的
完整实操代码参考:
下一篇
分享一个关于内容创作的反直觉发现 →
IAsyncEnumerable 早就能实现流式传输。我之前做了一个简单的测试,写了个 Minimal API,每 400ms 产生一个数据项:
app.MapGet("/steps/json", (CancellationToken ct) => Produce(padding: 0, ct));
async IAsyncEnumerable Produce(int padding, [EnumeratorCancellation] CancellationToken ct = default)
{
for (var i = 1; i <= 8; i++)
{
await Task.Delay(400, ct);
yield return new Step(i, $"step {i}/8", "");
}
}用 curl 测发现,数据其实是一条条实时蹦出来的,根本没有缓存。之所以大家觉得它在缓存,是因为前端 fetch(...).json() 必须等 body 结束才 resolve,这给了人一种“服务端在缓冲”的错觉。
为了彻底解决浏览器端的接收问题,.NET 10 终于把 SSE (Server-Sent Events) 做成了内置的一等公民。现在的写法极其简单,直接改个返回类型就行:
app.MapGet("/steps/sse", (CancellationToken ct) =>
TypedResults.ServerSentEvents(ProduceSse(ct)));
async IAsyncEnumerable<SseItem> ProduceSse([EnumeratorCancellation] CancellationToken ct = default)
{
await foreach (var step in Produce(padding: 0, ct))
yield return new SseItem(step, eventType: "step") { EventId = step.Number.ToString() };
}前端接收也不再需要安装任何沉重的包,两行原生 JS 搞定:
const source = new EventSource("/steps/sse");
source.addEventListener("step", e => render(JSON.parse(e.data)));实操心得与踩坑点:
- 开销对比: SSE 会增加一些 framing 字符(比如
event:和data:),对于极小的数据包会有 60% 左右的额外开销,但对于常规业务数据来说可以忽略不计。 - 连接限制: HTTP/1.1 下浏览器对同域连接数有限制(约 6 个),每个
EventSource都会占用一个。所以建议务必在 HTTP/2 环境下部署。 - 中间件干扰: 响应压缩(Response Compression)或某些反向代理可能会强行把流式响应变成一个大 Blob,导致 SSE 失效,部署时得检查配置。
- 适用场景: 如果是纯单向的进度推送或仪表盘更新,.NET 10 的 SSE 应该是首选;如果是需要双向通信或大规模扇出推送,再考虑 SignalR。
完整实操代码参考:
https://github.com/ssukhpinder/dev-to-code-samples/tree/main/004-json-streaming-vs-sse