SseEmitter必须设置超时并手动管理生命周期,否则连接异常断开后会永久滞留内存导致泄漏;需构造时传入超时值、注册onCompletion/onTimeout/onError回调清理资源,并每次send后调用FlushAsync。

ASP.NET Core中SseEmitter必须配超时且需手动管理生命周期
不设超时的 SseEmitter 在连接异常断开后会永久滞留在内存里,导致连接数泄漏、GC压力上升甚至OOM。框架不会自动回收未完成的 emitter,必须显式设置超时并注册回调清理。
-
SseEmitter构造时传入超时毫秒值(如new SseEmitter(30 * 60 * 1000L)),超过该时间未发送数据则自动触发onTimeout - 必须为每个 emitter 注册
onCompletion、onTimeout、onError三个回调,在其中从连接池(如ConcurrentDictionary<long, List<SseEmitter>>)中移除自身 - 不要依赖
Dispose或终结器——SseEmitter的生命周期由 HTTP 请求上下文控制,回调才是唯一可靠出口
跨域场景下SSE必须禁用Credentials或显式指定Origin
前端用 EventSource 发起请求时若携带 cookie(withCredentials: true),服务端返回的 Access-Control-Allow-Origin: "*" 会被浏览器直接拒绝,连接静默失败,控制台无错误提示。
- 若需凭证,
services.AddCors()中必须用具体域名(如"https://myapp.com"),且启用SupportsCredentials = true - 若用通配符
"*",则前端必须设withCredentials: false,且不能发 cookie - 务必检查响应头是否真包含
Access-Control-Allow-Credentials: true和匹配的Access-Control-Allow-Origin,可用curl -i http://localhost:5000/api/notify/stream验证
每次写入后必须调用FlushAsync,否则前端永远收不到
ASP.NET Core 默认启用响应缓冲,SseEmitter.send() 写入的是内存缓冲区,不 FlushAsync 就不会真正发到 TCP 层。现象是连接建立成功、状态码 200,但前端 onmessage 一个事件也不触发。
- 所有
SendAsync调用后必须紧跟await _response.Body.FlushAsync() - 心跳事件(
event: heartbeat\ndata: ping\n\n)也得 flush,它是维持连接的关键 - 若使用
StreamWriter包装Response.Body,记得构造时传leaveOpen: true并设AutoFlush = true,否则仍需手动 flush
重连机制依赖Last-Event-ID,服务端必须解析并补发历史事件
浏览器在断线重连时会自动带上 Last-Event-ID 请求头,但 ASP.NET Core 不解析它——你得手动从 HttpContext.Request.Headers["Last-Event-ID"] 读取,并据此查询数据库或缓存,把断连期间漏掉的事件补推一遍。
- 前端首次连接不带该 header,后续重连才带;服务端要兼容 null/空字符串/非法格式三种情况
- 补发逻辑应限制条数(如最多 100 条),避免长连接刚建就刷屏阻塞
- 每条事件必须带
id:字段(如id: 12345\n),否则浏览器无法更新lastEventId,下次重连又从头开始
实际部署时最常被忽略的是 Nginx 或 IIS 的缓冲与超时配置:Nginx 默认 proxy_buffering on 且 proxy_read_timeout 60,IIS 有内建响应缓冲,这些都会吃掉你的 data: 行,必须显式关掉。


















