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

来源:这里教程网 时间:2026-02-21 17:41:15 作者:

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

避免
JsonSerializer
System.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/finally
using
清理资源(比如取消
Timer
、释放数据库连接等)。

真实项目里,SSE 很少裸写循环;多数会结合

IAsyncEnumerable<t></t>
+
ChannelReader
IObservable
做解耦,但底层响应流的写法和头设置逻辑完全一致。最容易被忽略的,其实是
FlushAsync
的调用时机和
RequestAborted
的全程穿透 —— 这两点一漏,就变成“看似能发,实则收不到”或者“服务端悄悄堆积数百个僵尸任务”。

相关推荐