Server-Sent Events 基础
Server-Sent Events(SSE)是一种 HTML5 技术,允许服务器通过 HTTP 连接向浏览器单向推送数据。与 WebSocket 的双向通信不同,SSE 专门用于服务器到客户端的单向数据流,特别适合实时通知、股票行情、日志监控等场景。SSE 基于 HTTP 协议,使用简单,自动重连,且对服务器要求较低。
前置知识
阅读本节前,建议先了解:WebSocket 实战
基础概念
什么是 SSE
Server-Sent Events 让服务器可以通过持久的 HTTP 连接持续向客户端推送消息。客户端使用 EventSource API 接收这些消息,无需复杂的连接管理。
SSE vs 传统轮询
| 特性 | 短轮询 | 长轮询 | SSE |
|---|---|---|---|
| 通信方式 | 客户端定时请求 | 服务器保持请求 | 持久连接推送 |
| 实时性 | 差(有延迟) | 较好 | 优秀 |
| 服务器开销 | 高(频繁请求) | 中 | 低(单连接) |
| 实现复杂度 | 简单 | 中等 | 简单 |
| 协议 | HTTP | HTTP | HTTP |
| 自动重连 | 需手动实现 | 需手动实现 | 内置 |
SSE 的特点
| 特性 | 说明 |
|---|---|
| 单向通信 | 服务器 → 客户端 |
| 自动重连 | 浏览器自动处理断线重连 |
| 文本协议 | text/event-stream Content-Type |
| 事件类型 | 支持命名事件和自定义事件 |
| HTTP 基础 | 不需要特殊协议或端口 |
语法与 API
服务器端数据格式
SSE 的数据格式是纯文本,每条消息由若干字段组成:
data: 消息内容\n
\n// 简单消息
data: Hello World\n
\n
// 多行消息
data: 第一行\n
data: 第二行\n
data: 第三行\n
\n
// 带事件类型的消息
event: notification\n
data: 你有新消息\n
\n
// 带 ID 的消息
id: 12345\n
data: 带ID的消息\n
\n
// 带重试间隔的消息
retry: 3000\n
data: 3秒后重连\n
\nSSE 服务端示例(Node.js)
javascript
// Node.js SSE 服务端示例
const http = require('http');
const server = http.createServer((req, res) => {
// 设置 SSE 响应头
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*'
});
// 每秒发送一次数据
let counter = 0;
const interval = setInterval(() => {
counter++;
// 发送数据
res.write(`data: ${JSON.stringify({ time: new Date().toISOString(), count: counter })}\n\n`);
// 发送自定义事件
if (counter % 5 === 0) {
res.write(`event: milestone\ndata: 已发送 ${counter} 条消息\n\n`);
}
}, 1000);
// 客户端断开时清理
req.on('close', () => {
clearInterval(interval);
});
});
server.listen(3000, () => {
console.log('SSE 服务器运行在 http://localhost:3000');
});客户端 EventSource
javascript
// 创建 EventSource 连接
const eventSource = new EventSource('http://localhost:3000/sse');
// 监听默认消息(无事件类型的消息)
eventSource.onmessage = (event) => {
console.log('收到消息:', event.data);
const data = JSON.parse(event.data);
console.log('时间:', data.time, '计数:', data.count);
};
// 监听自定义事件
eventSource.addEventListener('milestone', (event) => {
console.log('里程碑:', event.data);
});
// 监听连接打开
eventSource.onopen = () => {
console.log('SSE 连接已建立');
};
// 监听错误
eventSource.onerror = (event) => {
console.error('SSE 错误:', event);
// EventSource 会自动重连
};
// 关闭连接
eventSource.close();详细说明
EventSource 属性
| 属性 | 类型 | 说明 |
|---|---|---|
readyState | number | 连接状态(0=连接中, 1=已连接, 2=已关闭) |
url | string | 连接 URL |
withCredentials | boolean | 是否发送跨域凭证 |
javascript
const es = new EventSource('http://localhost:3000/sse');
// readyState 常量
EventSource.CONNECTING; // 0
EventSource.OPEN; // 1
EventSource.CLOSED; // 2
// 跨域发送凭证
es.withCredentials = true;MessageEvent 对象
javascript
eventSource.onmessage = (event) => {
console.log(event.data); // 消息数据
console.log(event.origin); // 来源域名
console.log(event.lastEventId); // 事件 ID
console.log(event.type); // 事件类型('message')
};| 属性 | 说明 |
|---|---|
data | 消息内容字符串 |
origin | 服务器域名 |
lastEventId | 服务器发送的最后事件 ID |
type | 事件类型名称 |
bubbles | 是否冒泡 |
cancelable | 是否可取消 |
自动重连机制
EventSource 内置自动重连机制:
- 连接断开时自动尝试重新连接
- 使用服务器设置的
retry间隔(默认 3 秒) - 重连时发送
Last-Event-ID请求头,服务器可从断点续传
javascript
// 服务器端发送重试间隔
// retry: 5000\n\n // 5 秒后重连
// 客户端自动重连流程
// 1. 连接断开
// 2. 等待 retry 间隔(默认 3 秒)
// 3. 自动发送新请求
// 4. 请求头包含 Last-Event-ID(如果有)实战示例
SSE 实时数据面板
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>SSE 实时数据面板</title>
<style>
body { font-family: -apple-system, sans-serif; padding: 20px; background: #f5f7fa; }
.dashboard {
max-width: 600px; margin: 0 auto;
}
h2 { text-align: center; color: #1a1a2e; }
.status-bar {
display: flex; justify-content: space-between; padding: 12px 16px;
background: white; border-radius: 8px; margin-bottom: 16px;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
}
.status-dot {
width: 10px; height: 10px; border-radius: 50%;
display: inline-block; margin-right: 6px;
}
.status-dot.connected { background: #4CAF50; }
.status-dot.disconnected { background: #F44336; }
.metrics {
display: grid; grid-template-columns: repeat(3, 1fr);
gap: 12px; margin-bottom: 16px;
}
.metric-card {
text-align: center; padding: 20px;
background: white; border-radius: 12px;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
}
.metric-value { font-size: 32px; font-weight: 700; color: #1a73e8; }
.metric-label { font-size: 13px; color: #888; margin-top: 4px; }
.event-log {
height: 300px; overflow-y: auto; padding: 12px;
background: white; border-radius: 12px;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
font-family: monospace; font-size: 13px;
}
.log-entry { padding: 4px 0; border-bottom: 1px solid #f0f0f0; }
.log-time { color: #aaa; }
.log-data { color: #333; }
.log-event { color: #1a73e8; font-weight: 600; }
.controls {
display: flex; gap: 10px; margin-top: 12px;
}
.btn {
flex: 1; padding: 10px; border: none; border-radius: 8px;
cursor: pointer; font-size: 14px; font-weight: 600; color: white;
}
</style>
</head>
<body>
<div class="dashboard">
<h2>SSE 实时数据面板</h2>
<div class="status-bar">
<span><span class="status-dot disconnected" id="statusDot"></span><span id="statusText">未连接</span></span>
<span id="eventCount">事件数: 0</span>
</div>
<div class="metrics">
<div class="metric-card">
<div class="metric-value" id="cpuValue">--</div>
<div class="metric-label">CPU 使用率</div>
</div>
<div class="metric-card">
<div class="metric-value" id="memValue">--</div>
<div class="metric-label">内存使用率</div>
</div>
<div class="metric-card">
<div class="metric-value" id="connValue">--</div>
<div class="metric-label">活跃连接</div>
</div>
</div>
<div class="event-log" id="eventLog">等待连接...</div>
<div class="controls">
<button class="btn" style="background:#4CAF50;" id="connectBtn" onclick="connectSSE()">连接</button>
<button class="btn" style="background:#F44336;" id="disconnectBtn" onclick="disconnectSSE()" disabled>断开</button>
</div>
</div>
<script>
let eventSource = null;
let count = 0;
function connectSSE() {
// 使用模拟 SSE(实际使用 EventSource 连接真实服务器)
// const eventSource = new EventSource('http://localhost:3000/sse');
count = 0;
// 模拟 SSE 数据流
const interval = setInterval(() => {
count++;
const cpu = (30 + Math.random() * 40).toFixed(1);
const mem = (50 + Math.random() * 30).toFixed(1);
const conn = Math.floor(Math.random() * 200 + 50);
document.getElementById('cpuValue').textContent = cpu + '%';
document.getElementById('memValue').textContent = mem + '%';
document.getElementById('connValue').textContent = conn;
addLog('data', `CPU: ${cpu}%, 内存: ${mem}%, 连接: ${conn}`);
document.getElementById('eventCount').textContent = `事件数: ${count}`;
if (count % 5 === 0) {
addLog('milestone', `已推送 ${count} 条数据`);
}
}, 2000);
eventSource = { interval, active: true };
document.getElementById('statusDot').className = 'status-dot connected';
document.getElementById('statusText').textContent = '已连接';
document.getElementById('connectBtn').disabled = true;
document.getElementById('disconnectBtn').disabled = false;
}
function disconnectSSE() {
if (eventSource) {
clearInterval(eventSource.interval);
eventSource = null;
}
document.getElementById('statusDot').className = 'status-dot disconnected';
document.getElementById('statusText').textContent = '已断开';
document.getElementById('connectBtn').disabled = false;
document.getElementById('disconnectBtn').disabled = true;
}
function addLog(type, data) {
const log = document.getElementById('eventLog');
const time = new Date().toLocaleTimeString();
log.innerHTML += `<div class="log-entry">
<span class="log-time">[${time}]</span>
<span class="log-event">[${type}]</span>
<span class="log-data">${data}</span>
</div>`;
log.scrollTop = log.scrollHeight;
}
</script>
</body>
</html>注意事项
- 单向通信:SSE 只支持服务器到客户端,客户端不能发送数据
- 文本协议:SSE 只能传输文本数据,不能传输二进制
- 连接限制:每个域名最多 6 个 SSE 连接
- IE 不支持:IE 不支持 EventSource(Edge 支持)
- 自动重连:浏览器会在连接断开后自动重连
最佳实践
- 使用
event字段区分不同类型的消息 - 使用
id字段支持断点续传 - 使用
retry字段控制重连间隔 - 在服务器端正确设置 CORS 头
- 客户端监听
error事件处理异常
下一节
继续学习:EventSource API