HTTP SSE Server-Sent Events 推送
什么是 Server-Sent Events?
Server-Sent Events(简称 SSE)是一项基于 HTTP 协议的服务器推送技术。它允许服务器通过一个持久的、单向的 HTTP 连接向客户端不断发送数据更新。与请求-响应模式不同,SSE 的连接由客户端发起,之后服务器可随时主动推送数据,客户端无需反复轮询。
- 协议基础:基于标准 HTTP/HTTPS,无需额外协议或握手。
- 数据方向:单向——从服务器流向客户端。
- 内容格式:UTF-8 编码的纯文本,遵循
text/event-streamMIME 类型。 - 浏览器支持:所有现代浏览器原生支持
EventSourceAPI,除 IE 外兼容性极佳。
SSE 是如何工作的?
建立连接
客户端通过 EventSource 对象向服务器发起一个 HTTP GET 请求,并在请求头中声明可接受的 MIME 类型:Accept: text/event-stream。服务器一旦接受该连接,必须返回状态码 200 以及响应头 Content-Type: text/event-stream,并保持连接不关闭。
消息格式
SSE 的数据流由一系列以两个换行符 \n\n 分隔的字段组成。每个字段以关键字开头,后跟冒号和值。最常见的字段有:
data:携带消息的实际内容。可以多行连续出现,合并成一个完整消息。event:自定义事件类型。不指定时,默认触发message事件。id:消息的唯一标识,用于断线重连时的Last-Event-ID头。retry:指定客户端在连接断开后重新尝试连接的毫秒数。
一个完整的 SSE 消息示例:
event: user-update
id: 42
data: {"name": "Alice", "status": "online"}
data: 这条消息没有指定事件类型,会触发默认 message 事件
注意:每条数据末尾由两个换行符 \n\n 终结,表示一条消息的结束。
连接保持与重连
若连接意外断开,浏览器内置的 EventSource 会自动尝试重连。服务器可以在任意时点发送 retry 字段来调整重连间隔。重连时,浏览器会在新请求头中携带 Last-Event-ID,服务器可利用该 ID 从断点恢复数据发送。
SSE 与 WebSocket 的对比
| 特性 | SSE | WebSocket |
|---|---|---|
| 数据传输方向 | 单向(服务器到客户端) | 双向 |
| 协议 | HTTP/1.1 或 HTTP/2 | 独立协议(ws:// / wss://) |
| 复杂程度 | 非常低,只需普通 HTTP 服务器即可 | 较高,需处理升级握手和帧协议 |
| 浏览器 API | EventSource 简洁易用 |
WebSocket API |
| 自动重连 | 内置支持 | 需手动实现 |
| 二进制数据 | 仅支持文本(UTF-8) | 支持文本与二进制 |
| 防火墙/代理友好 | 极友好,在标准 HTTP 端口上运行 | 可能被某些代理阻挡 |
| HTTP/2 多路复用 | 天然兼容,可共享连接 | 独立连接,无法复用 HTTP/2 流 |
选型建议:当你只需要服务器主动推送更新(例如新闻推送、股价更新、赛事比分),且从客户端到服务器的通讯可通过常规 AJAX 完成时,SSE 是更简单、更易于扩展的选择。需要双向实时交互(如聊天、协作编辑)才考虑 WebSocket。
适用场景
- 实时仪表盘与监控:系统指标、日志流、用户行为分析。
- 社交动态与通知:新消息提醒、点赞、评论更新。
- 金融行情推送:股票、加密货币、汇率变动。
- 物联网设备状态:传感器读数、设备告警。
- AI 生成内容流:如流式输出的聊天机器人或长篇文本生成。
- 游戏中的状态同步:适合单向状态广播(如排行榜、全局倒计时)。
服务器端实现
以下以 Node.js + Express 为例,展示一个基础 SSE 服务端。
1. 基本推送服务
const express = require('express');
const app = express();
app.get('/events', (req, res) => {
// 设置 SSE 必要的响应头
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
// 允许跨域(按需配置)
'Access-Control-Allow-Origin': '*'
});
// 发送初始注释或 retry
res.write(': connected\n\n');
// 定时发送当前时间
const intervalId = setInterval(() => {
const data = {
time: new Date().toISOString(),
value: Math.floor(Math.random() * 100)
};
// 发送带事件类型的消息
res.write(`event: update\ndata: ${JSON.stringify(data)}\n\n`);
}, 2000);
// 客户端断开连接时清理资源
req.on('close', () => {
clearInterval(intervalId);
res.end();
});
});
app.listen(3000, () => console.log('SSE server running on port 3000'));
2. 利用消息 ID 实现断点续传
为每条消息添加 id 字段,客户端重连时会通过 Last-Event-ID 请求头发送最后收到的 ID。服务器可据此从历史记录中继续推送。
let lastId = 0;
app.get('/events-with-id', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
});
// 检查客户端断线时最后的 id
const lastEventId = req.headers['last-event-id'];
if (lastEventId) {
// 实际场景中可从消息队列或日志中获取该 ID 之后的事件
console.log(`Client reconnected with id: ${lastEventId}`);
}
const intervalId = setInterval(() => {
lastId++;
const message = {
id: lastId,
text: `Event number ${lastId}`
};
res.write(`id: ${lastId}\ndata: ${JSON.stringify(message)}\n\n`);
}, 1000);
req.on('close', () => {
clearInterval(intervalId);
});
});
3. 生产环境考虑
- 连接管理:每个客户端占用一个连接,需注意服务器最大连接数限制。使用 HTTP/2 可复用连接,大幅提升并发能力。
- 负载均衡:需确保同一客户端的请求始终落到同一后端实例(粘性会话),否则重连 ID 机制会失效。或采用发布/订阅模式将事件广播至所有实例。
- 消息格式:纯文本传输,数据量大时可考虑启用 Gzip 压缩(但注意缓冲可能导致延迟)。
- 超时:一些代理或 CDN 会断开长时间空闲的 HTTP 连接。可定期发送心跳注释(以冒号开头的行,如
: heartbeat\n\n)保持连接。
客户端使用
1. 建立连接与接收消息
const eventSource = new EventSource('http://localhost:3000/events');
// 默认消息事件(未指定 event 字段)
eventSource.onmessage = (event) => {
console.log('Default message:', event.data);
};
// 自定义事件监听
eventSource.addEventListener('update', (event) => {
const data = JSON.parse(event.data);
console.log('Update received:', data);
// 更新 UI...
});
// 连接打开时触发
eventSource.onopen = () => {
console.log('SSE connection opened');
};
// 错误处理(包含连接关闭等情况)
eventSource.onerror = (err) => {
console.error('SSE error:', err);
// 浏览器会自动重连,除非手动关闭
};
2. 主动关闭连接
eventSource.close();
3. 重连与 readyState
EventSource.CONNECTING(0):正在连接或重连。EventSource.OPEN(1):连接已建立。EventSource.CLOSED(2):连接已关闭(手动调用close()或连接失败且浏览器放弃重连)。
高级特性
多路复用与 HTTP/2
当使用 HTTP/2 时,多个 SSE 连接可复用在同一个 TCP 连接上(流并发),大幅减少资源消耗。所有现代浏览器和主流服务器(Nginx、Caddy、Node.js)均支持。只需确保服务器启用 HTTP/2,无需改动 SSE 代码。
仅发送注释保持连接
注释行可作心跳使用,对客户端不可见:
: heartbeat
客户端 EventSource 会忽略它,但能刷新底层 TCP 连接,防止代理切断。
动态重试间隔
通过 retry 字段控制浏览器重连频率:
retry: 3000
表示断开后 3 秒重新连接。每条新 retry 值都会覆盖前一个设定。
自定义事件与多通道
通过 event 字段可将数据分类推送,客户端按需监听。例如同一个连接可同时推送 chat、alert、system 事件,客户端只处理自己关心的类型。
CORS 支持
跨域请求时,服务端必须返回 Access-Control-Allow-Origin 头。浏览器不会在 EventSource 中发送非简单请求(如包含自定义头),但会发送 Origin 头,服务端需允许。
安全与性能最佳实践
- 始终使用 HTTPS:防止数据篡改和窃听。
- 鉴权:SSE 连接无法自定义请求头,通常将令牌放在查询参数或 Cookie 中。如
?token=xxx,服务端验证后维持连接。 - 防止资源耗尽:限制单个 IP 的连接数,设置合理的心跳间隔,及时释放关闭的连接。
- 消息大小控制:SSE 是纯文本流,避免单条消息过大导致内存压力。
- 避免缓存:在响应头中设置
Cache-Control: no-cache(或no-store),防止中间节点缓存事件流。 - 内容安全策略 (CSP):如使用
connect-src指令,确保EventSource的 URL 被允许。
常见问题排查
| 现象 | 可能原因 | 解决措施 |
|---|---|---|
| 客户端连接后不触发事件 | 服务器未返回正确的 Content-Type 或消息格式缺少两个换行 |
检查响应头必须为 text/event-stream,每条消息以 \n\n 结尾 |
| 连接频繁断开重连 | 网络代理或 CDN 超时切断 | 添加定期注释心跳,调整 retry 间隔 |
| 重连后数据丢失 | 服务器未实现 Last-Event-ID 逻辑 |
在服务端读取该请求头,补发缺失事件 |
| 跨域请求失败 | 缺少 CORS 响应头 | 添加 Access-Control-Allow-Origin 头 |
| HTTP/1.1 并发瓶颈 | 每个客户端独占一个 TCP 连接 | 升级到 HTTP/2 或使用负载均衡分散连接 |
总结
Server-Sent Events 是一种轻量、易维护的实时推送方案。它完美契合了“服务器主动通知客户端”的多数场景,且得益于原生 HTTP 协议和浏览器的自动重连、事件管理等支持,极大降低了开发复杂性。当你面临实时单向数据推送需求时,不妨优先考虑 SSE——你可能会惊喜于它的简洁与高效。