C# Server-Sent Events (SSE)实现方法 C# ASP.NET Core如何实现SSE

1次阅读

ASP.net Core 6+ 中最简单返回 SSE 流的方式是手动写入 HttpResponse.Body 并设置响应头:StatusCode=200、ContentType=”text/Event-stream”、Cache-Control=”no-cache”、Connection=”keep-alive”,每次写入 data: 消息后调用 FlushAsync,且所有 await 必须传入 HttpContext.RequestAborted 以防止资源泄漏。

C# Server-Sent Events (SSE)实现方法 C# ASP.NET Core如何实现SSE

ASP.NET Core 6+ 中用 IActionResult 返回 SSE 流最简单

不需要引入第三方库,.NET 6 起内置了对 Server-Sent Events 的基础支持。核心是返回一个持续写入的 FileStreamResult 或更推荐的 StreamingFileResult 变体——但实际最稳妥的是直接用 HttpResponse 写入原始流,并手动设置响应头。

关键点:必须禁用响应缓冲、设置正确的 MIME 类型和缓存策略,否则浏览器收不到实时事件

  • Response.StatusCode = 200
  • Response.ContentType = "text/event-stream"
  • Response.Headers.Add("Cache-Control", "no-cache")
  • Response.Headers.Add("Connection", "keep-alive")
  • 调用 Response.Body.FlushAsync() 每次写完一行后(尤其在开发环境 iis express 下容易卡住)

HttpResponse 手动写入 SSE 格式数据

SSE 协议本身极轻量:每条消息由若干字段行(data:id:event:retry:)组成,空行分隔。浏览器只认 data: 开头的行,且会自动拼接多行 data: 成一个完整字符串

示例片段(在 Controller Action 中):

Response.StatusCode = 200; Response.ContentType = "text/event-stream"; Response.Headers.Add("Cache-Control", "no-cache"); Response.Headers.Add("Connection", "keep-alive");  var writer = new StreamWriter(Response.Body, Encoding.UTF8) { AutoFlush = true }; while (!HttpContext.RequestAborted.IsCancellationRequested) {     await writer.WriteLineAsync($"data: {{"time":"{DateTime.Now:O}"}}");     await writer.WriteLineAsync("");     await Task.Delay(1000, HttpContext.RequestAborted); }

注意:AutoFlush = true 很重要;若不用 StreamWriter,直接用 Response.Body.WriteAsync,记得每次写完调 FlushAsync

避免 jsonSerializerSystem.Text.json 自动换行导致格式错误

如果用 JsonSerializer.Serialize 输出对象再写入流,它默认不换行,但你仍需手动加 data: 前缀和末尾空行。更麻烦的是,若 JSON 含换行符(如字符串里有 n),SSE 会把它当成消息分隔,导致解析失败。

  • 不要直接 WriteAsync(JsonSerializer.Serialize(obj))
  • 应先序列化为单行 JSON:JsonSerializerOptions options = new() { WriteIndented = false };
  • 然后拼接:await writer.WriteLineAsync($"data: {json}");
  • 严格确保每条消息以 data: 开始、以空行结束

客户端断连时如何安全清理后台任务

ASP.NET Core 不会自动取消已启动的异步循环。若用户关闭页面或网络中断,HttpContext.RequestAborted 是唯一可靠信号,但必须在所有 await 点都传入它。

常见陷阱:

  • 忘记在 Task.Delay(1000) 里传 HttpContext.RequestAborted → 任务继续跑,资源泄漏
  • while (true) 但没检查 IsCancellationRequested → 无法退出
  • 在循环中启动新 Task.Run 且未绑定 CancellationToken → 彻底失控

真正可靠的模式是:整个循环逻辑在一个 async 方法里,每个 await 都带 token,外层用 try/finallyusing 清理资源(比如取消 Timer、释放数据库连接等)。

真实项目里,SSE 很少裸写循环;多数会结合 IAsyncEnumerable + ChannelReaderIObservable 做解耦,但底层响应流的写法和头设置逻辑完全一致。最容易被忽略的,其实是 FlushAsync 的调用时机和 RequestAborted 的全程穿透 —— 这两点一漏,就变成“看似能发,实则收不到”或者“服务端悄悄积数百个僵尸任务”。

text=ZqhQzanResources