尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
SSE服务端推送实战:协议原理、流式输出与排障
1. 先把SSE说清楚它到底是什么能解决什么问题SSE全称 Server-Sent Events服务端发送事件。名字听起来有点学术但用一句大白话就能讲明白它就是让服务器主动往浏览器“推送”消息的一条单向通道。传统的网页交互是“你问一句我答一句”浏览器不发请求服务器就没法说话。SSE 打破了这个限制——连接一旦建立服务器随时可以往下灌数据浏览器只管接收和渲染。这个能力听起来和 WebSocket 很像但两者的定位完全不同。WebSocket 是双向的全双工通道客户端和服务端都能随时发消息适合聊天室、协同编辑、在线游戏这类高频双向交互的场景。SSE 是单向的只有服务端往客户端推客户端要发消息得另开一个普通 HTTP 请求。这个“残缺”恰恰是它的优势协议简单、基于纯 HTTP、浏览器原生支持自动重连、实现成本极低。我最初接触 SSE 是在做一个后台任务的进度展示需求。用户点一下“生成报表”后端要跑十几秒甚至几分钟中间有多个阶段。用轮询吧前端每隔一秒发一次请求接口日志被刷得密密麻麻大部分请求返回的都是“还没好”浪费资源还增加延迟。用 WebSocket 吧为了一个单向的进度推送引入一整套双向通信机制还要处理心跳、断线重连、连接鉴权代码量翻好几倍。后来换成 SSE服务端一个接口保持连接有进展就write一条事件出去前端一个EventSource接住代码加起来不到五十行跑得还特别稳。SSE 最擅长的事情就是“服务器有话说客户端只管听”这一类单向、持续、低频到中频的数据流。典型的场景包括大模型应用的流式输出这个后面会重点讲、任务进度推送、系统通知与告警、实时日志与监控面板、股票行情这类只读数据源、新闻或社交信息流的增量更新。如果你的需求里客户端几乎不需要主动说话那 SSE 基本就是性价比最高的选择。学习门槛方面SSE 对新手相当友好。前端只需要认识EventSource这一个对象后端只要会写普通的 HTTP 响应、会控制Content-Type和手动flush就能跑起来。但“能跑”和“跑得稳”之间隔着不少坑代理层缓冲导致消息延迟、连接数上限、断线重连时的事件重复、多标签页把连接数打满等等。这些才是真正决定项目成败的细节也是我下面要重点拆解的部分。这篇文章会从协议原理讲到落地实操从选型对比讲到线上排障。不管你是刚听说 SSE 想快速上手还是已经用过但被某些奇怪现象困扰应该都能从里面找到能直接抄作业的东西。2. 为什么选它SSE 的技术定位与选型逻辑2.1 三种实时方案到底该怎么选做实时推送市面上主要就三条路短轮询、WebSocket、SSE。很多人一上来就默认 WebSocket觉得它“最实时”“最先进”但实际项目里选型的关键从来不是技术先不先进而是通信模型匹不匹配业务模型。先说短轮询。客户端定时发请求问服务端有没有新数据实现最简单任何技术栈都能做。缺点是延迟取决于轮询间隔间隔短了请求量爆炸间隔长了实时性差而且大量请求返回空结果白白消耗连接和带宽。它适合数据变化频率很低、实时性要求不高的场景比如每五分钟查一次配置是否更新。再说 WebSocket。它在 TCP 之上做了一次 HTTP 握手升级之后就是全双工通信延迟极低适合高频双向交互。代价是协议复杂需要自己维护心跳、需要处理连接状态机、很多老版本的反向代理和网关对它支持不好、还要单独考虑鉴权和权限校验的位置。用一句话概括能力最强成本也最高。SSE 则站在中间偏“轻”的位置。它复用普通 HTTP 请求响应头里声明Content-Type: text/event-stream连接保持不关闭服务端持续写数据。浏览器端用EventSource接收自带断线重连和事件 ID 续传。它不需要协议升级穿透性好实现成本低调试方便用curl就能直接看流。我把三者的关键差异整理成了下面这张表选型时对照着看基本不会错维度短轮询SSEWebSocket通信方向客户端拉服务端单向推双向全双工协议基础HTTPHTTP独立协议需升级握手实现复杂度低低中高断线重连天然每次都是新请求浏览器自动重连需自行实现消息格式自定义文本固定格式文本/二进制代理与网关兼容性最好较好需关闭缓冲一般单连接开销低短连接中长连接占用中典型场景低频配置刷新流式输出、进度、通知聊天、协同、游戏2.2 选 SSE 的三个真实理由第一业务模型本来就是单向的。这是最核心的一条。如果你的数据流从服务端流向客户端客户端只是被动展示那么引入双向通道就是过度设计。流式生成内容、任务进度、系统通知、监控指标刷新全都属于这一类。我见过不少项目为了“以后可能要双向”而选了 WebSocket结果那个“以后”一直没来却要为双向通信的复杂度持续买单。第二需要借助 HTTP 的现有基础设施。SSE 走的是标准 HTTP意味着现有的登录态、Cookie、请求头、限流、审计、链路追踪全都能直接复用。WebSocket 的握手虽然也带 HTTP 头但升级之后的帧通信就脱离了常规 HTTP 处理链路很多中间件的能力用不上鉴权往往得在握手阶段做完后续无法再按请求粒度控制。SSE 因为是持续的一个 HTTP 响应鉴权逻辑放在普通的请求拦截里就行心智负担小很多。第三自动重连和事件续传是白送的。浏览器对EventSource内置了重连机制连接断了会按一定间隔自动重试。更关键的是如果服务端给每条消息带上id:字段浏览器重连时会自动在请求头里带上Last-Event-ID服务端据此可以只补发断线期间漏掉的消息。这套机制不需要写一行重连代码对做“不能丢消息”的场景非常友好。WebSocket 要实现同等能力得自己维护重连状态、消息序号、服务端补发逻辑工作量完全不是一个量级。2.3 什么时候不该用 SSE选型不只看优点更要看边界。有几种情况我会明确避开 SSE。一是客户端需要频繁、低延迟地向服务端发消息。SSE 的上行通道要靠额外的 HTTP 请求每一次都是一次完整请求高频场景下开销远大于 WebSocket 的一条双向连接。协同编辑、多人游戏、实时语音这类必须用 WebSocket。二是需要传输二进制数据。SSE 的消息体只能是 UTF-8 文本图片、音频、二进制协议帧都得做 Base64 编码体积膨胀 33% 以上得不偿失。这类需求要么走 WebSocket要么干脆用分片 HTTP 下载。三是单域名下并发连接极多。HTTP/1.1 时代浏览器对同一域名的并发连接数有限制通常 6 个左右每个 SSE 连接都会长期占用一个名额开多个标签页很容易把连接数打满导致后续普通请求排队。这个坑在 HTTP/2 下会缓解很多因为多路复用让连接数限制不再是瓶颈但如果你的服务端是 HTTP/1.1 且用户习惯开一堆标签页就得提前规划分域或者做连接复用。后面排障章节我会专门讲这个问题的处理办法。3. 协议层拆开看SSE 的消息格式与运行机制3.1 一条 SSE 消息长什么样SSE 的报文格式非常朴素就是若干行“字段名: 字段值”以空行作为一条消息的结束。它只定义了四个字段记住它们就掌握了协议的全部id: 1001 event: progress retry: 3000 data: {stage:parsing,percent:35} data: 第二行数据 data: 第三行数据这几个字段的分工很明确。id是这条消息的编号浏览器会记住最后一个收到的 id重连时通过Last-Event-ID请求头带回去服务端可以据此续传。event是事件类型名前端可以用addEventListener(progress, ...)针对特定类型单独监听不写则默认是message类型。retry告诉浏览器断线后隔多少毫秒重连。data是真正的载荷一条消息可以有多行data浏览器会把它们用换行符拼接起来交给前端。这里有几个容易踩的细节必须提前讲清楚。第一行分隔符的兼容性。规范里允许\n、\r、\r\n三种换行但实际部署时最好统一用\n。我遇到过某个中间层把\r\n处理成了两个换行导致前端收到大量空消息。第二data里不能直接放未处理的换行。如果你的 JSON 内容经过格式化、带有换行必须拆成多行data:前缀否则前端解析会断掉。稳妥的做法是发送前先JSON.stringify保证载荷是单行。第三注释行以冒号开头。形如: keep-alive的行会被浏览器忽略这个特性常被用来做心跳保活避免中间代理因为长时间没数据而掐断连接。第四空行才是消息边界。只写了data: xxx但没跟一个空行浏览器不会认为消息结束会一直等。很多“前端收不到消息”的问题根源就是服务端少写了那个换行。3.2 连接建立到断开的完整生命周期一次典型的 SSE 交互会经历这么几个阶段。建立阶段前端new EventSource(/api/stream)浏览器发起一个普通的 GET 请求。服务端返回200响应头里必须包含Content-Type: text/event-stream同时建议带上Cache-Control: no-cache和Connection: keep-alive。这三个头缺一个都可能出问题类型不对浏览器不认缓存头缺失可能导致中间层缓存响应连接头影响长连接的维持。推送阶段连接建立后不关闭服务端持续往响应体里写数据并立即 flush。这里的关键是flush——数据写进缓冲区不等于发到了客户端必须显式刷新。不同语言和框架的刷新方式不同Node.js 里是res.flushHeaders()配合写入Java 的 Servlet 需要拿到OutputStream后flush()Python 的 Flask 要用生成器返回并且注意 WSGI 的缓冲行为。这一块是实践中最容易出问题的地方后面实操部分会给出具体写法。断线阶段网络抖动、服务重启、代理超时、客户端主动close()都会导致连接断开。如果是浏览器侧检测到非主动断开它会按retry指定的间隔默认约 3 秒自动重连并在请求头带上Last-Event-ID。续传阶段服务端读到Last-Event-ID从该 id 之后的消息开始补发前端就感知不到中间断过。要注意的是续传能力需要服务端自己实现浏览器只负责把 id 带回来补发逻辑得你来写。常见做法是把近期消息按 id 存在内存环形缓冲区或 Redis 里按需回放。3.3 关于 HTTP/2 与连接数限制的补充说明前面提过连接数的问题这里展开说一下因为它是决定 SSE 能不能规模落地的关键。在 HTTP/1.1 下浏览器对同一 origin 的并发连接数限制通常是 6。每个活跃的 SSE 连接会独占一个用户开三个标签页、每个页面订阅两条流连接数就打满了后续的普通接口请求只能排队等待。表现出来就是“页面卡住了”“其他请求一直 pending”。解决办法有三条按推荐程度排序。首选升级到 HTTP/2多路复用让所有流共享一条 TCP 连接连接数限制基本不再是问题这也是目前生产环境最主流的做法。其次是把 SSE 接口放到独立的子域名下比如stream.example.com这样它和主站不同 origin连接数配额分开计算。最后是在应用层做连接复用比如用SharedWorker或BroadcastChannel让多个标签页共享一条 SSE 连接由一个“主页面”接收后再分发给其他页面。这个方案最复杂但对多标签页场景最优雅。需要提醒的是HTTP/2 下也有个坑某些代理或网关对 HTTP/2 的流式响应做了缓冲会攒够一定大小才下发导致前端收到消息是“一批一批”来的而不是逐条。这个问题的排查方法我放在最后一章那里有具体的定位手段。4. 服务端怎么把流稳定地推出去4.1 最小可用实现与关键响应头先给一个能直接跑的最小例子用 Node.js 的原生http模块写不依赖任何框架方便看清本质const http require(http); const server http.createServer((req, res) { if (req.url ! /stream) { res.writeHead(404); return res.end(); } res.writeHead(200, { Content-Type: text/event-stream, Cache-Control: no-cache, no-transform, Connection: keep-alive, X-Accel-Buffering: no }); let seq 0; const timer setInterval(() { seq 1; res.write(id: ${seq}\n); res.write(event: tick\n); res.write(data: ${JSON.stringify({ seq, ts: Date.now() })}\n\n); }, 1000); req.on(close, () { clearInterval(timer); console.log(client disconnected); }); }); server.listen(3000);代码很短但每一行都有讲究。Content-Type必须是text/event-stream这是浏览器的识别依据。Cache-Control里的no-transform很关键它能阻止部分代理对响应做压缩或改写避免流被“攒起来”。X-Accel-Buffering: no是针对 Nginx 的告诉它不要缓冲这个响应这行如果漏了在 Nginx 后面部署时会表现为消息延迟几十秒才到。req.on(close)里的清理同样重要。客户端断开后如果定时器没被清掉会持续往一个已关闭的连接写数据轻则报错刷日志重则内存泄漏。生产环境里每个连接都必须绑定完整的清理逻辑包括定时器、数据库游标、订阅关系。4.2 用框架封装把重复逻辑抽干净实际项目里不会手写http模块通常会用 Express、Koa、Spring、Flask 这类框架。不同框架的流式写法差异不小我把常见几种的要点整理如下。技术栈关键写法注意事项Node.js Expressres.setHeader后直接res.write不要用res.json会结束响应Node.js Koactx.respond false接管原生 res否则 Koa 会自行结束响应Java Spring MVC返回SseEmitter需配置异步超时时间否则到点自动断Java WebFlux返回FluxServerSentEvent背压处理更自然推荐新项目使用Python Flask生成器 Response(stream_with_context(...))必须用流式响应包装注意多线程模型Python FastAPI返回StreamingResponse异步生成器需注意不要阻塞事件循环Go 标准库http.Flusher手动Flush()每次写完必须 Flush否则不生效我踩过最深的一个坑在 Koa 上。Koa 默认会在中间件执行完后自动结束响应如果你只是ctx.res.write而没有设置ctx.respond false框架会在某个时刻把连接关掉表现为前端收到几条消息后突然断流。这个问题的报错信息很不直观当时排查了大半天才定位到。另一个高频问题是Spring 的SseEmitter超时。它默认的超时时间不同版本不一样常见是 30 秒一到就会触发完成回调连接被关闭。很多人以为是网络问题其实是超时配置没调。解决方式是构造时显式传入一个足够大的超时值或者设为 0 表示不超时同时在onCompletion和onTimeout回调里做好资源清理。4.3 心跳、背压与资源回收长连接最怕两件事被中间层误杀和慢消费者拖垮服务端。先说心跳。很多代理和负载均衡器有空闲超时设置比如 60 秒内没有数据传输就断开连接。SSE 的场景下如果业务消息本来就稀疏比如告警推送可能几小时才一条连接会被静默掐断。解决办法是定期发送注释行作为心跳const heartbeat setInterval(() { res.write(: ping\n\n); }, 15000);注意心跳间隔要小于链路上最短的那个空闲超时。我一般设 15 秒既能保活又不会产生太多无效流量。心跳用注释行而不是真实事件是为了不干扰前端的业务逻辑。再说背压。如果服务端生产消息的速度超过客户端消费的速度数据会在服务端缓冲区堆积内存持续上涨。Node.js 里可以通过res.write()的返回值判断返回false表示缓冲区已满此时应该暂停生产、等待drain事件。对于推送频率高的场景比如行情数据这个处理必须做否则一个慢客户端就能把整个进程的内存吃光。资源回收方面除了前面说的清理定时器还要注意订阅关系要解绑比如从消息总线取消订阅、数据库游标要关闭、用户会话状态要清理。我习惯把每个连接的所有资源句柄收在一个对象里在close回调里统一释放这样不容易漏。提示长连接服务上线前务必在真实代理链路上做一次“挂机测试”——建立连接后不做任何操作放置 10 分钟以上观察是否被断开。这一步能提前暴露绝大多数保活配置问题。5. 前端接入EventSource 的用法与边界5.1 基础用法与事件监听前端的接入成本极低核心就一个对象const es new EventSource(/api/stream); es.onmessage (e) { console.log(默认消息, e.data); }; es.addEventListener(progress, (e) { const payload JSON.parse(e.data); updateProgressBar(payload.percent); }); es.onerror (err) { console.warn(连接异常浏览器将自动重连, es.readyState); };EventSource有三个状态CONNECTING0、OPEN1、CLOSED2。onerror触发时如果状态还是CONNECTING说明浏览器正在自动重连此时不要手动 close 再重建那会打断浏览器的重连节奏。只有当状态变成CLOSED比如服务端返回了非 200 状态码或者你主动调用了close()才需要人工介入。这里有个很隐蔽的坑服务端返回 204 会导致连接永久关闭且不再重连。规范里 204 表示“没有更多数据了”浏览器收到后会停止重连。有些开发者想用 204 表示“任务已完成”结果前端再也收不到后续消息。如果确实要通知结束正确做法是发一条普通事件由前端主动close()。还有一个坑是401/403 这类鉴权失败。浏览器遇到非 200 的响应会关闭连接并停止重连不同浏览器行为略有差异但普遍不会无限重试。所以如果 SSE 接口需要登录态一定要确保 Cookie 或 token 有效或者改用带凭据的 URL 参数方案并在前端处理鉴权失败后的跳转逻辑。5.2 原生 EventSource 的两个硬限制用久了会发现原生EventSource有两个绕不过去的限制。第一不支持自定义请求头。这意味着你没法在请求头里塞Authorization: Bearer xxx。如果鉴权靠 Cookie那没问题同源请求会自动带上。但如果你的系统用的是 token 放在 header 里的方案原生EventSource就无能为力了。常见的变通办法是把 token 放在 URL 查询参数里注意日志脱敏或者改用基于fetch的流式读取方案。第二只支持 GET 请求。如果推送的内容依赖复杂的查询参数只能全部塞进 URL长度受限且不优雅。这同样是EventSource的协议约束。如果这两个限制影响了业务方案是用fetch手动实现 SSE 的客户端。fetch支持自定义头、支持 POST、支持AbortController取消配合ReadableStream逐块读取响应体再按 SSE 格式自己解析即可。社区里有不少成熟的封装库但核心逻辑不复杂我建议至少自己写一遍理解其中的分块解析和粘包处理async function connectSSE(url, token, onEvent) { const controller new AbortController(); const resp await fetch(url, { headers: { Authorization: Bearer ${token}, Accept: text/event-stream }, signal: controller.signal }); const reader resp.body.getReader(); const decoder new TextDecoder(); let buffer ; while (true) { const { done, value } await reader.read(); if (done) break; buffer decoder.decode(value, { stream: true }); let idx; while ((idx buffer.indexOf(\n\n)) ! -1) { const raw buffer.slice(0, idx); buffer buffer.slice(idx 2); onEvent(parseEvent(raw)); } } return controller; }这段代码的关键在buffer的处理。网络传输是按块来的一个 SSE 消息可能被拆到两个 chunk 里也可能两个消息挤在一个 chunk 里。所以必须维护缓冲区按\n\n切分完整的消息剩下的留到下一轮。这是所有流式解析的通用套路处理不当就会出现“偶尔丢消息”或“JSON 解析失败”的诡异问题而且往往在高并发或网络抖动时才暴露极难复现。5.3 页面生命周期与连接管理前端还有一个容易被忽视的点页面卸载时连接会不会自动关闭。原生EventSource在页面关闭时会由浏览器自动断开一般不需要手动处理。但在单页应用里路由切换并不会卸载页面组件销毁时如果没调close()连接就会一直挂着。组件反复创建销毁连接数会越积越多。我的习惯是在组件的卸载钩子里显式关闭onUnmounted(() { es.close(); });同时要注意避免重复创建。有些代码在onerror里直接new EventSource重建而浏览器的自动重连也在同时进行结果就是连接数翻倍增长。正确做法是依赖浏览器的自动重连只在状态为CLOSED时才考虑手动重建且重建前先确保旧实例已经关闭。6. 大模型流式输出SSE 目前最热的落地场景6.1 为什么流式输出几乎都选 SSE这两年 SSE 重新火起来很大程度上是因为大模型应用的普及。文本生成类接口的典型特征是服务端持续产出 token客户端逐字展示客户端全程不需要往这条连接上发任何东西。这个模型和 SSE 的定位严丝合缝。如果换成 WebSocket你要处理握手升级、心跳、连接状态管理还要考虑网关对 WebSocket 的支持情况。而用 SSE一个普通的 POST 请求就能搞定很多实现是 POST 发起生成、响应体直接流式返回前端拿到流后逐块解析天然适配“打字机效果”。更重要的是大模型生成往往要几十秒SSE 的长连接 自动重连机制能很好地覆盖网络波动。还有一个实际考量很多部署环境的网关和防火墙对 WebSocket 的支持并不可靠尤其是企业内网。SSE 因为就是普通 HTTP穿透性明显更好出问题的概率低得多。6.2 流式响应的实现要点大模型流式输出的服务端实现核心是把上游模型服务的流原样转发给下游浏览器。用 Node.js 写大致是这样app.post(/api/chat, async (req, res) { res.writeHead(200, { Content-Type: text/event-stream, Cache-Control: no-cache, no-transform, Connection: keep-alive, X-Accel-Buffering: no }); const upstream await callModelStream(req.body); for await (const chunk of upstream) { const text chunk.text || ; if (text) { res.write(data: ${JSON.stringify({ delta: text })}\n\n); } } res.write(event: done\ndata: {}\n\n); res.end(); });这里有几个实战要点。第一务必逐块转发不要攒。有些人为了“减少写入次数”把多个 chunk 合并后再发结果打字机效果变成“一段一段蹦”体验很差。流式的价值就在于即时性。第二异常要显式下发。上游模型服务可能中途报错此时不能直接res.end()前端会以为正常结束。正确做法是先发一条event: error的消息说明原因再结束响应。前端据此展示错误提示而不是干等着。第三客户端断开要及时中止上游。用户点了“停止生成”或者直接关了页面服务端如果不感知会继续消耗模型资源直到生成完毕。处理方式是在req.on(close)里中止上游请求多数 SDK 支持AbortController或类似的取消机制。这一条对成本控制很重要尤其是按量计费的服务。第四注意data的特殊字符。生成的内容里可能有换行、可能有特殊符号一定要JSON.stringify后再发否则换行会把一条消息拆成多条前端解析出错。6.3 前端渲染的性能细节流式输出在前端看起来简单——收到就追加但高频更新下的性能问题很容易被忽略。如果每个 token 都触发一次状态更新和 DOM 重渲染在生成速度快的时候会造成明显卡顿尤其是在移动端。我的做法是做一层节流合并收到的增量先存进缓冲区用requestAnimationFrame或固定 50 毫秒的定时器批量提交给视图层。这样既保留了打字机的视觉效果又把渲染次数降了一个数量级。实测在长文本生成场景下CPU 占用能降一半以上。另外要注意自动滚动的策略。新内容追加时如果无脑滚到底部用户想往回翻看历史内容就会被强行拉回。常见的处理是判断用户当前是否在底部附近在底部才自动跟随一旦用户手动上滑就暂停自动滚动等用户回到底部再恢复。这个细节虽小但直接决定阅读体验。7. 排障实录那些让人抓狂的典型问题7.1 消息延迟九成是代理缓冲惹的祸现象本地开发一切正常部署到测试环境后前端要等几十秒才收到一批消息然后一次性全出来。这个问题的原因几乎总是中间层缓冲。Nginx 默认会对响应做缓冲攒够一定大小或等到响应结束才下发。SSE 是持续输出、永不结束的响应正好撞在枪口上。解决要分几层一起看。Nginx 侧需要关闭缓冲location /api/stream { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Connection ; proxy_buffering off; proxy_cache off; chunked_transfer_encoding off; proxy_read_timeout 3600s; }proxy_buffering off是核心proxy_read_timeout也要调大默认 60 秒会把长连接掐断。应用侧则要加X-Accel-Buffering: no响应头双保险。如果前面还有 CDN 或云负载均衡也要确认它们对text/event-stream不做缓冲有些厂商需要单独提工单开启流式支持。排查手段很简单绕过代理直连后端。用curl -N http://后端地址/stream-N关闭 curl 自身的缓冲如果直连是逐条到达、经过代理是批量到达那基本就锁定是代理问题。7.2 连接反复断开超时配置的连环坑现象连接建立后大约 30 秒或 60 秒就断一次然后自动重连日志里频繁出现连接建立和断开。这类问题要沿着链路逐层排查超时设置任何一层的超时都可能成为短板环节常见默认值调整建议Nginxproxy_read_timeout60s调到 3600s 或更长负载均衡空闲超时60s~900s按业务调整配合心跳SpringSseEmitter超时30s显式设置或设 0 不超时Node.js 服务端超时视框架而定确认server.timeout为 0客户端网络设备不确定靠心跳保活兜底即便所有超时都调大了我仍然建议保留心跳。因为链路上可能存在你无法配置的设备比如用户的家庭路由器、公司的安全网关心跳是唯一可靠的保活手段。7.3 断线重连后消息重复或丢失现象网络抖动后重连前端发现有些消息重复出现或者中间少了几条。根因是续传逻辑没做对。浏览器重连时会把最后收到的 id 放在Last-Event-ID头里但服务端如果没读这个头、没做补发就只能从当前最新位置继续推中间的消息就丢了。反过来如果服务端补发时把 id 等于Last-Event-ID的那条也重发了一遍前端就会看到重复。正确的语义是补发 id 大于Last-Event-ID的所有消息。服务端实现时要注意严格比较别把边界搞错。另外如果消息缓冲区有容量上限比如只保留最近 1000 条超出范围的补发请求要明确告知前端“历史已过期”让前端走全量刷新而不是静默漏数据。前端侧也要做幂等处理。我的习惯是给每条消息一个业务侧的唯一序号收到重复的直接丢弃。这样即使服务端补发逻辑有瑕疵也不会造成展示错乱。7.4 常见问题速查表把上面这些零散的经验整理成一张表出问题时按图索骥能省不少时间现象最可能的原因快速验证处理方向消息批量到达代理缓冲curl 直连对比关闭 proxy_buffering加 no-transform定时断开某层超时观察断开间隔是否固定逐层调大超时 心跳前端收不到任何消息响应头错误或缺少空行浏览器网络面板看响应头检查 Content-Type 和消息结尾连接数打满、其他请求卡住HTTP/1.1 并发限制看同时打开的标签页数量上 HTTP/2 或分域名重连后数据重复补发边界错误打印 Last-Event-ID 和补发范围严格用大于比较前端加幂等服务端内存持续上涨慢消费者导致缓冲堆积监控连接数与内存曲线处理背压限制单连接缓冲服务重启后大面积断连长连接未做优雅关闭观察重启时的日志优雅停机通知客户端重连生成任务停不下来未监听 close 事件关闭页面看服务端是否继续跑在 close 回调中中止上游注意服务发布时的优雅停机经常被忽略。滚动更新会直接掐断所有长连接用户侧表现为“突然断了”。如果前端有自动重连倒还好但正在生成的内容就丢了。稳妥做法是在停机前先给客户端发一条“服务即将重启”的事件等待几秒再关闭连接。8. 我个人的几条实战心得做了几个基于 SSE 的项目后有几条体会是一直在用的。把连接当成稀缺资源来管理。长连接不是免费的每个连接都占着服务端的文件描述符和内存。上线前一定要估算峰值连接数据此调整系统的ulimit、连接池大小和内存配额。我习惯在监控里单独盯“活跃连接数”这个指标它的异常增长往往预示着前端有连接泄漏。心跳和超时是一对必须一起配。只调大超时不加心跳链路上任何一台你管不着的设备都可能悄悄掐断连接只加心跳不调超时服务端自己的超时仍会生效。两者配合才能覆盖完整链路。续传能力要么做对要么别做。半吊子的续传比不做更糟——用户以为消息不会丢实际上丢了还不知道。如果暂时没有可靠的缓冲区存储宁可让前端在重连后走一次全量刷新也不要给出“看起来能续传”的假象。优先考虑 HTTP/2。这一条能省掉一大半连接数相关的麻烦。如果你的服务还在 HTTP/1.1 上跑而 SSE 又要面向多标签页的重度用户升级的收益会非常明显。流式接口的日志要单独设计。普通接口打一条访问日志就够了流式接口则要记录连接的建立、心跳、消息条数、断开原因和持续时长。这些数据在排查“为什么用户说卡住了”时是唯一可靠的依据光看错误日志什么也发现不了。最后再分享一个排查小技巧遇到流式相关的疑难杂症先用curl -N -v直连后端看原始输出。如果 curl 逐条到达问题一定在代理或前端如果 curl 也是批量到达那问题在服务端方向一下就清晰了。这个动作花不了十秒钟却能省掉大量猜测。
RELATED

