SSE 技术详解
约 4727 字大约 16 分钟
2025-11-07
SSE
前端的 SSE(Server-Sent Events) 是一种 服务器主动向浏览器推送数据 的技术。它建立在 HTTP 协议 之上,可以让服务端持续不断地向客户端发送消息,而不需要客户端频繁轮询。
SSE = “服务端单向推送” 技术
客户端发起一次请求后,服务器可以持续发送事件数据,客户端实时接收更新。
SSE 的核心在于使用一种特殊的 HTTP 响应类型:
Content-Type: text/event-stream通信流程:
- 浏览器通过
EventSource对象发起一个普通的 HTTP 请求; - 服务端保持这个连接 不断开;
- 服务器使用特定格式持续输出数据;
- 浏览器实时接收并触发对应的事件。
前端代码示例
// 创建一个 SSE 连接
const source = new EventSource('/events');
// 监听默认消息
source.onmessage = (event) => {
console.log('收到消息:', event.data);
};
// 监听自定义事件类型
source.addEventListener('update', (event) => {
console.log('收到 update 事件:', event.data);
});
// 连接打开
source.onopen = () => console.log('连接已打开');
// 连接出错
source.onerror = (err) => console.error('连接出错', err);服务端示例(Node.js / Express)
import express from 'express';
const app = express();
app.get('/events', (req, res) => {
// 设置响应头
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// 每隔 2 秒发送一次消息
const timer = setInterval(() => {
res.write(`data: ${new Date().toLocaleTimeString()}\n\n`);
}, 2000);
// 客户端断开时清理
req.on('close', () => {
clearInterval(timer);
res.end();
});
});
app.listen(3000);
console.log('SSE server running on http://localhost:3000');数据格式规范
服务器发送的每条消息都是 纯文本,按以下格式:
data: hello
data: world
event: customEvent
id: 123
retry: 3000- 每条消息以
\n\n结尾; data:是消息主体;event:指定事件类型;id:可用于断线重连时继续接收;retry:表示客户端自动重连时间(毫秒)。
与 WebSocket 对比
| 对比项 | SSE | WebSocket |
|---|---|---|
| 通信方向 | 单向(服务端 → 客户端) | 双向 |
| 协议 | 基于 HTTP | 独立的 ws:// 协议 |
| 重连机制 | 内置自动重连 | 需要手动实现 |
| 数据格式 | 纯文本(UTF-8) | 任意(文本 / 二进制) |
| 浏览器支持 | 普遍支持(IE 不支持) | 普遍支持 |
| 适用场景 | 实时通知、进度更新、日志流 | 聊天、游戏、股票行情等实时交互 |
适用场景举例
- 网站通知中心(如“你有新消息”)
- 长时间任务进度实时推送(构建日志、视频转码状态)
- 实时新闻、天气、数据看板
- 日志监控控制台(实时输出)
实时通信技术
| 技术 | 连接方式 | 实时性 | 服务端压力 | 适用场景 |
|---|---|---|---|---|
| 短轮询 (Polling) | 客户端每隔几秒发请求 | 较差(取决于间隔) | 🚨 高(每次都新建 HTTP) | 简单通知、低频刷新 |
| 长轮询 (Long Polling) | 客户端请求保持挂起,服务端有数据再返回 | 中等 | ⚠️ 中(连接常被创建销毁) | 聊天、通知(早期) |
| SSE (Server-Sent Events) | 服务端持续推送,客户端被动接收 | ✅ 好 | ✅ 低(复用 HTTP) | 实时日志、事件流、轻聊天 |
| WebSocket | 建立独立的双向 TCP 连接 | 🚀 极好 | ⚠️ 较高(连接常驻) | 聊天、游戏、协作应用 |
聊天软件的选型
| 聊天规模 | 常用技术 |
|---|---|
| 小型系统(几十或几百用户) | WebSocket(简单好用) |
| 中型系统(上万连接) | WebSocket + 连接网关(如 Nginx + WS 反向代理) |
| 大型系统(百万+连接) | 混合架构:WebSocket + SSE + 消息队列(Kafka、Redis、MQTT 等) |
| 超大规模(微信、Telegram、Slack) | 自研协议(长连接 + 二进制压缩 + 分布式路由) |
SSE 是 轻量推送 技术,它的优势主要在于:
- 基于 HTTP,穿透性好
走 80/443 端口,不会被公司防火墙拦截;
- 自动重连、断点续传
断线后浏览器自动重连,带上 Last-Event-ID 继续接收未读消息;
- 天然节省资源
无需心跳包(WebSocket 需要维持心跳), 每个连接占用的服务器内存更少;
- 单向推送足够聊天展示
实际上,很多聊天软件的数据流方向是:
发送消息 -> HTTP POST
接收消息 -> SSE 推送即 “发送用 HTTP,请求是一次性的;接收用 SSE,保持实时更新”。
这样比起 WebSocket 双向连接,服务器压力低一大截!
笔记
SSE 简单理解就是一个单向的 websocket ,可以让服务器主动给客户端发消息。
基于 http/https 协议,占用的服务器资源远小于 websocket,服务端的开发难度也低于 websocket
只能服务端给客户端发信息
适用于实时通讯中的 服务端只需要给客户端发消息 的场景
定义一个 SSE 服务很简单,就和定义一个普通接口一样,开启一个 http 服务,定义一个 get 请求,设置 content-type 为 text/event-stream; charset = utf-8,这一步很重要,每次往响应中写入一个数据就会向前端发一个信息,调用 end 结束响应就会断开,但是如果这样断开,客户端会主动重连
DEMO
import express from 'express';
const app = express();
const PORT = 3000;
// 允许静态文件访问(供前端页面使用)
app.use(express.static('.'));
app.get('/sse', (req, res) => {
console.log('📡 收到客户端连接请求');
// 设置响应头(SSE 关键点)
res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// 告知客户端连接已建立
res.write(`data: 👋 服务器连接成功,开始发送消息...\n\n`);
let count = 0;
const message = '你好啊,我是SSE协议,收到请回答!';
// 每秒发送一次消息
const timer = setInterval(() => {
if (count < message.length) {
const char = message[count++];
res.write(`data: ${char}\n\n`);
console.log(`🟢 已发送字符:${char}`);
} else {
// 全部发送完毕
clearInterval(timer);
console.log('✅ 文字发送完毕,准备断开连接');
res.write(`data: [服务器] 消息发送完毕,断开连接。\n\n`);
res.end();
}
}, 100);
// 客户端关闭时清理资源
req.on('close', () => {
clearInterval(timer);
console.log('❌ 客户端已断开连接');
});
});
app.listen(PORT, () => {
console.log(`🚀 SSE 服务已启动:http://localhost:${PORT}`);
});<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>SSE Demo</title>
<style>
body {
font-family: "Segoe UI", sans-serif;
padding: 20px;
background: #f0f3f7;
}
#log {
background: #fff;
border: 1px solid #ddd;
padding: 10px;
border-radius: 6px;
max-width: 600px;
white-space: pre-wrap;
}
</style>
</head>
<body>
<h1>🌐 SSE 实时通信 Demo</h1>
<button id="connect">连接服务器</button>
<button id="disconnect">断开连接</button>
<div id="log"></div>
<script>
const logEl = document.getElementById('log');
let source = null;
const log = (msg) => {
logEl.textContent += msg + '\n';
console.log(msg);
};
document.getElementById('connect').onclick = () => {
if (source) {
log('⚠️ 已经连接中');
return;
}
log('🚀 正在连接服务器...');
source = new EventSource('/sse');
source.onopen = () => log('✅ 连接已建立');
source.onmessage = (e) => log('📨 收到消息:' + e.data);
source.onerror = (e) => {
log('❌ 出现错误,可能连接已断开');
source.close();
source = null;
};
};
document.getElementById('disconnect').onclick = () => {
if (source) {
source.close();
source = null;
log('🔌 手动断开连接');
}
};
</script>
</body>
</html>注意事项
严格遵守 SSE 格式
每条消息必须以 data: 开头,结尾用 两个换行符 \n\n。
可选字段:
event:自定义事件类型id:用于断线重连retry:客户端自动重连时间(毫秒)
id: 1
event: message
data: Hello SSE!原因:浏览器自带的 EventSource 解析 SSE 消息时,如果格式不正确,会导致 onmessage、自定义事件回调无法触发。
连接关闭策略
不主动关闭连接 是标准做法,保持长连接让客户端自动重连。
特殊情况:
- 如果服务端调用
res.end()结束响应,浏览器 EventSource 仍然会尝试自动重连。 - 因此,即便“消息发送完毕”,客户端仍会重新发起连接。
服务器可以利用 Last-Event-ID 让客户端重连后 继续未接收的消息。
断线重连
- 浏览器 EventSource 默认在连接意外断开后自动重连。
id字段可以用来恢复消息连续性。- 可以通过
retry:指定自动重连间隔:
retry: 5000EventSource 实例与重连
一旦 EventSource 对象被关闭(主动 close() 或服务端 end()),同一个实例无法再次使用。
要重新连接,必须 创建新的 EventSource 实例:
source = new EventSource('/sse');消息处理
onmessage只接收符合 SSE 格式的消息。- 可使用
addEventListener('自定义事件名', handler)监听自定义事件。 - 使用
id字段可以在断线重连后恢复消息。
断开连接
- 主动断开:调用
source.close()。 - 被动断开(网络异常、服务端
end()):EventSource 会尝试自动重连。
补充
- SSE 是浏览器原生单向事件流,客户端行为受 EventSource 标准限制。
- 自定义客户端 可以绕过标准限制,实现自己的格式、重连策略和消息缓存。
标准断开流程
服务端发送完毕的消息
- 服务端在消息流里通知客户端“消息发送完毕”,通常是一条特殊标记消息,比如:
data: [SERVER] 本次消息发送完毕\n\n不要直接调用 res.end(),以免客户端立刻断开,虽然浏览器 EventSource 会自动重连,但这可能不是你想要的效果。
客户端处理完毕后主动断开
- 客户端在接收到“完毕”消息后,调用:
source.close();这样保证:
- 消息完整接收
- 不触发 EventSource 的自动重连
- 资源被正确释放
服务端可以关闭连接
- 如果客户端先调用
close(),服务端可以安全地res.end()断开连接。 - 这种方式保证了 客户端主控断开,而不是服务端强制断开,符合 SSE 的长连接理念。
自定义事件
event: 指定 当前消息的事件类型,只对 紧跟在它后面的 data: 消息有效。
覆盖性:只影响它所在消息,不会影响后续的消息。
换句话说,如果你想多条消息都是同一个自定义事件,每条消息都必须写 event: myevent。
默认事件:没有 event: 的消息,客户端通过 onmessage 接收。
# 默认事件
data: 这是默认事件消息
data: 可以多行
\n
# 自定义事件 myevent
event: myevent
data: 第一条自定义事件消息
\n
# 下一条消息不写 event,自动恢复默认事件
data: 又是默认事件消息
\n
# 再次发送自定义事件 myevent
event: myevent
data: 第二条自定义事件消息
\n注意:每条消息的 event: 必须写在它前面。
event: 只影响 紧接着的这一条消息,不会“持续作用”到后续消息。
const source = new EventSource('/sse');
// 默认事件
source.onmessage = (e) => console.log('默认事件:', e.data);
// 自定义事件
source.addEventListener('myevent', (e) => console.log('自定义事件 myevent:', e.data));onmessage:接收没有 event: 的消息(默认事件)
addEventListener('事件名', callback):接收指定自定义事件
如果一条消息带了 event: myevent,只有 myevent 的监听器会被触发。
数据可以多行
event: myevent
data: 第一行
data: 第二行
\n浏览器会把多行 data: 拼接成一个字符串,换行用 \n。
消息结束必须用空行 \n\n
id 字段独立
id: 可以用于断线重连,不影响事件类型。
标准消息(默认事件):
data: Hello World\n\n前端用
EventSource.onmessage接收。这种消息不指定
event:字段,属于 默认事件类型。
自定义事件:
event: myevent
data: 自定义事件消息内容\n\nevent:指定事件名称。前端可以用
addEventListener('myevent', handler)专门监听该事件。
一个 SSE 连接可以同时发送多种事件类型。
多种事件同时发送
setInterval(() => {
res.write(`data: 默认消息 ${Date.now()}\n\n`);
res.write(`event: heartbeat\ndata: 心跳 ${Date.now()}\n\n`);
}, 2000);前端可以这样处理:
source.onmessage = (e) => console.log('默认:', e.data);
source.addEventListener('heartbeat', (e) => console.log('心跳:', e.data));这样一个 SSE 连接就可以同时推送多种类型的消息,非常适合 长连接、状态流、实时通知。
安全鉴权
在现实开发中,SSE(Server-Sent Events)通常用于 实时消息推送,但涉及鉴权和跨浏览器兼容时,有一些特殊注意事项。
原生 EventSource 的鉴权限制
原生 EventSource 无法自定义 HTTP 请求头:
const source = new EventSource('/sse', {
withCredentials: true // 可以携带 cookies,但无法自定义 headers
});如果服务端要求鉴权(比如 JWT Token):
不安全的做法:把 token 拼接到 URL 上:
const source = new EventSource(`/sse?token=${token}`);URL 会暴露在浏览器历史、日志和代理中,不安全。
推荐做法:使用 cookies 携带 token。
- 前端通过
withCredentials: true发送请求时,会自动携带同域 cookies。 - 服务端读取 cookies 并验证鉴权。
注意事项
跨域请求:
withCredentials: true要求服务端允许Access-Control-Allow-Credentials: true。- 并且
Access-Control-Allow-Origin不能使用*,必须指定域名。
JWT + Cookie:
- 前端开启
withCredentials: true。 - 服务端读取 cookie 并验证 JWT。
自定义请求头:
- 原生
EventSource不支持。 - 如果必须自定义请求头(例如
Authorization: Bearer ...),就需要自定义 SSE 客户端。
原生 EventSource 的兼容性
IE 不支持原生 EventSource。
为了兼容,可以:
使用 polyfill:
- 自定义请求头
withCredentials- IE 兼容
或者自行实现 SSE 客户端。
自定义 SSE 客户端实现原理
自定义 SSE 客户端的核心是 通过 Fetch API 获取可读流,然后自己解析流中的数据。
核心步骤
发起请求,获取可读流
const res = await fetch('/sse', {
headers: { 'Authorization': 'Bearer ' + token },
credentials: 'include' // 可选,携带 cookies
});
const reader = res.body.getReader(); // 获取可读流的读取对象res.body.getReader() 返回一个 流读取器,可以逐块读取服务端发送的数据。
创建文本解码器
const decoder = new TextDecoder('utf-8');作用:
- 可读流返回的是 Uint8Array(字节数组)。
TextDecoder.decode(value, { stream: true })将字节数组解码成字符串。{ stream: true }表示流式解码,支持分块拼接。
循环读取数据
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break; // 流读取完毕
buffer += decoder.decode(value, { stream: true }); // 解码字节数组
// 按行解析 SSE 消息
let lines = buffer.split('\n');
buffer = lines.pop(); // 保留未完整的行
for (const line of lines) {
// 解析自定义前缀,比如 'yuu:' 开头
if (line.startsWith('yuu:')) {
const msg = line.slice(3).trim();
console.log('收到消息:', msg);
}
}
}核心点总结
- 可读流返回的是
Uint8Array,需要文本解码器转成字符串。 - 文本编码器/解码器(
TextEncoder/TextDecoder):- 用于字节数组 ↔ 字符串转换
- 流式模式 (
stream: true) 可以拼接跨块消息
- 自定义协议:
- 可以使用固定前缀(如
yuu:)代替data:,防止浏览器默认解析或警告
- 可以使用固定前缀(如
- 逐行解析:
- 需要自己处理
\n换行符 - 可以按事件类型解析自定义事件、id 等字段
- 需要自己处理
自定义 SSE 客户端优缺点
优点
- 可携带自定义请求头(Authorization)
- 可自定义消息格式(不用受浏览器限制)
- 兼容不支持原生
EventSource的环境(IE/老浏览器) - 可灵活处理断线重连策略
缺点
- 实现复杂,需手动解析流、处理拆分、拼接消息
- 长连接需要手动管理心跳、重连等机制
实际开发中的替代方案
event-source-polyfill:
- 可以在 IE 中使用
- 支持自定义请求头和
withCredentials - 避免手动实现流解析逻辑
示例:
import 'event-source-polyfill';
const source = new EventSourcePolyfill('/sse', {
headers: { 'Authorization': 'Bearer ' + token },
withCredentials: true
});
source.onmessage = (e) => console.log('收到消息:', e.data);总结
原生 EventSource:
- 不能自定义请求头,只能通过 cookies 携带 token
- 支持
withCredentials携带 cookie - IE 不支持
自定义 SSE 客户端:
- 通过
fetch+ 可读流实现 - 使用
TextDecoder解码字节数组 - 可自定义协议(例如
yu:前缀) - 可携带任意请求头(Authorization)
安全性:
- URL 传 token 不安全
- 建议通过 cookies 或自定义请求头
推荐:
- 如果兼容性和请求头需求不复杂,用 event-source-polyfill
- 如果需要完全自定义,可实现自己的 SSE 客户端
最佳实践
在控制台可以看到 SSE 的请求类型
也就是说一个接口可以定义很多个事件,完成很多的功能
客户端中,如果通信完毕,记得关闭事件
本质
sse 的本质是以可读流的方式逐个按字节给客户端传输文本,并且 http 是一直连接的,所以服务器可以一直通过连接推送信息
本质上就是一个 get 请求,只不过要按流解析返回
自定义 SSE 客户端示例
- 可自定义请求头(携带 token)
- 支持
withCredentials(携带 cookies) - 自定义事件解析(支持自定义前缀,比如
yu:) - 支持默认事件和自定义事件
- 支持
onmessage和addEventListener - 自动重连机制(可配置)
class CustomEventSource {
constructor(url, options = {}) {
this.url = url;
this.headers = options.headers || {};
this.withCredentials = !!options.withCredentials;
this.retry = options.retry || 3000; // 重连间隔
this.listeners = {}; // 事件监听器
this.onmessage = null; // 默认事件回调
this.closed = false;
this.lastEventId = '';
this._connect();
}
// 注册自定义事件
addEventListener(event, callback) {
if (!this.listeners[event]) this.listeners[event] = [];
this.listeners[event].push(callback);
}
// 内部方法:触发事件
_dispatchEvent(event, data) {
if (event === 'message' && this.onmessage) {
this.onmessage({ data });
}
if (this.listeners[event]) {
this.listeners[event].forEach(cb => cb({ data }));
}
}
// 主动关闭连接
close() {
this.closed = true;
if (this.controller) this.controller.abort();
}
// 内部方法:连接 SSE
async _connect() {
if (this.closed) return;
try {
this.controller = new AbortController();
const signal = this.controller.signal;
// 构建 fetch 请求
const res = await fetch(this.url, {
headers: {
...this.headers,
'Cache-Control': 'no-cache',
'Accept': 'text/event-stream',
},
credentials: this.withCredentials ? 'include' : 'same-origin',
signal
});
if (!res.body) throw new Error('SSE not supported in this environment');
const reader = res.body.getReader();
const decoder = new TextDecoder('utf-8');
let buffer = '';
while (!this.closed) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
let lines = buffer.split('\n');
buffer = lines.pop(); // 保留未完整的行
for (let line of lines) {
line = line.trim();
if (!line) continue;
// 支持自定义前缀,比如 'yu:' 或默认 'data:'
let eventType = 'message';
let data = '';
if (line.startsWith('event:')) {
eventType = line.slice(6).trim();
} else if (line.startsWith('yu:')) {
eventType = 'yu'; // 自定义事件类型
data = line.slice(3).trim();
this._dispatchEvent(eventType, data);
continue;
} else if (line.startsWith('data:')) {
data = line.slice(5).trim();
this._dispatchEvent(eventType, data);
} else if (line.startsWith('id:')) {
this.lastEventId = line.slice(3).trim();
}
}
}
} catch (err) {
if (!this.closed) {
console.warn('SSE连接异常,尝试重连:', err);
setTimeout(() => this._connect(), this.retry);
}
}
}
}使用示例
// 创建自定义 SSE 客户端
const source = new CustomEventSource('/sse', {
headers: { 'Authorization': 'Bearer mytoken123' },
withCredentials: true,
retry: 5000
});
// 默认事件
source.onmessage = (e) => {
console.log('默认事件:', e.data);
};
// 自定义事件
source.addEventListener('yu', (e) => {
console.log('自定义事件 yu:', e.data);
});
// 主动关闭连接
// source.close();功能特点
原生 EventSource 功能兼容
onmessage(默认事件)addEventListener('event', callback)(自定义事件)- 自动重连
增强功能
- 自定义请求头(可携带 token)
- 支持
withCredentials(cookies 鉴权) - 自定义事件解析(支持
yu:前缀) - 支持
id:字段保存 lastEventId,可扩展断线重连逻辑 - 可配置重连间隔
可扩展
- 可以自定义前缀解析规则
- 可以扩展
retry:逻辑支持动态调整 - 可加入心跳检测和长连接超时处理
说明
- 每条消息都需要 完整的 SSE 格式,否则不会触发事件回调。
decoder.decode(value, { stream: true })用于将 Uint8Array 字节数组 解码成字符串,支持分块流式处理。- 如果你需要在 IE 或不支持 EventSource 的浏览器使用,可以用这个自定义客户端替代原生 EventSource。
- 服务器端仍然遵守 SSE 协议即可,客户端解析逻辑可完全自定义(例如固定
yu:前缀)。