简介:本文系统讲解SSE协议的定义、原理与前端流式处理实践,帮助开发者理解其为何适合AI应用场景,掌握EventSource的使用方式、协议细节与工程注意事项,并厘清其与轮询、WebSocket等方案的差异与选型边界。
在传统 HTTP 请求模型中,客户端发送请求,服务器处理完毕后一次性返回完整响应,连接随即关闭。这种模式在大多数业务场景中运行良好,但当响应内容需要长时间计算、持续生成时,用户就必须面对一段”无反馈的等待期”。
Server-Sent Events(简称 SSE)正是为解决这类问题而设计的一种基于 HTTP 的服务器推送技术。它的核心定义可以概括为:
SSE 是一种允许服务器通过单一、长生命周期的 HTTP 连接,以文本流的形式持续向客户端单向推送数据的 Web 技术。
它由浏览器端的 EventSource API 和一套简洁的文本流协议共同组成。服务器通过在响应头中声明 Content-Type: text/event-stream,告知客户端”这不是一个普通响应,而是一个持续的数据流”。此后连接保持打开,服务器可以随时向客户端追加数据片段,客户端则在数据到达时即时触发事件回调。
从技术视角看,SSE 是 HTTP 协议之上的轻量级推送约定;从使用视角看,它是”让页面实时接收服务器消息”的一种低成本方案;从业务视角看,它是 AI 流式输出、进度反馈、实时通知等场景的常见底座。
在 SSE 出现之前,实现”服务器主动通知客户端”主要有几种方式,各有代价:
Upgrade: websocket),服务端、网关、负载均衡都要额外支持,接入与运维成本更高。然而,大量真实业务并不需要双向通信。以 AI 对话为例,数据流向几乎是纯粹的单向:服务器持续生成 token,客户端只负责接收和展示。客户端偶尔发送的”提问”本身用一次普通 POST 请求就能完成,无需为此维持一条全双工通道。
在这种”服务器推、客户端收”为主的场景中,SSE 的定位非常清晰:用标准 HTTP 完成单向实时推送,以最小的心智负担和部署成本换取足够的实时能力。
SSE 的协议层非常朴素——它本质上是一个 Content-Type: text/event-stream 的 HTTP 响应,body 按特定格式持续输出。典型的数据帧格式如下:
data: 你好data: 这是第二段内容event: donedata: [DONE]
关键规则包括:
data::消息内容,可跨多行,每行以 data: 开头,客户端会把它们用换行拼接;event::自定义事件类型,客户端可用 addEventListener 监听;id::消息 ID,用于断线重连后通过 Last-Event-ID 请求头告知服务器”我从哪里继续”;retry::建议的重连间隔(毫秒);这套格式设计得极为克制:纯文本、人类可读、易于在服务端拼接生成,也非常适合流式追加。
浏览器端的接入成本极低:
const es = new EventSource('/api/stream');es.onmessage = (event) => {// 每收到一个 data 帧就触发一次console.log('收到片段:', event.data);};es.addEventListener('done', () => {es.close(); // 主动关闭连接});es.onerror = (err) => {// 连接异常,浏览器会自动尝试重连console.warn('连接异常', err);};
EventSource 的几个行为值得注意:
retry 指定的间隔自动重连,开发者无需手写重试逻辑;readyState 提供 CONNECTING / OPEN / CLOSED 三种状态,便于 UI 展示;EventSource 只能发起 GET 请求,无法自定义请求头或 body,这是它的一个重要限制。SSE 只负责把数据片段送达,如何呈现则取决于前端。AI 流式输出的典型处理流程是:
EventSource(或使用 fetch + ReadableStream 手动解析流);requestAnimationFrame 或短时间窗口合并批量更新;[DONE])或连接关闭后,停止接收并收尾(如启用”重新生成”按钮);close(),网络异常则依赖自动重连或降级为普通请求。如果用 fetch 手动消费流,示意逻辑如下:
const res = await fetch('/api/chat', { method: 'POST', body });const reader = res.body.getReader();const decoder = new TextDecoder();while (true) {const { value, done } = await reader.read();if (done) break;const chunk = decoder.decode(value, { stream: true });// 按 \n\n 切分事件帧,逐帧解析 data:renderChunk(chunk);}
相比 EventSource,fetch 方式支持 POST、自定义头,更适配需要携带对话上下文的请求,但需要自行实现分帧解析与重连逻辑。
retry 与 Last-Event-ID,客户端容错逻辑大幅简化;EventSource 是浏览器标准 API,无需引入额外依赖;| 维度 | 短轮询 | 长轮询 | SSE | WebSocket |
|---|---|---|---|---|
| 通信方向 | 单向(请求-响应) | 单向 | 服务器→客户端单向 | 全双工 |
| 协议基础 | HTTP | HTTP | HTTP | 独立协议,需升级 |
| 实时性 | 低 | 中 | 高 | 高 |
| 服务端复杂度 | 低 | 中高 | 低 | 中高 |
| 自动重连 | 无 | 无 | 内置 | 需自行实现 |
| 二进制支持 | 是 | 是 | 否(文本) | 是 |
选型的基本判断是:需要客户端高频、低延迟地双向交互,选 WebSocket;只需服务器向客户端单向持续推送文本,选 SSE;两者都不需要,普通请求即可。
: 开头)保活;EventSource 无法携带自定义头,鉴权通常依赖 Cookie、URL 参数,或改用 fetch 流式方案;SSE 是一种以标准 HTTP 为基础、以 text/event-stream 为载体的单向服务器推送技术。它用极简的协议格式和浏览器原生的 EventSource API,把”服务器持续向客户端推文本”这件事变得轻量、可靠且易于部署。在 AI 流式输出这类以服务器推送为主的场景中,SSE 以简洁的实现提供了高效、可靠的解决方案;而当前端需要 POST 语义、自定义头或更精细的流控制时,基于 fetch 与 ReadableStream 的手动流式处理则是自然补充。
理解 SSE 的关键,不在于记住它的 API 细节,而在于认清它的边界:它是单向的、文本的、基于 HTTP 的实时推送通道——只要场景落在这一边界内,它往往就是成本最低、维护最省心的选择。