Sever-Sent Events
服务器发送事件(Server-Sent Events,简称SSE)是一种允许服务器向浏览器推送信息的技术。
是基于 HTTP 的、服务端→客户端单向、持续推送文本事件流。
识别特征
如何判断某个请求是否使用了 SSE 技术?
最可靠的方式是看 Request/Response 的「指纹」,
1️⃣看 Response
- 看 Response Header, 下面第一个头是强制的,后面三个是辅助头
<font style="background-color:#FBF5CB;">Content-Type: text/event-stream</font>—— SSE 唯一强制也是最具辨识度的标志Connection: keep-alive—— 连接要长期保持Cache-Control: no-cache/no-store—— 禁止缓存X-Accel-Buffering: no—— 禁用 Nginx 缓冲
- 看 Response Body, SSE 有固定的行格式
event: done
id: 42
data: {"token":"你好"}
2️⃣ 看 Request
Request Header 的 Accept: text/event-stream,但这不是强制的要求。
如果是断线重连的情况,浏览器会自动带上一个请求头:Last-Event-ID: xx,也可以作为一个识别特征。
典型应用场景
SSE 适用于那些只需要向下推、内容是文本,想要开箱即用的断线重连机制的场景。
- LLM 流式输出,eg:deepseek
- 实时数据看板
- 进度与任务状态
SSE 的局限
- 只能传 utf-8 文本,不支持推送二进制
- 在使用 HTTP1.0 或 1.1 协议时,浏览器针对单个域名存在 6 条连接上限的并发限制
示例:一次正常的 SSE 流程
SSE 的全流程,从建立到结束过程中,都发生了什么事情?
1️⃣ Client 使用 EventSource 创建一个对象实例,传入 URL
es = new EventSource(`/api/chat/stream?prompt=${encodeURIComponent(prompt)}&sid=${sid}`)
2️⃣ 浏览器发出 GET 请求(依旧使用 HTTP 协议),指定接收类型为 text/event-stream
3️⃣ Server 收到 GET 请求,执行服务逻辑,产生 token 流,然后使用 http.ServerResponse的 write() 方法向 HTTP 响应块分体(chunked)写入数据,且不关闭连接。
res.send():一次性发送并自动end(),适合普通请求-响应;发完连接就结束。res.write():多次、增量写入,连接保持打开,适合流式;需手动end()。
4️⃣ Client 通过监听 EventSource 对象的 onmessage 事件获取 Server 返回的 chunked 数据,然后更新到 UI 上,实现流式渲染。
5️⃣ Server 在 token 流生成完毕之后,调用 end() 方法结束响应流程
6️⃣ Client 通过监听 EventSource 对象的 done 事件,在 Client 结束响应流程后,调用 EventSource 的 close() 方法关闭当前连接。
需要注意的是,这里 EventSource 对象所监听的 done 事件并非是 EventSource 规范内置的事件类型。 而是自定义命名事件 —— 由 Sever 在 SSE 数据流里动态声明(前面的write(event: done))
✅ 至此,一个正常的 SSE 流程就完成了。
客户端 API:EventSource
权威资料:EventSource | MDN
EventSource 接口是 web 内容与服务器发送事件通信的接口。
一个 EventSource 实例会对 HTTP 服务器开启一个持久化的连接,以 text/event-stream 格式发送事件。此连接会一直保持开启,直到通过 **EventSource.close()** 关闭。
interface EventSource {
readonly readyState: 0 | 1 | 2; // 分别对应 connecting open 和 closed。
readonly url: string;
readonly withCredentials?: boolean; // 是否使用 cors 凭据,默认值是 false。
close(): void;
onopen: ((this: EventSource, ev: Event) => any) | null;
onmessage: ((this: EventSource, ev: MessageEvent<any>) => any) | null;
onerror: ((this: EventSource, ev: Event) => any) | null;
}
⚙ 自定义事件
默认情况下,服务器发来的数据总是触发浏览器 EventSource 实例的 message 事件。除此之外,在服务端还可以自定义 SSE 事件,这种情况下发送回来的数据不会触发 EventSource 实例的 message 事件,可以通过 addEventListener 去设置自定义事件的监听回调。
// 服务端
res.write(`event: done\ndata: xx`)
// 客户端
es.addEventListener('done', (e)=>{
// 处理 done 相关的逻辑
})
服务器实现
数据格式
服务器向浏览器发送的 SSE 数据必须是 utf-8 编码的文本,并且要设置如下的 HTTP Header 信息:
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
X-Accel-Buffering: no, // 关闭代理缓冲
每一次发送的消息由若干个 message 组成,每个 message 之间用 \n\n 分隔。每个 message 内部由若干行组成,每一行都是 [field]: value\n 这种格式。
其中 [filed] 可以取 4 个值:data, event, id, retry
data: some text\n\n
id: 1\ndata: another message\n
id: 2\ndata: with two lines \n\n
除此之外,还可以有冒号开头的行表示注释。通常服务器每隔一段时间就会向浏览器发送一个注释,来保证连接不中断。 不过这个注释在 JS API 底层获取不到。
: This is a comment
data 字段
数据内容用 data 字段表示。
如果数据很长,可以分成多行。
data: {\n
data: "foo": "bar",\n
data: "baz", 555\n
data: }\n\n
event 字段
Event 字段表示自定义的事件类型,缺省默认是 message 事件。
浏览器可以用 addEventListener() 监听该事件。
event: done\n
data: totalNums\n\n
id 字段
数据标识符用 ID 字段表示,相当于每条数据的编号。
浏览器用 lastEventId 属性读取这个值,一旦连接断线,浏览器会发送一个 HTTP 头,里面包含一个特殊的 Last-Event-ID 头信息。将这个值发送回来,是用来帮助服务器端重建连接的。
因此,这个头信息可以被视为一种同步机制。
retry 字段
服务器用这个字段指定浏览器重新发起连接的时间间隔。
retry: 10000\n
两种情况会导致浏览器重新发起连接:
- 一种是时间间隔到期;
- 另一种是网络错误等原因导致连接出错。
客户端的另一种实现
在客户端一侧,EventSource 对象提供了开箱即用的 SSE 服务,但 EventSource 存在一个局限:默认且只支持 GET 请求。
而大模型接口的请求使用的是 POST 方法 + JSON body 的载体形式。
所以另外一种实现是:用 fetch 拿到 response.body 的 ReadableStream,手动按 \n\n 切分 SSE 帧,并手动实现自动断线重连和 Last-Event-ID 续传。
SSE 规范
在 HTTP 层面并不存在 “SSE 协议”这种东西。
RFC 也没有规定 text/event-stream内部的格式。
实际上, SSE 这套格式的约定来自于:WHATWG 的 HTML Living Standard 中 "Server-sent events" 章节(历史上最早由 Opera 在 2006 年提出并实现;后被 Ian Hickson 纳入 HTML5 规范草案;在 W3C 与 WHATWG 分家后,该章节由 WHATWG 作为 Living Standard 持续维护)。
这份规范定义了三件事:
- MIME 类型:必须是 text/event-stream,这是浏览器识别流的前提;
- 事件流的语法(ABNF):field: value 文本行、空行分帧,字段只有 4 个 —— event, data, id, entry,冒号开头是注释行;
- 解析算法(Interpreting an event stream):浏览器如何把字节流切分成一行行,聚合到空行时派发(dispatch)事件,如何维护 last event ID,并用于重连时发送 Last-Event-ID 请求头
EventSource API 就是这份规范的配套客户端,浏览器内部实现的就是该章节的解析算法。
所以本质上是规范先定义了格式与解析算法,浏览器按规范实现解析器,服务端为了和这个解析器互通,就必须发出符合该格式的字节流。
这是互操作性约束,而不是语言或框架层面的强制。
附录
原文链接
演示项目
参考资料
拓展注释
- WHATWG(Web Hypertext Application Technology Working Group,网页超文本应用技术工作组,官方读法有 what-wee-gee / what-wig 等)不是一个传统意义上的标准组织,官方自我定位是:一个致力于通过标准和测试推动 Web 演进的开放社区。官网:whatwg.org/
- "Living Standard",一种没有版本号、像软件一样持续滚动更新的 Web 标准形态,随开发者反馈和浏览器实现不断演进,每次变更都会发布一个永久可引用的冻结快照——由 WHATWG 创立并践,标志性的标准有:
- HTML5
- DOM
- Fetch(现代 Web 的网络请求模型)
- URL
- Encoding(字符编码在 Web 上的运作方式)
- Streams(流式数据 API)
- MIME ,(Multipurpose Internet Mail Extensions,多用途互联网邮件扩展)类型是一种标准,用于标识互联网上的文件、数据流或文档的性质和格式。它最初是为电子邮件设计的,但现在广泛应用于 Web 浏览器、服务器、API 以及各种网络协议中,帮助系统正确处理不同类型的内容。
- W3C,(World Wide Web Consortium,万维网联盟)是**制定万维网标准的国际行业协会,**产出标准文件 Recommendation(REC,推荐标准),代表标准有:
- 标记语言:XML、SVG、MathML
- CSS
- WCAG(全球法规常引用的无障碍基线)
- i18n(多语言排版、字符编码、 bidi)
- IndexedDB