相关推荐

CLM:语言模型的原生上下文自我管理技术

CLM:语言模型的原生上下文自我管理技术

1. 这不是“更长的上下文”,而是语言模型第一次学会“自己整理书桌”你有没有试过让一个刚入职的实习生帮你整理一屋子散落的会议纪要、客户邮件、产品需求草稿和上周的聊天记录?他可能把所有材料堆进一个文件夹,标上“重要资料”&#xff0c…

📅 2026/10/26 5:49:24
DuckDB与MySQL在大数据分析场景下的性能对比

DuckDB与MySQL在大数据分析场景下的性能对比

1. 项目背景与测试目标最近在优化一个数据分析项目时,遇到了一个棘手的问题:当数据量膨胀到千万级甚至亿级时,传统的关系型数据库查询性能开始急剧下降。这让我开始关注新兴的嵌入式分析型数据库DuckDB,并决定与老牌选手MySQL进行…

📅 2026/10/26 5:49:24
Android智能汽车租赁系统开发实践

Android智能汽车租赁系统开发实践

1. 项目背景与需求分析在移动互联网高速发展的今天,传统汽车租赁行业正面临数字化转型的关键节点。作为一名深耕Android开发多年的技术从业者,我观察到市场上大多数汽车租赁App仍停留在简单的信息展示阶段,缺乏真正的"智能化"体验。…

