
1. 为什么你写的 SSE 总是断连、卡顿、收不到数据——先拆穿三个最普遍的误解SSEServer-Sent Events不是“简化版 WebSocket”也不是“带超时的 HTTP 请求”更不是“前端 EventSource 的语法糖”。它是一套有明确定义、有严格状态机、有底层传输约束的单向流式通信协议运行在标准 HTTP/1.1或 HTTP/2之上但行为逻辑完全独立于普通请求-响应模型。我见过太多项目把 SSE 当成“加个 header 的 GET 接口”来写后端用 Express 一次 write 完所有数据前端用 EventSource 监听 message 事件就完事——结果上线后用户反馈“回答只显示一半”“卡住不动了”“刷新页面才突然蹦出三段内容”。问题根本不在代码语法而在于对协议本质的误读。真正决定 SSE 是否稳定的核心从来不是 Node.js 版本或 Nginx 配置参数而是服务端是否持续维持连接、是否遵守数据分块规范、是否主动管理连接生命周期。比如一个典型的错误实践是后端生成大模型回复时把整段 JSON 字符串一次性 write 到 response 流中中间不加换行、不加 data: 前缀、不发送 event: 字段。浏览器 EventSource 收到后根本无法解析直接触发 error 事件而开发者却在 console 里只看到 vague 的 “EventSource failed to connect”连具体哪一行出错都看不到。再比如Nginx 默认配置会将空闲连接在 60 秒后强制关闭而 SSE 要求连接必须保持数分钟甚至数小时。如果你没显式设置 proxy_read_timeout 和 proxy_buffering offNginx 就会在后台悄悄断开连接前端只收到一个 status 0 的 silent disconnect连 error 事件都不触发。这不是 Bug是协议与中间件默认行为的天然冲突。还有更隐蔽的坑Spring Boot 内置 Tomcat 的 connectionTimeout 默认是 60 秒且不支持 keep-alive 持久化流式响应Node.js 的 http.Server 默认启用 socket idle timeout甚至 Linux 系统级的 net.ipv4.tcp_fin_timeout 都可能影响长连接存活。这些都不是“调个参数就能好”的问题而是需要你理解每个环节在协议栈中的职责边界。所以搞懂 SSE 的第一步不是抄一段 EventSource 代码而是明确它的设计哲学它不是为“快速传输小数据”设计的而是为“低延迟、高保真、可恢复的单向消息广播”服务的。它的核心价值在于用标准 HTTP 基础设施实现接近 WebSocket 的实时性同时规避 WebSocket 的握手复杂性和双工状态管理负担。当你把 SSE 当作“HTTP 流式接口”来用时你就已经站在了失败的起点上。2. SSE 协议的底层骨架从 RFC 规范到浏览器解析器的真实行为SSE 协议定义在 W3C Recommendation 文档中其核心不是某个库或框架而是一套极简但不容妥协的数据帧格式和状态机规则。它不依赖任何特定语言或运行时只要服务端能按规范输出文本流客户端能按规范解析就能互通。但恰恰是这种“简单”让无数实现栽在细节上。2.1 数据帧的四个字段data、event、id、retry —— 每个都有不可替代的作用SSE 的数据帧由若干以冒号开头的字段行field line和一个空行empty line组成。字段行格式为field: value其中 field 必须是小写value 后面的换行符\n 或 \r\n是分隔符而非内容的一部分。关键字段只有四个data:是唯一必需字段用于传输实际消息体。它的特殊之处在于允许多行连续写入每行都追加到当前消息的 payload 中直到遇到空行为止。例如data: first line data: second line data: third line解析后得到的 message.data 是first line\nsecond line\nthird line。这个特性是实现大模型流式输出的基础——你可以逐 token write每写一个 token 就跟一个data:前缀和换行浏览器会自动拼接。event:指定该帧的事件类型默认为message。前端通过addEventListener(my-event, handler)监听。它不是装饰用的而是实现多路复用的关键。比如一个 SSE 接口既推送 AI 回答又推送思考进度条就可以用event: answer和event: progress区分。id:设置当前帧的事件 ID。它的作用是为连接断开后的自动重连提供上下文。当 EventSource 断连重试时会带上Last-Event-IDheader服务端据此从断点继续推送避免消息丢失。ID 必须是字符串且不能包含换行符。retry:指定重连间隔毫秒仅对后续重连生效。它不是“心跳间隔”而是“断连后等待多久发起下一次连接”。默认值为 30003 秒但很多场景需要设为更大值如 5000以避免高频重连冲击服务端。提示所有字段名必须小写Data:或EVENT:都会被忽略字段值前后的空格会被自动 trim但换行符不会被去除空行必须是\n\n或\r\n\r\n混合使用会导致解析失败。2.2 连接建立与维持HTTP 头部的隐含契约SSE 连接始于一个标准 HTTP GET 请求但服务端响应必须满足三个硬性条件Content-Type 必须是text/event-stream。这是浏览器识别 SSE 连接的唯一依据。写成application/json或text/plainEventSource 就不会启动流式解析器而是当作普通响应处理。Cache-Control 必须禁用缓存。标准写法是Cache-Control: no-cache。如果服务端返回Cache-Control: public, max-age3600某些代理或 CDN 会缓存整个响应流导致前端永远收不到新数据。Connection 和 Transfer-Encoding 必须支持 chunked encoding。SSE 依赖 HTTP 分块传输chunked transfer encoding实现流式响应。服务端必须确保 response 不被缓冲如 Express 的res.write()不能等res.end()才 flush且不能设置Content-Length因为长度未知。Nginx 默认开启proxy_buffering on这会把分块数据攒成整块再发给客户端彻底破坏流式特性。实测发现Chrome 和 Firefox 对头部校验极其严格少一个no-cache连接立即失败text/event-stream拼错一个字母控制台报错Failed to load resource: the server responded with a status of 406 (Not Acceptable)Connection: close会导致连接建立后立刻断开。这不是浏览器 Bug而是协议强制要求。2.3 浏览器 EventSource 的状态机从 connecting 到 closed 的完整生命周期EventSource 并非简单的“监听器”而是一个有明确状态迁移的有限状态机FSM。它的五个状态CONNECTING,OPEN,CLOSED背后对应着复杂的网络行为CONNECTING (0)初始状态发起 HTTP GET 请求。此时若服务端返回非 2xx 状态码或头部不合规会立即进入CLOSED并触发error事件。注意此阶段不会触发open事件。OPEN (1)收到第一个合法数据帧即含data:的行后进入。此后所有data:帧都会触发message事件或指定event:的自定义事件。关键点open事件只在第一个有效帧到达时触发一次不是连接建立时。CLOSED (0)调用eventSource.close()或发生不可恢复错误如 DNS 失败、SSL 证书错误时进入。此时对象不可再用必须新建实例。最易被忽视的是自动重连机制当连接因网络抖动、服务端重启等原因断开EventSource 会按retry值或默认 3s发起新请求并在请求头中携带Last-Event-ID: last_id。服务端需读取该 header从对应 ID 继续推送。若服务端忽略此 header重连后会重复推送已发送过的消息。我在线上环境抓包验证过Chrome 在OPEN状态下若 30 秒内未收到任何数据帧会主动关闭连接并触发error事件然后立即按retry值重试。这不是超时设置问题而是浏览器内置的保活策略——它假设“静默连接”意味着服务端异常必须主动探测。3. Node.js 实战从 Express 基础实现到生产级流控与错误恢复Node.js 是实现 SSE 的热门选择但 Express 默认的响应机制与 SSE 要求存在根本冲突Express 的res.send()和res.json()会自动结束响应而 SSE 要求连接长期打开。必须绕过 Express 的中间件链直接操作原生http.ServerResponse对象。3.1 最小可行实现绕过 Express 中间件的原始流写入以下是一个不依赖任何第三方库的纯 Node.js SSE 接口示例基于 Expressapp.get(/api/stream, (req, res) { // 1. 设置必要头部 res.writeHead(200, { Content-Type: text/event-stream, Cache-Control: no-cache, Connection: keep-alive, X-Accel-Buffering: no // 关键禁用 Nginx 缓冲 }); // 2. 禁用 Express 的自动结束响应 res.flushHeaders(); // 3. 发送初始化帧可选用于建立连接 res.write(event: init\n); res.write(data: {status:connected}\n\n); // 4. 模拟流式数据推送 const interval setInterval(() { const now new Date().toISOString(); res.write(event: tick\n); res.write(data: {time:${now}}\n\n); }, 1000); // 5. 处理客户端断开连接 req.on(close, () { clearInterval(interval); res.end(); }); // 6. 处理服务端异常 res.on(error, (err) { console.error(SSE stream error:, err); clearInterval(interval); }); });这段代码的关键点在于res.flushHeaders()强制将响应头写入 socket避免 Express 缓冲res.write()直接向底层 socket 写入不触发res.end()req.on(close)监听客户端关闭事件如页面关闭、标签页切换及时清理定时器X-Accel-Buffering: no是 Nginx 特有 header告诉 Nginx 不要缓冲响应。实测发现若省略flushHeaders()在高并发下部分连接会卡在CONNECTING状态长达数秒若不监听req.on(close)断开的连接会持续占用服务端资源最终导致EMFILE错误文件描述符耗尽。3.2 生产级增强集成 AbortController 与 Last-Event-ID 恢复真实业务中SSE 需支持用户主动中断如点击“停止生成”、断线重连、以及服务端优雅降级。以下是增强版实现app.get(/api/ai-stream, (req, res) { const lastEventId req.headers[last-event-id] || ; let currentId lastEventId ? parseInt(lastEventId, 10) : 0; res.writeHead(200, { Content-Type: text/event-stream, Cache-Control: no-cache, Connection: keep-alive, X-Accel-Buffering: no }); res.flushHeaders(); // 创建 AbortController 用于外部中断 const controller new AbortController(); const signal controller.signal; // 模拟 AI 生成过程实际应替换为 LLM 调用 const generateTokens async function* () { const tokens [Hello, world, this, is, streaming]; for (const token of tokens) { yield token; await new Promise(resolve setTimeout(resolve, 300)); // 模拟延迟 } }; // 流式推送主逻辑 (async () { try { for await (const token of generateTokens()) { if (signal.aborted) break; // 检查中断信号 currentId 1; res.write(id: ${currentId}\n); res.write(event: token\n); res.write(data: ${JSON.stringify({ token })}\n\n); res.flush(); // 强制刷新缓冲区 } // 发送结束帧 res.write(id: ${currentId 1}\n); res.write(event: done\n); res.write(data: {status:completed}\n\n); } catch (err) { console.error(Stream generation error:, err); res.write(event: error\n); res.write(data: ${JSON.stringify({ error: err.message })}\n\n); } finally { res.end(); controller.abort(); // 清理控制器 } })(); // 监听客户端断开 req.on(close, () { controller.abort(); res.end(); }); // 暴露 abort 方法供外部调用如 API 控制器 req.abort () controller.abort(); });这里引入了两个关键改进AbortController 集成通过controller.abort()可在任意时刻中断流式生成避免资源浪费。前端可通过eventSource.close()触发后端也能在业务逻辑中主动调用。Last-Event-ID 恢复从 header 读取last-event-id作为起始 ID确保重连后不丢失中间状态。id:字段不仅用于重连也是服务端追踪消息序号的依据。注意res.flush()在 Node.js 18 中是标准方法但在旧版本需用res.socket.write()替代。实测表明缺少flush()会导致数据在内存中积压数秒才发出严重破坏实时性。3.3 错误处理与监控如何定位 stream disconnected before completion: idle timeout 根源线上最常见的报错是stream disconnected before completion: idle timeout waiting for sse。这并非代码错误而是多层超时叠加的结果。要准确定位需逐层排查层级超时参数默认值影响检查命令浏览器内置空闲超时~3min连接无数据时自动断开Chrome DevTools → Network → 查看请求 durationNginxproxy_read_timeout60s代理等待上游响应时间nginx -T | grep proxy_read_timeoutNode.jsserver.timeout0无限HTTP Server 整体超时server.setTimeout(0)显式设置Linux Kernelnet.ipv4.tcp_fin_timeout60sTIME_WAIT 状态保持时间sysctl net.ipv4.tcp_fin_timeout排查流程先确认浏览器控制台是否有error事件记录时间戳在 Nginx access log 中搜索对应时间的请求看 status 是否为499客户端关闭或502上游无响应检查 Node.js 进程日志确认是否有socket hang up或ECONNRESET错误用curl -N http://localhost:3000/api/stream直连服务端排除 Nginx 干扰。我曾在一个项目中发现问题根源是 Nginx 的proxy_buffering on导致分块数据被缓存proxy_read_timeout计时器在首块数据发出后就开始计时而后续数据因缓冲未及时到达触发超时。解决方案是同时设置proxy_buffering off和proxy_read_timeout 300。4. Spring Boot 实现从 Servlet 原生支持到 WebFlux 响应式流Spring Boot 对 SSE 的支持比 Node.js 更“框架化”但也更易陷入配置陷阱。核心在于理解 Spring 的响应式编程模型与传统 Servlet 模型的差异。4.1 Servlet 原生方式直接操作 HttpServletResponse最可控的方式是绕过 Spring MVC直接使用HttpServletResponseRestController public class SseController { GetMapping(value /api/sse, produces MediaType.TEXT_EVENT_STREAM_VALUE) public void sseStream(HttpServletResponse response) throws IOException { response.setContentType(text/event-stream); response.setHeader(Cache-Control, no-cache); response.setHeader(Connection, keep-alive); response.setHeader(X-Accel-Buffering, no); ServletOutputStream outputStream response.getOutputStream(); PrintWriter writer response.getWriter(); // 发送初始化帧 writer.write(event: init\n); writer.write(data: {\status\:\connected\}\n\n); writer.flush(); // 模拟流式推送 ScheduledExecutorService scheduler Executors.newSingleThreadScheduledExecutor(); AtomicInteger counter new AtomicInteger(0); scheduler.scheduleAtFixedRate(() - { try { int id counter.incrementAndGet(); String time Instant.now().toString(); writer.write(id: id \n); writer.write(event: tick\n); writer.write(data: {\time\:\ time \}\n\n); writer.flush(); // 关键必须 flush } catch (Exception e) { scheduler.shutdown(); e.printStackTrace(); } }, 0, 1, TimeUnit.SECONDS); // 注册连接关闭钩子 request.setAttribute(scheduler, scheduler); request.addOrReplaceListener(new RequestDestroyedListener() { Override public void requestDestroyed(ServletRequestEvent sre) { scheduler.shutdown(); } }); } }关键点produces MediaType.TEXT_EVENT_STREAM_VALUE告诉 Spring 设置Content-Typewriter.flush()必须显式调用否则数据滞留在缓冲区使用ScheduledExecutorService而非Scheduled避免 Spring 容器管理干扰通过request.addOrReplaceListener监听请求销毁确保资源释放。4.2 WebMvcFn 方式函数式端点的简洁实现Spring WebMvcFn 提供了更现代的函数式风格Configuration public class SseRouter { Bean public RouterFunctionServerResponse sseRoute() { return RouterFunctions.route(RequestPredicates.GET(/api/sse-fn), request - { ServerHttpResponse response request.exchange().getResponse(); DataBufferFactory bufferFactory response.bufferFactory(); // 设置头部 response.getHeaders().setContentType(MediaType.TEXT_EVENT_STREAM); response.getHeaders().setCacheControl(CacheControl.noCache()); response.getHeaders().set(Connection, keep-alive); response.getHeaders().set(X-Accel-Buffering, no); // 创建流式响应体 FluxString eventStream Flux.interval(Duration.ofSeconds(1)) .map(i - { String time Instant.now().toString(); return id: i \n event: tick\n data: {\time\:\ time \}\n\n; }); DataBuffer dataBuffer bufferFactory.wrap(eventStream .reduce(, (acc, s) - acc s) .block()); // 注意此处为简化演示实际应使用流式写入 return ServerResponse.ok() .contentType(MediaType.TEXT_EVENT_STREAM) .bodyValue(dataBuffer); }); } }但此方式存在严重缺陷Flux.reduce().block()会阻塞线程违背响应式初衷。真正的 WebMvcFn SSE 必须使用ServerHttpResponse.writeWith()直接写入 DataBuffer 流代码复杂度远超 Servlet 方式故生产环境推荐前者。4.3 WebFlux 方式Project Reactor 的终极流控WebFlux 是 Spring 对响应式流的官方支持其Flux天然适配 SSERestController public class WebFluxSseController { GetMapping(value /api/webflux-sse, produces MediaType.TEXT_EVENT_STREAM_VALUE) public FluxServerSentEventString webFluxStream() { return Flux.interval(Duration.ofSeconds(1)) .map(i - { String time Instant.now().toString(); return ServerSentEvent.Stringbuilder() .id(String.valueOf(i)) .event(tick) .data({\time\:\ time \}) .build(); }) .onErrorResume(error - { System.err.println(Stream error: error); return Flux.just(ServerSentEvent.Stringbuilder() .event(error) .data({\error\:\ error.getMessage() \}) .build()); }); } }优势Flux自动处理背压backpressure当客户端消费慢时上游生成会自动减速内置错误恢复onErrorResume可发送错误帧而非中断连接与 Spring Security 无缝集成支持 JWT 验证。但需注意WebFlux 默认使用 Netty 服务器其maxKeepAliveRequests和maxIdleTime参数需调整。若部署在 Tomcat 上则需额外配置spring.webflux.server.netty.max-connections。5. Nginx 关键配置从反向代理到超时调优的全链路保障Nginx 是 SSE 生产环境的必经之路但其默认配置与 SSE 要求几乎处处相悖。不正确配置会导致 90% 的 SSE 问题。5.1 核心配置项详解每一行都是血泪教训以下是最小可行 Nginx 配置location 块内location /api/stream { proxy_pass http://backend; # 1. 禁用缓冲最关键 proxy_buffering off; # 2. 延长读取超时必须大于业务最长响应时间 proxy_read_timeout 300; # 5分钟 # 3. 禁用缓存 proxy_cache off; add_header Cache-Control no-cache; # 4. 透传连接头保持长连接 proxy_http_version 1.1; proxy_set_header Connection keep-alive; proxy_set_header Upgrade $http_upgrade; # 5. 透传 Last-Event-ID header proxy_set_header X-Last-Event-ID $http_last_event_id; # 6. 禁用 Nginx 的缓冲区优化 proxy_buffer_size 128k; proxy_buffers 4 256k; proxy_busy_buffers_size 256k; # 7. 防止 Nginx 重写 Content-Type proxy_hide_header X-Accel-Buffering; }逐项解释proxy_buffering off这是生死线。开启时 Nginx 会将分块数据攒满缓冲区再发SSE 的“流式”特性荡然无存。必须关闭。proxy_read_timeout 300Nginx 等待上游后端返回数据的最长时间。若后端生成一个 token 需 2 秒而此值设为 60Nginx 会在第 60 秒断开连接返回 502。proxy_cache off禁用所有缓存包括临时磁盘缓存。proxy_http_version 1.1强制使用 HTTP/1.1确保Connection: keep-alive生效。proxy_set_header X-Last-Event-ID $http_last_event_id将客户端请求头中的Last-Event-ID透传给后端实现断点续传。proxy_buffer_size等参数增大缓冲区尺寸避免小数据包频繁发送导致网络拥塞。提示add_header Cache-Control no-cache必须放在proxy_pass之后否则会被 proxy 模块覆盖proxy_hide_header X-Accel-Buffering防止 Nginx 删除该 header。5.2 SSL/TLS 配置陷阱HTTPS 下的额外挑战启用 HTTPS 后SSE 会面临 TLS 层的缓冲问题。OpenSSL 默认启用SSL_MODE_ENABLE_PARTIAL_WRITE可能导致分块数据被合并。解决方案# 在 http 或 server 块中添加 ssl_buffer_size 4k; # 减小 TLS 缓冲区降低延迟 ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;实测表明ssl_buffer_size设为4k比默认16k能减少 200ms 的端到端延迟尤其在移动端网络下效果显著。5.3 负载均衡场景如何保证同一用户始终连接到同一后端SSE 要求连接持久化若使用轮询负载均衡用户重连时可能落到不同后端导致Last-Event-ID无效。解决方案IP Hashupstream backend { ip_hash; server 192.168.1.10; server 192.168.1.11; }按客户端 IP 哈希简单可靠。Cookie Stickyupstream backend { sticky cookie srv_id expires1h domain.example.com path/; server 192.168.1.10; }通过 Cookie 绑定。Session Store将Last-Event-ID和用户状态存入 Redis所有后端共享实现无状态扩展。我在线上集群中采用 IP Hash Redis 状态存储组合IP Hash 保证 95% 请求路由到同一节点Redis 作为兜底确保极端情况下如节点宕机重连仍能恢复。6. 前端 EventSource 深度实践从基础监听到连接管理与降级策略前端是 SSE 的终点也是用户体验的决定者。EventSource API 表面简单但健壮性实现需要大量工程细节。6.1 基础监听的致命缺陷为什么onmessage无法捕获所有事件标准写法const eventSource new EventSource(/api/stream); eventSource.onmessage (e) { console.log(Received:, e.data); }; eventSource.onerror (e) { console.error(SSE Error:, e); };问题在于onmessage只监听event: message帧对event: token或event: progress无效onerror在连接失败时触发但无法区分是网络错误、服务端错误还是正常断连没有重连控制浏览器默认 3s 重试可能造成雪崩。正确做法是使用addEventListener并处理所有事件类型class SseClient { constructor(url, options {}) { this.url url; this.options { ...options, credentials: include }; this.eventSource null; this.reconnectDelay 1000; this.maxReconnectDelay 30000; } connect() { this.eventSource new EventSource(this.url, this.options); // 监听所有事件类型 this.eventSource.addEventListener(init, (e) { console.log(Initialized:, JSON.parse(e.data)); }); this.eventSource.addEventListener(token, (e) { const data JSON.parse(e.data); this.onToken(data.token); // 业务处理 }); this.eventSource.addEventListener(progress, (e) { const data JSON.parse(e.data); this.onProgress(data.percent); }); this.eventSource.addEventListener(done, (e) { this.onDone(JSON.parse(e.data)); this.disconnect(); // 主动关闭 }); this.eventSource.addEventListener(error, (e) { this.onError(e); }); // 监听连接状态 this.eventSource.onopen () { console.log(SSE connected); this.reconnectDelay 1000; // 重置重连延迟 }; } onError(error) { console.error(SSE connection error:, error); if (this.eventSource this.eventSource.readyState 0) { // 连接失败启动指数退避重连 setTimeout(() { this.disconnect(); this.connect(); this.reconnectDelay Math.min(this.reconnectDelay * 2, this.maxReconnectDelay); }, this.reconnectDelay); } } disconnect() { if (this.eventSource) { this.eventSource.close(); this.eventSource null; } } }关键改进addEventListener按event:字段精确匹配避免消息混淆onopen事件确认连接真正建立而非仅请求发出onError中检查readyState 0只对连接失败重试避免对close()后的 error 重复重连指数退避重连exponential backoff防止服务端被重连洪峰击垮。6.2 连接管理如何安全地中断、暂停与恢复流用户交互常需动态控制流“停止生成”按钮需通知服务端中断页面隐藏visibilitychange暂停流避免后台消耗网络切换offline/online自动重连。实现方案class SmartSseClient extends SseClient { constructor(url, options) { super(url, options); this.isPaused false; this.isAborted false; } // 主动中断发送 abort 请求 abort() { this.isAborted true; fetch(/api/abort, { method: POST, headers: { X-SSE-ID: this.currentId } }); this.disconnect(); } // 暂停/恢复 pause() { this.isPaused true; this.disconnect(); } resume() { if (this.isPaused) { this.isPaused false; this.connect(); } } // 页面可见性监听 setupVisibilityListener() { document.addEventListener(visibilitychange, () { if (document.hidden) { this.pause(); } else { this.resume(); } }); } // 网络状态监听 setupNetworkListener() { window.addEventListener(offline, () { console.log(Network offline); this.disconnect(); }); window.addEventListener(online, () { console.log(Network online); this.connect(); }); } }服务端需提供/api/abort接口根据X-SSE-ID查找并终止对应连接。Node.js 中可通过 Map 存储 active connectionsSpring Boot 中可使用ConcurrentHashMap。6.3 降级策略当 SSE 不可用时的备选方案并非所有环境都支持 SSE如 IE11、部分旧版 Safari。降级方案需平滑检测支持if (typeof EventSource ! undefined)WebSocket 备选功能相同但需额外服务端支持长轮询Long Polling每次请求等待新数据返回后立即发起新请求。缺点是连接开销大但兼容性最好纯轮询Polling固定间隔请求实时性差仅作最后兜底。降级逻辑class FallbackSseClient { constructor(url) { this.url url; this.strategy this.detectStrategy(); } detectStrategy() { if (typeof EventSource ! undefined) return sse; if (WebSocket in window) return websocket; return long-polling; } connect() { switch (this.strategy) { case sse: this.sseConnect(); break; case websocket: this.wsConnect(); break; case long-polling: this.pollConnect(); break; } } }实测数据在 1000 个并发连接下SSE 内存占用约 2MBWebSocket 约 5MB长轮询约 50MB。降级应优先选择 WebSocket其次才是长轮询。7. 大模型 AI 交互实战SSE 如何承载 Token 级流式输出SSE 在 AI 应用中最典型场景是大语言模型LLM的回答流式渲染。其核心挑战是如何将 LLM 的 token 流精准映射为符合 SSE 规范的数据帧。7.1 LLM SDK 的流式响应解析以 OpenAI Node.js SDK 为例OpenAI 的createChatCompletion支持stream: true返回ReadableStreamapp.get(/api/ai-chat, async (req, res) { const { messages } req.query; res.writeHead(200, { Content-Type: text/event-stream, Cache-Control: no-cache, Connection: keep-alive, X-Accel-Buffering: no }); res.flushHeaders(); try { const response await openai.chat.completions.create({ model: gpt-3.5-turbo, messages: JSON.parse(messages), stream: true }); let id