服务器推送事件 (SSE)
浏览器 API 与协议,第 16 章
服务器推送事件(Server-Sent Events,简称 SSE)实现了从服务器到客户端的高效文本事件流推送——例如服务器生成的实时通知或更新。为实现这一目标,SSE 引入了两个核心组件:浏览器端的全新 EventSource 接口,允许客户端以 DOM 事件的形式接收服务器推送;以及”事件流”数据格式,用于传输单个更新。
浏览器中的 EventSource API 与定义明确的事件流数据格式相结合,使 SSE 成为处理浏览器实时数据的高效且不可或缺的工具:
- 通过单一长连接实现低延迟传输
- 高效的浏览器消息解析,无无界缓冲区问题
- 自动跟踪最后接收消息并自动重连
- 以 DOM 事件形式通知客户端消息
底层实现上,SSE 提供了跨浏览器的高效 XHR 流式传输;消息实际通过单一长连接 HTTP 传输。然而,与自行处理 XHR 流式传输不同,浏览器接管了所有连接管理和消息解析工作,让我们的应用可以专注于业务逻辑!简而言之,SSE 让实时数据处理变得简单高效。让我们深入了解其内部机制。
EventSource API
EventSource 接口将所有底层连接建立和消息解析抽象为一个简洁的浏览器 API。开始使用非常简单,只需指定 SSE 事件流资源的 URL,并在对象上注册相应的 JavaScript 事件监听器:
var source = new EventSource("/path/to/stream-url");
source.onopen = function () { ... };
source.onerror = function () { ... };
source.addEventListener("foo", function (event) {
processFoo(event.data);
});
source.onmessage = function (event) {
log_message(event.id, event.data);
if (event.id == "CLOSE") {
source.close();
}
}
- 打开到流端点的新 SSE 连接
- 可选回调,连接建立时触发
- 可选回调,连接失败时触发
- 订阅 “foo” 类型事件;调用自定义逻辑
- 订阅所有无明确类型的事件
- 如果服务器发送 “CLOSE” 消息 ID,则关闭 SSE 连接
EventSource 可以利用与普通 XHR 相同的 CORS 权限和可选工作流程,从远程源流式传输事件数据。
这就是客户端 API 的全部内容。实现逻辑已为我们处理:连接自动协商、接收数据增量解析、消息边界识别,最后由浏览器触发 DOM 事件。EventSource 接口让应用专注于业务逻辑:打开新连接、处理接收的事件通知、完成时终止流。
SSE 提供了内存高效的 XHR 流式传输实现。与原始 XHR 连接(在连接关闭前缓冲完整接收的响应)不同,SSE 连接可以丢弃已处理的消息,而无需在内存中累积所有消息。
锦上添花的是,EventSource 接口还提供自动重连和最后接收消息跟踪功能:如果连接断开,EventSource 会自动重新连接到服务器,并可选地通告最后接收消息的 ID,以便恢复流并重新传输丢失的消息。
浏览器如何知道每条消息的 ID、类型和边界?这就是事件流协议的作用。简洁的客户端 API 与定义明确的数据格式相结合,使我们能够将大部分工作交给浏览器处理。两者相辅相成,尽管底层数据协议对浏览器中的应用完全透明。
使用自定义 JavaScript 模拟 EventSource
SSE 是 HTML5 规范的早期补充,现代浏览器均已原生支持。对于旧版浏览器(如 Internet Explorer 和早期 Android 浏览器),可以通过 JavaScript 库(即”polyfill”)模拟 EventSource 接口。同样,事件流的传输也可以基于现有 XHR 机制实现:
if (!window.EventSource) {
// 加载 JavaScript polyfill 库
}
var source = new EventSource("/event-stream-endpoint");
...
使用 polyfill 库的好处在于,它再次让我们的应用专注于应用逻辑,而非浏览器差异和实现状态。然而,虽然 polyfill 提供一致的 API,需注意底层 XHR 传输效率不如原生实现:
- XHR 轮询会产生消息延迟和高请求开销
- XHR 长轮询最小化延迟但仍有高请求开销
- XHR 流式传输支持有限,且将所有数据缓冲在内存中
在没有原生支持高效 XHR 事件流数据流式传输的情况下,polyfill 库可能回退到轮询、长轮询或 XHR 流式传输,每种方式都有其性能成本。如需完整讨论,请参阅实时通知与传输章节。
简而言之,检查你的 polyfill 库实现,确保它满足性能目标!许多流行库(如 jQuery.EventSource)使用 XHR 轮询来模拟 SSE 传输——简单,但也是低效的传输方式。
事件流协议
SSE 事件流作为流式 HTTP 响应传输:客户端发起常规 HTTP 请求,服务器以自定义 “text/event-stream” 内容类型响应,然后流式传输 UTF-8 编码的事件数据。不过,这听起来仍显复杂,来看个例子:
=> 请求
GET /stream HTTP/1.1
Host: example.com
Accept: text/event-stream
<= 响应
HTTP/1.1 200 OK
Connection: keep-alive
Content-Type: text/event-stream
Transfer-Encoding: chunked
retry: 15000
data: 第一条消息是简单字符串。
data: {"message": "JSON payload"}
event: foo
data: "foo" 类型的消息
id: 42
event: bar
data: "bar" 类型的多行消息
data: ID 为 "42"
id: 43
data: 最后一条消息,ID "43"
- 客户端通过 EventSource 接口发起连接
- 服务器以 “text/event-stream” 内容类型响应
- 服务器设置客户端重连间隔(15 秒),以防连接断开
- 无消息类型的简单文本事件
- 无消息类型的 JSON 载荷
- “foo” 类型的简单文本事件
- 带消息 ID 和类型的多行事件
- 带可选 ID 的简单文本事件
事件流协议极易理解和实现:
- 事件载荷是一个或多个相邻 data 字段的值
- 事件可携带可选 ID 和事件类型字符串
- 事件边界以换行符标记
在接收端,EventSource 接口通过查找换行分隔符解析传入流,从 data 字段提取载荷,检查可选 ID 和类型,最后分派 DOM 事件通知应用。如果存在类型,则触发自定义 DOM 事件;否则调用通用的 “onmessage” 回调;详见 EventSource API 章节。
UTF-8 编码与 SSE 二进制传输
EventSource 不对实际载荷进行额外处理:消息从 data 字段提取,连接在一起,直接传递给应用。因此,服务器可以推送任何文本格式(如简单字符串、JSON 载荷等),应用需自行解码。
需注意,所有事件源数据均为 UTF-8 编码:SSE 并非用于传输二进制载荷的机制!如有必要,可将任意二进制对象 base64 编码以适配 SSE,但这会产生较高(33%)的字节开销;详见资源内联章节。
担心 UTF-8 在线路上的高开销?SSE 连接是流式 HTTP 响应,意味着它可以像任何其他 HTTP 响应一样被压缩(即 gzip)传输!虽然 SSE 不适用于二进制数据传输,但它仍是高效的传输方式:确保你的服务器对 SSE 流应用 gzip 压缩。
缺乏对二进制流的支持并非疏漏。SSE 专门设计为简单、高效的服务器到客户端文本数据传输工具。如需传输二进制载荷,WebSocket 才是合适的工具。
除自动事件解析外,SSE 还提供内置支持,用于重建断开的连接以及恢复客户端断开期间可能错过的消息。默认情况下,如果连接断开,浏览器会自动重建连接。SSE 规范建议 2-3 秒延迟,这是大多数浏览器的常见默认值,但服务器也可随时通过向客户端发送 retry 命令设置自定义间隔。
同样,服务器可为每条消息关联任意 ID 字符串。浏览器自动记住最后接收的 ID,并在发出重连请求时自动附加 “Last-Event-ID” HTTP 头,值为记住的 ID。示例如下:
(现有 SSE 连接)
retry: 4500
id: 43
data: Lorem ipsum
(连接断开)
(4500 毫秒后)
=> 请求
GET /stream HTTP/1.1
Host: example.com
Accept: text/event-stream
Last-Event-ID: 43
<= 响应
HTTP/1.1 200 OK
Content-Type: text/event-stream
Connection: keep-alive
Transfer-Encoding: chunked
id: 44
data: dolor sit amet
- 服务器将客户端重连间隔设置为 4.5 秒
- 简单文本事件,ID: 43
- 自动客户端重连请求,带最后接收事件 ID
- 服务器以 “text/event-stream” 内容类型响应
- 简单文本事件,ID: 44
客户端应用无需提供额外逻辑来重建连接或记住最后接收的事件 ID。整个流程由浏览器处理,我们依赖服务器处理恢复。具体而言,根据应用和数据流的需求,服务器可实现多种不同策略:
- 如果丢失消息可接受,则无需事件 ID 或特殊逻辑:只需让客户端重连并恢复流
- 如果需要消息恢复,则服务器需为相关事件指定 ID,以便客户端重连时报告最后接收的 ID。此外,服务器需实现某种本地缓存,以恢复并向客户端重新传输错过的消息
消息持久化回溯多远的具体实现细节,当然取决于应用需求。另请注意,ID 是可选的事件流字段。因此,服务器也可选择在传输的事件流中检查点特定消息或里程碑。简而言之,评估你的需求,在服务器端实现适当逻辑。
SSE 使用场景与性能
SSE 是服务器到客户端文本实时数据流的高性能传输方式:消息可在服务器可用时立即推送(低延迟),消息开销最小(长连接、事件流协议和 gzip 压缩),浏览器处理所有消息解析,且无无界缓冲区。再加上便捷的 EventSource API(自动重连、以 DOM 事件形式通知消息),SSE 成为处理实时数据的不可或缺的工具!
SSE 有两个关键限制。首先,它仅支持服务器到客户端,因此不适用于请求流式传输场景——例如向服务器流式传输大文件上传。其次,事件流协议专门设计用于传输 UTF-8 数据:二进制流式传输虽然可行,但效率低下。
UTF-8 限制通常可在应用层解决:SSE 向应用通知服务器有新的二进制资源可用,应用再发起 XHR 请求获取。虽然这会产生额外的往返延迟,但也有好处:可利用 XHR 提供的众多服务,如响应缓存、传输编码(压缩)等。如果资源被流式传输,浏览器缓存无法缓存它。
实时推送与轮询一样,可能对电池寿命产生较大负面影响。首先,考虑批处理消息以避免唤醒无线电。其次,消除不必要的保活;无线电空闲时 SSE 连接不会”断开”。更多详情,请参阅消除周期性低效数据传输章节。
SSE 基于 TLS 的流式传输
SSE 在常规 HTTP 连接之上提供简单便捷的实时传输,使其在服务器端易于部署,在客户端易于 polyfill。然而,现有网络中间件(如代理服务器和防火墙)可能不了解 SSE,仍可能引发问题:中间件可能选择缓冲事件流数据,导致延迟增加或 SSE 连接完全中断。
因此,如果遇到此类问题,可考虑通过 TLS 连接传输 SSE 事件流;请参阅代理、中间件、TLS 与 Web 新协议章节。
现代部署建议:
- HTTP/2 多路复用:在 HTTP/2 环境下,SSE 可与其他资源共享单一连接,减少连接开销
- TLS 1.3:0-RTT 握手可进一步降低重连延迟
- 服务器推送(Server Push):虽然 HTTP/2 服务器推送曾被考虑用于 SSE 优化,但现代最佳实践建议谨慎使用,因其可能被滥用或已被部分浏览器弃用