简介:SSE(Server-Sent Events)是基于HTTP协议的服务器单向推送技术,也是当前大模型应用实现逐字流式输出的主流方案。本文系统讲解SSE的定义、核心组成、消息格式与工作原理,结合大模型场景分析典型应用,并与轮询、长轮询、WebSocket进行对比,帮助读者一文建立对SSE的完整认知,掌握选型与接入要点。
SSE(Server-Sent Events,服务器推送事件)是一种基于 HTTP 协议的服务器推送技术,它允许服务端在一条持久的 HTTP 连接上,主动、持续地向客户端发送数据流。
理解SSE,需要抓住三个关键词:
可以用一个通俗的公式来概括:SSE = HTTP扩展字段 + Keepalive长连接。
从不同视角理解这个概念会更立体:
text/event-stream、以分块传输方式持续输出、迟迟“不结束”的HTTP响应;目前几乎所有主流浏览器都原生支持SSE,服务端各类主流语言与框架也均有成熟支持,这是它得以广泛普及的基础。
Web的默认通信模式是请求-响应:客户端发起请求,服务器返回响应,一次交互结束。如果客户端想持续获取“最新数据”,传统做法有:
WebSocket提供了真正的全双工通信能力,但它是一个独立协议:需要先通过HTTP发起“协议升级”握手,之后的通信便脱离标准HTTP语义。这带来一些代价:部分代理、网关等中间设备需要额外配置才能正确转发;服务器需要独立实现连接与协议管理;对于“只需要服务器推数据”的场景,全双工能力其实是冗余的。
SSE正好补上这块空缺:只需要服务器→客户端单向推送时,用纯HTTP的方式实现“服务器主动推”。它简单、轻量、可靠,浏览器原生支持自动重连,无需引入新协议。这也是为什么实时通知、行情推送、日志流,以及当前大模型对话的逐字流式输出,都不约而同选择了SSE。
一次SSE通信在HTTP层面表现为:
客户端请求:
Accept: text/event-stream,表明期望接收事件流。服务端响应:
Content-Type: text/event-stream;Cache-Control: no-cache,避免响应被中间层缓存;Connection: keep-alive,维持长连接;SSE的流内容是纯文本,按“事件块”组织,事件块之间用空行分隔。每个事件块可包含四个标准字段:
| 字段 | 作用 | 说明 |
|---|---|---|
data |
数据本体 | 必备字段;若数据含换行,需拆为多行data,客户端接收后自动拼接 |
event |
事件类型 | 自定义事件名,客户端可为不同事件注册不同监听器;缺省为 message |
id |
事件编号 | 客户端记录最近收到的id,重连时通过 Last-Event-ID 请求头带回,用于断点续传 |
retry |
重连间隔 | 建议客户端重连前等待的毫秒数 |
此外还有“注释行”:以冒号 : 开头的行会被客户端忽略,常被服务端用作心跳保活。
一个典型的事件流片段如下:
id: 1001event: tokendata: {"delta": "你"}id: 1002event: tokendata: {"delta": "好"}event: donedata: [DONE]
浏览器原生提供EventSource标准接口,基本用法:
// 建立连接(整个生命周期只需请求这一次)const source = new EventSource('/api/chat/stream?prompt=你好');// 监听默认的 message 事件source.onmessage = (e) => {console.log('收到数据:', e.data);};// 监听自定义事件source.addEventListener('token', (e) => {appendText(JSON.parse(e.data).delta); // 逐段追加渲染});source.addEventListener('done', () => {source.close(); // 任务结束,主动关闭});// 错误处理source.onerror = () => {// 浏览器通常会自动重连;如需停止可调用 source.close()};
EventSource的三个关键特性值得记住:
retry 字段调整;Last-Event-ID,服务端可据此从断点继续推送;readyState 有三种取值——CONNECTING(连接中或重连中)、OPEN(已打开)、CLOSED(已关闭)。服务端的核心工作只有一件事:按格式持续写出数据并及时刷新。伪代码示意:
# 服务端伪代码:处理一次SSE请求设置响应头: Content-Type = text/event-stream设置响应头: Cache-Control = no-cache设置响应头: Connection = keep-alive序号 = 0for chunk in 大模型逐块生成(prompt):序号 += 1输出 "id: " + 序号输出 "data: " + JSON(chunk)输出空行flush() # 关键:立即刷新输出缓冲输出 "event: done"输出 "data: [DONE]"输出空行结束响应
其中 flush() 是最容易踩坑的一步:如果每个事件写完后不立即刷新,数据会滞留在服务端缓冲区,客户端将感知不到“流式”效果,表现为长时间空白后一次性输出全部内容。
一次完整的SSE交互流程如下:
Accept: text/event-stream;Content-Type: text/event-stream,且不结束该响应,连接保持打开;event 字段分发到对应监听器,同时记录 lastEventId;: ping),防止中间代理或防火墙因连接长期无数据而将其切断;Last-Event-ID;服务端据此从断点继续推送,避免数据丢失;close() 主动断开。特别说明SSE与大模型的契合点:一次回答的生成往往持续数秒到数十秒,内容是逐块(逐词)产生的。SSE恰好把“服务端的生成过程”实时映射为“客户端的渲染过程”——用户看到打字机般的逐字输出,首字响应时间从“等待全部生成完”缩短为“等待第一块生成完”,体验提升显著,同时天然支持“停止生成”等交互。
判断是否适合SSE的黄金法则:只要“数据由服务器持续产生、客户端只需要接收”,且数据是文本形态,SSE就值得优先考虑。
| 维度 | SSE | WebSocket |
|---|---|---|
| 通信方向 | 单向(服务器→客户端) | 全双工(双向) |
| 协议基础 | 标准HTTP | 独立协议(经HTTP升级握手后切换) |
| 数据格式 | 仅UTF-8文本 | 文本与二进制均可 |
| 自动重连 | 原生支持,配合Last-Event-ID断点续传 | 需自行实现 |
| 基础设施兼容 | 走标准HTTP,代理/网关普遍兼容 | 部分中间设备需专门配置 |
| 实现复杂度 | 低,客户端几行代码 | 相对较高 |
选型建议:
需要澄清一个常见误解:大模型对话虽然形式上像“聊天”,但单轮问答中客户端只在开头发一次请求,回答阶段只需要服务器→客户端的单向推送,因此SSE完全够用,这也是业界主流大模型对话类应用普遍采用SSE而非WebSocket的原因。
HTTP/1.1下浏览器对同一域名的并发连接数有限(通常为6个),同时打开多个SSE连接可能互相挤占。HTTP/2的多路复用允许多个流共享一条TCP连接,可显著缓解这一问题,是SSE密集型应用部署时值得确认的前提条件。
不少反向代理、网关组件默认会缓冲后端响应,导致事件被“攒够一批”才到达客户端,流式效果失效。接入时需确认中间链路针对 text/event-stream 关闭了缓冲,或在响应头中声明不缓冲。
负载均衡、服务器对“安静”的长连接可能有空闲超时机制。应对方式:一是适当调大相关超时配置;二是服务端定期发送注释行心跳(如每15~30秒发送一次 : keepalive)。
HTTP/1.1同源连接数上限会约束并发SSE数量(例如用户同时开多个流式会话、多个标签页)。可通过使用HTTP/2、域名分片或“单连接多事件复用分发”来规避。
SSE只能传输UTF-8文本。若需传输二进制数据(图片、音频帧),要么Base64编码(体积膨胀约三分之一),要么改用WebSocket。
Last-Event-ID 的合法性,防止伪造越权拉取。长连接持续占用服务端资源,需要:设置合理的最大连接数与排队策略;在客户端断开时及时释放资源;结合负载均衡分发连接避免单实例过载;任务结束(如模型生成完毕)要显式结束响应,避免产生僵尸连接。
data 内容含换行时必须拆成多行 data:,客户端接收后会用换行拼接还原;自动重连虽然方便,但业务上必须明确:重连后是通过 Last-Event-ID 续传,还是重新开始?对重复推送敏感的场景,服务端需设计幂等、去重或会话恢复机制。
SSE是一种构建在标准HTTP之上的服务器单向推送技术:一次请求、一条长连接、多段持续响应。它的核心价值在于用最小的复杂度实现了“服务器主动向客户端推数据”:
它的适用边界同样清晰:只传UTF-8文本、只做服务器→客户端方向。通知推送、实时数据更新、日志流、任务进度,尤其是大模型逐字流式输出这类“客户端发一次、服务器持续回”的场景,SSE往往是比WebSocket更简单、更合适的选择;而双向高频交互仍应交给WebSocket。
理解SSE,本质上是理解一个朴素的工程判断:在满足需求的前提下,优先选择约束更强、机制更简单、与现有设施更兼容的方案。这正是SSE在大模型时代重新走红的根本原因。