📅 2026/10/26 5:49:24
MORE NEWS

更多资讯

📰

机器学习多元方法:从PCA到LDA的实战指南

1. 多元方法在机器学习中的核心地位多元方法是《机器学习导论》第5章的核心内容,也是从单变量分析向高维空间拓展的关键转折点。我在实际教学中发现,很多初学者在接触PCA降维或LDA分类时,常常困惑于"为什么要处理多维数据"。其实举…

📰

AI日报:多Agent协作、模型部署与行业应用实战拆解

2026年10月6日,AI圈的“信息流速”一点没降。今天的热搜词很有代表性:一边是大众向的AI应用持续霸榜,另一边是开发者生态和产业侧的话题密度明显在上涨——AI编程插件、多Agent协作、模型部署、硬件设计里的AI接口,全都挤进了同一…

📰

故障诊断中机理与数据融合:随机森林与热力学仿真的可解释性实践

故障诊断这个方向,做了几年之后你会发现一个很尴尬的局面:纯机理建模的人瞧不上数据驱动的方法,觉得那是"黑箱拟合",没有物理意义;而搞机器学习的人又觉得机理模型太理想化,工况一变就歇菜。我在…

📰

学术写作工具测评:8款提升论文效率的利器

1. 学术写作工具全景测评:从开题到毕业的智能解决方案去年指导本科生论文时,有个场景让我印象深刻:凌晨两点收到学生微信,附件里是第三版被导师打回的开题报告,文档上密密麻麻的批注触目惊心。这促使我开始系统研究市面…

📰

PLC控制绕线机系统:硬件选型、软件设计与故障排查指南

简介:《基于PLC控制的绕线机系统的设计与检修》完整版毕业设计文档,面向机电设备管理、电气自动化等专业学生及工业控制系统维护人员。文档以绕线机这一典型生产设备为例,系统讲解PLC控制系统从原理选型、硬件配置到程序设计的完整过程&#…

📰

H264裸流转RTMP:SPS/PPS、avcC与时间戳封装实践

简介:这份压缩包面向希望打通 H.264 到直播推流链路的中初级音视频开发者,聚焦如何借助 FFmpeg API 在内存中完成 H.264 NAL 单元到 FLV 封装的转换,并通过 RTMP 协议推送至流媒体服务器,可作为理解直播推流核心流程的轻量参考。包…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