pin_drop当前位置:知识文库 ❯ 图文
后端通信:SSE服务器推送 - 从入门到实践详解
教程简介
Server-Sent Events(SSE)是HTML5提供的服务器推送技术,允许服务器通过HTTP连接单向向客户端推送数据。与WebSocket的全双工通信不同,SSE专注于服务器到客户端的单向推送,具有更简单的API、内置的自动重连、事件类型支持等优势。SSE非常适合实时通知、数据监控、新闻推送等单向数据流场景。
本教程将全面讲解SSE原理、EventSource API、事件格式、连接管理、自动重连、事件ID、自定义事件类型、SSE与WebSocket对比,以及实时通知实现。
核心概念
1. SSE原理
SSE基于HTTP长连接实现服务器推送:
客户端发起HTTP请求,请求头包含
Accept: text/event-stream服务器保持连接打开,持续发送事件数据
数据以
text/event-stream格式传输连接断开时浏览器自动重连
2. EventSource API
代码示例
// 创建连接
const eventSource = new EventSource('/api/events');
// 监听消息
eventSource.onmessage = function(event) {
console.log('收到消息:', event.data);
console.log('事件ID:', event.lastEventId);
};
// 监听连接打开
eventSource.onopen = function() {
console.log('连接已建立');
};
// 监听错误
eventSource.onerror = function(event) {
if (eventSource.readyState === EventSource.CONNECTING) {
console.log('正在重连...');
} else if (eventSource.readyState === EventSource.CLOSED) {
console.log('连接已关闭');
}
};
// 关闭连接
eventSource.close();EventSource属性:
readyState状态:
3. 事件格式
SSE事件流格式:
代码示例
field: value\n每个事件以空行分隔:
代码示例
field1: value1\n
field2: value2\n
\n字段类型:
多行数据:
代码示例
data: 第一行\n
data: 第二行\n
\n客户端接收时两行用换行符连接:"第一行\n第二行"
JSON数据:
代码示例
data: {"name":"张三","email":"zhangsan@example.com"}\n
\n4. 自定义事件类型
代码示例
// 监听自定义事件
eventSource.addEventListener('update', function(event) {
const data = JSON.parse(event.data);
console.log('更新事件:', data);
});
eventSource.addEventListener('notification', function(event) {
const data = JSON.parse(event.data);
console.log('通知事件:', data);
});
eventSource.addEventListener('alert', function(event) {
console.log('告警事件:', event.data);
});服务器发送:
代码示例
event: update\n
data: {"price": 99.9}\n
\n
event: notification\n
data: {"message": "新消息"}\n
\n
event: alert\n
data: 系统告警!\n
\n5. 事件ID与自动重连
SSE内置自动重连机制:
连接断开时浏览器自动重连
使用
id字段标记事件,重连时通过Last-Event-ID头部发送最后一个事件ID服务器据此从断点续传
代码示例
// 客户端
const eventSource = new EventSource('/api/events');
// 服务器发送带ID的事件
// id: 100\n
// data: 消息内容\n
// \n
// 重连时浏览器自动发送
// Last-Event-ID: 100retry字段:设置重连间隔
代码示例
retry: 5000\n
\n6. SSE vs WebSocket
SSE的优势:
基于HTTP,无需特殊协议
内置自动重连和事件ID
更简单的API
天然兼容代理和CDN
SSE的劣势:
单向通信
不支持二进制数据
IE不支持(可用polyfill)
7. 使用Fetch实现SSE
当需要更多控制时,可以用Fetch API手动解析SSE流:
代码示例
async function connectSSE(url) {
const response = await fetch(url, {
headers: { 'Accept': 'text/event-stream' }
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n\n');
buffer = lines.pop();
for (const event of lines) {
if (!event.trim()) continue;
let data = '', eventType = 'message', id = '';
for (const line of event.split('\n')) {
if (line.startsWith('data:')) data += line.slice(5).trim() + '\n';
else if (line.startsWith('event:')) eventType = line.slice(6).trim();
else if (line.startsWith('id:')) id = line.slice(3).trim();
}
data = data.trimEnd();
if (data) {
console.log({ eventType, data, id });
}
}
}
}代码示例
示例1:SSE事件流模拟器
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SSE事件流模拟器</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', system-ui, sans-serif; background: #f0f2f5; padding: 20px; color: #333; }
.container { max-width: 900px; margin: 0 auto; }
h1 { text-align: center; color: #1a73e8; margin-bottom: 24px; font-size: 28px; }
.card { background: #fff; border-radius: 12px; padding: 24px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
.card h2 { font-size: 18px; color: #1a73e8; margin-bottom: 16px; padding-bottom: 8px; border-bottom: 2px solid #e8eaed; }
.status-bar { display: flex; align-items: center; gap: 10px; margin-bottom: 16px; padding: 10px; background: #f8f9fa; border-radius: 8px; }
.status-dot { width: 10px; height: 10px; border-radius: 50%; }
.dot-connecting { background: #ff9800; animation: pulse 1s infinite; }
.dot-open { background: #34a853; }
.dot-closed { background: #ea4335; }
@keyframes pulse { 0%,100% { opacity:1; } 50% { opacity:0.4; } }
.btn-row { display: flex; gap: 8px; margin-bottom: 16px; flex-wrap: wrap; }
button { padding: 8px 16px; border: none; border-radius: 6px; font-weight: 600; cursor: pointer; font-size: 13px; }
.btn-connect { background: #34a853; color: #fff; }
.btn-disconnect { background: #ea4335; color: #fff; }
.btn-simulate { background: #1a73e8; color: #fff; }
.event-log { background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; font-family: 'Consolas', monospace; font-size: 12px; line-height: 1.8; max-height: 350px; overflow-y: auto; white-space: pre-wrap; }
.log-event { color: #64b5f6; }
.log-data { color: #81c784; }
.log-system { color: #ffb74d; }
.log-error { color: #e57373; }
.stats { display: flex; gap: 16px; margin-top: 12px; }
.stat { padding: 8px 14px; background: #f8f9fa; border-radius: 6px; font-size: 13px; }
.stat b { color: #1a73e8; }
</style>
</head>
<body>
<div class="container">
<h1>SSE事件流模拟器</h1>
<div class="card">
<h2>连接状态</h2>
<div class="status-bar">
<div class="status-dot dot-closed" id="statusDot"></div>
<span id="statusText">未连接</span>
</div>
<div class="btn-row">
<button class="btn-connect" onclick="startSimulation()">开始模拟</button>
<button class="btn-disconnect" onclick="stopSimulation()">停止</button>
<button class="btn-simulate" onclick="sendCustomEvent()">发送自定义事件</button>
</div>
</div>
<div class="card">
<h2>事件流</h2>
<div class="event-log" id="eventLog">等待连接...</div>
<div class="stats">
<div class="stat">事件数: <b id="eventCount">0</b></div>
<div class="stat">最后ID: <b id="lastId">-</b></div>
<div class="stat">运行时间: <b id="runtime">0s</b></div>
</div>
</div>
</div>
<script>
let simulationTimer = null;
let eventId = 0;
let eventCount = 0;
let startTime = 0;
let runtimeTimer = null;
const eventTypes = [
{ type: 'message', data: () => JSON.stringify({ text: '系统消息', time: new Date().toISOString() }) },
{ type: 'update', data: () => JSON.stringify({ price: (Math.random() * 1000).toFixed(2), change: (Math.random() > 0.5 ? '+' : '-') + (Math.random() * 5).toFixed(2) }) },
{ type: 'notification', data: () => JSON.stringify({ title: '新通知', body: '您有一条新消息', type: ['info', 'warning', 'success'][Math.floor(Math.random() * 3)] }) },
{ type: 'heartbeat', data: () => 'ping' }
];
function log(msg, type = 'system') {
const time = new Date().toLocaleTimeString();
document.getElementById('eventLog').innerHTML += `<span class="log-${type}">[${time}] ${msg}</span>\n`;
document.getElementById('eventLog').scrollTop = document.getElementById('eventLog').scrollTop + 1000;
}
function updateStatus(state) {
const dot = document.getElementById('statusDot');
const text = document.getElementById('statusText');
const states = {
connecting: { dot: 'dot-connecting', text: '正在连接...' },
open: { dot: 'dot-open', text: '已连接' },
closed: { dot: 'dot-closed', text: '已断开' }
};
dot.className = 'status-dot ' + states[state].dot;
text.textContent = states[state].text;
}
function startSimulation() {
if (simulationTimer) return;
eventId = 0;
eventCount = 0;
startTime = Date.now();
updateStatus('connecting');
log('正在建立SSE连接...', 'system');
setTimeout(() => {
updateStatus('open');
log('SSE连接已建立', 'system');
log('Content-Type: text/event-stream', 'system');
log('retry: 5000', 'system');
log('---', 'system');
simulationTimer = setInterval(() => {
const eventType = eventTypes[Math.floor(Math.random() * eventTypes.length)];
if (eventType.type === 'heartbeat' && Math.random() > 0.3) return;
eventId++;
eventCount++;
log(`event: ${eventType.type}`, 'event');
log(`id: ${eventId}`, 'event');
log(`data: ${eventType.data()}`, 'data');
log('', 'system');
document.getElementById('eventCount').textContent = eventCount;
document.getElementById('lastId').textContent = eventId;
}, 2000 + Math.random() * 3000);
runtimeTimer = setInterval(() => {
document.getElementById('runtime').textContent = Math.floor((Date.now() - startTime) / 1000) + 's';
}, 1000);
}, 1000);
}
function stopSimulation() {
clearInterval(simulationTimer);
clearInterval(runtimeTimer);
simulationTimer = null;
runtimeTimer = null;
updateStatus('closed');
log('SSE连接已关闭', 'system');
}
function sendCustomEvent() {
if (!simulationTimer) { log('请先建立连接', 'error'); return; }
eventId++;
eventCount++;
const data = JSON.stringify({ message: '自定义事件', timestamp: Date.now() });
log('event: custom', 'event');
log(`id: ${eventId}`, 'event');
log(`data: ${data}`, 'data');
log('', 'system');
document.getElementById('eventCount').textContent = eventCount;
document.getElementById('lastId').textContent = eventId;
}
</script>
</body>
</html>示例2:实时通知系统
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>实时通知系统</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', system-ui, sans-serif; background: #f0f2f5; padding: 20px; color: #333; }
.container { max-width: 800px; margin: 0 auto; }
h1 { text-align: center; color: #1a73e8; margin-bottom: 24px; font-size: 28px; }
.card { background: #fff; border-radius: 12px; padding: 24px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
.card h2 { font-size: 18px; color: #1a73e8; margin-bottom: 16px; padding-bottom: 8px; border-bottom: 2px solid #e8eaed; }
.notification-list { list-style: none; max-height: 500px; overflow-y: auto; }
.notification-item { padding: 14px; border: 1px solid #e8eaed; border-radius: 10px; margin-bottom: 10px; animation: slideIn 0.3s ease; }
@keyframes slideIn { from { opacity:0; transform:translateY(-10px); } to { opacity:1; transform:translateY(0); } }
.notif-header { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
.notif-icon { width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 14px; }
.notif-info { background: #e3f2fd; color: #1565c0; }
.notif-success { background: #e8f5e9; color: #2e7d32; }
.notif-warning { background: #fff3e0; color: #ef6c00; }
.notif-error { background: #ffebee; color: #c62828; }
.notif-title { font-weight: 700; font-size: 14px; flex: 1; }
.notif-time { font-size: 11px; color: #999; }
.notif-body { font-size: 13px; color: #555; line-height: 1.5; }
.controls { display: flex; gap: 8px; margin-bottom: 16px; }
button { padding: 8px 16px; border: none; border-radius: 6px; font-weight: 600; cursor: pointer; font-size: 13px; }
.btn-start { background: #34a853; color: #fff; }
.btn-stop { background: #ea4335; color: #fff; }
.btn-clear { background: #5f6368; color: #fff; }
.empty-state { text-align: center; padding: 40px; color: #bbb; }
</style>
</head>
<body>
<div class="container">
<h1>实时通知系统</h1>
<div class="card">
<h2>通知控制</h2>
<div class="controls">
<button class="btn-start" onclick="startNotifications()">开始接收</button>
<button class="btn-stop" onclick="stopNotifications()">停止</button>
<button class="btn-clear" onclick="clearNotifications()">清空</button>
</div>
</div>
<div class="card">
<h2>通知列表</h2>
<ul class="notification-list" id="notifList">
<li class="empty-state">点击"开始接收"查看实时通知</li>
</ul>
</div>
</div>
<script>
let notifTimer = null;
const notifTemplates = [
{ type: 'info', title: '系统通知', body: '服务器维护将于今晚22:00开始' },
{ type: 'success', title: '操作成功', body: '文件上传完成,共3个文件' },
{ type: 'warning', title: '安全警告', body: '检测到异常登录尝试' },
{ type: 'error', title: '错误提示', body: '数据库连接超时,请稍后重试' },
{ type: 'info', title: '新消息', body: '张三给您发送了一条消息' },
{ type: 'success', title: '订单确认', body: '您的订单#1024已确认' },
{ type: 'warning', title: '存储空间', body: '存储空间使用率已达85%' },
{ type: 'info', title: '版本更新', body: '新版本v2.5.0已发布' }
];
function addNotification(notif) {
const list = document.getElementById('notifList');
const empty = list.querySelector('.empty-state');
if (empty) empty.remove();
const time = new Date().toLocaleTimeString();
const li = document.createElement('li');
li.className = 'notification-item';
li.innerHTML = `
<div class="notif-header">
<div class="notif-icon notif-${notif.type}">${notif.type === 'info' ? 'i' : notif.type === 'success' ? '' : notif.type === 'warning' ? '!' : ''}</div>
<span class="notif-title">${notif.title}</span>
<span class="notif-time">${time}</span>
</div>
<div class="notif-body">${notif.body}</div>
`;
list.insertBefore(li, list.firstChild);
// 限制最多显示20条
while (list.children.length > 20) {
list.removeChild(list.lastChild);
}
}
function startNotifications() {
if (notifTimer) return;
notifTimer = setInterval(() => {
const notif = notifTemplates[Math.floor(Math.random() * notifTemplates.length)];
addNotification(notif);
}, 3000 + Math.random() * 5000);
}
function stopNotifications() {
clearInterval(notifTimer);
notifTimer = null;
}
function clearNotifications() {
document.getElementById('notifList').innerHTML = '<li class="empty-state">通知已清空</li>';
}
</script>
</body>
</html>示例3:SSE与WebSocket对比工具
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SSE与WebSocket对比工具</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', system-ui, sans-serif; background: #f0f2f5; padding: 20px; color: #333; }
.container { max-width: 900px; margin: 0 auto; }
h1 { text-align: center; color: #1a73e8; margin-bottom: 24px; font-size: 28px; }
.card { background: #fff; border-radius: 12px; padding: 24px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
.card h2 { font-size: 18px; color: #1a73e8; margin-bottom: 16px; padding-bottom: 8px; border-bottom: 2px solid #e8eaed; }
.comparison-table { width: 100%; border-collapse: collapse; }
.comparison-table th, .comparison-table td { padding: 12px 14px; text-align: left; border-bottom: 1px solid #e8eaed; font-size: 14px; }
.comparison-table th { background: #f8f9fa; font-weight: 600; color: #555; }
.comparison-table th:first-child { width: 150px; }
.tag { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 12px; font-weight: 600; }
.tag-sse { background: #e3f2fd; color: #1565c0; }
.tag-ws { background: #e8f5e9; color: #2e7d32; }
.tag-both { background: #fff3e0; color: #ef6c00; }
.scenario-cards { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.scenario-card { padding: 16px; border: 2px solid #e8eaed; border-radius: 10px; }
.scenario-card h3 { font-size: 15px; margin-bottom: 8px; }
.scenario-card p { font-size: 13px; color: #666; line-height: 1.6; }
.recommend { font-weight: 700; font-size: 14px; margin-top: 8px; }
.rec-sse { color: #1565c0; }
.rec-ws { color: #2e7d32; }
.rec-both { color: #ef6c00; }
</style>
</head>
<body>
<div class="container">
<h1>SSE与WebSocket对比工具</h1>
<div class="card">
<h2>特性对比</h2>
<table class="comparison-table">
<thead><tr><th>特性</th><th><span class="tag tag-sse">SSE</span></th><th><span class="tag tag-ws">WebSocket</span></th></tr></thead>
<tbody>
<tr><td>通信方向</td><td>服务器→客户端</td><td>双向</td></tr>
<tr><td>底层协议</td><td>HTTP</td><td>ws/wss</td></tr>
<tr><td>数据格式</td><td>文本</td><td>文本 + 二进制</td></tr>
<tr><td>自动重连</td><td>内置</td><td>需手动实现</td></tr>
<tr><td>事件ID</td><td>内置</td><td>无</td></tr>
<tr><td>事件类型</td><td>内置</td><td>需自定义</td></tr>
<tr><td>浏览器API</td><td>EventSource</td><td>WebSocket</td></tr>
<tr><td>代理兼容</td><td>好(基于HTTP)</td><td>可能被阻止</td></tr>
<tr><td>连接开销</td><td>低</td><td>中等</td></tr>
<tr><td>IE支持</td><td>不支持</td><td>支持</td></tr>
<tr><td>实现复杂度</td><td>低</td><td>中</td></tr>
<tr><td>服务端推送</td><td>原生支持</td><td>原生支持</td></tr>
<tr><td>客户端发送</td><td>需另开HTTP</td><td>直接发送</td></tr>
</tbody>
</table>
</div>
<div class="card">
<h2>场景推荐</h2>
<div class="scenario-cards">
<div class="scenario-card">
<h3>实时通知</h3>
<p>服务器向客户端推送通知消息,客户端不需要向服务器发送数据。</p>
<div class="recommend rec-sse">推荐: SSE</div>
</div>
<div class="scenario-card">
<h3>聊天应用</h3>
<p>双向通信,客户端和服务器都需要发送消息。</p>
<div class="recommend rec-ws">推荐: WebSocket</div>
</div>
<div class="scenario-card">
<h3>数据监控</h3>
<p>服务器推送实时数据(股票、传感器等),客户端只接收。</p>
<div class="recommend rec-sse">推荐: SSE</div>
</div>
<div class="scenario-card">
<h3>在线协作</h3>
<p>多人实时编辑,需要双向同步状态。</p>
<div class="recommend rec-ws">推荐: WebSocket</div>
</div>
<div class="scenario-card">
<h3>新闻推送</h3>
<p>服务器推送最新新闻,客户端浏览阅读。</p>
<div class="recommend rec-sse">推荐: SSE</div>
</div>
<div class="scenario-card">
<h3>在线游戏</h3>
<p>高频双向通信,低延迟要求。</p>
<div class="recommend rec-ws">推荐: WebSocket</div>
</div>
</div>
</div>
</div>
</body>
</html>浏览器兼容性
Polyfill:eventsource polyfill库可在IE中使用SSE。
注意事项与最佳实践
1. 连接管理
浏览器对同域SSE连接数有限制(通常6个)
不需要时及时关闭连接
页面隐藏时考虑断开连接
2. 事件格式
确保每个事件以空行结尾
多行data使用多个data字段
JSON数据放在单个data字段中
3. 错误处理
监听onerror事件
检查readyState判断连接状态
实现自定义重连逻辑(如果需要)
4. 安全考虑
使用HTTPS传输敏感数据
验证事件数据格式
实现认证(Cookie或URL参数)
代码规范示例
SSE客户端封装
代码示例
class SSEClient {
constructor(url, options = {}) {
this.url = url;
this.options = { withCredentials: false, ...options };
this.eventSource = null;
this.listeners = new Map();
}
connect() {
this.eventSource = new EventSource(this.url, {
withCredentials: this.options.withCredentials
});
this.eventSource.onopen = () => this._emit('open');
this.eventSource.onerror = (e) => {
if (this.eventSource.readyState === EventSource.CLOSED) {
this._emit('close');
} else {
this._emit('error', e);
}
};
this.eventSource.onmessage = (e) => {
this._emit('message', { data: e.data, id: e.lastEventId });
};
// 注册自定义事件监听
for (const [event] of this.listeners) {
if (event !== 'open' && event !== 'error' && event !== 'close' && event !== 'message') {
this.eventSource.addEventListener(event, (e) => {
this._emit(event, { data: e.data, id: e.lastEventId });
});
}
}
}
on(event, callback) {
if (!this.listeners.has(event)) this.listeners.set(event, []);
this.listeners.get(event).push(callback);
}
off(event, callback) {
if (!this.listeners.has(event)) return;
if (callback) {
const cbs = this.listeners.get(event).filter(cb => cb !== callback);
this.listeners.set(event, cbs);
} else {
this.listeners.delete(event);
}
}
_emit(event, data) {
(this.listeners.get(event) || []).forEach(cb => cb(data));
}
close() {
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
}
}
}
// 使用
const sse = new SSEClient('/api/events');
sse.on('open', () => console.log('SSE已连接'));
sse.on('update', (e) => console.log('更新:', e.data));
sse.on('notification', (e) => console.log('通知:', e.data));
sse.connect();常见问题与解决方案
问题1:IE不支持EventSource
解决方案:使用eventsource polyfill库,或用Fetch API手动解析SSE流。
问题2:连接数限制
解决方案:合并多个SSE端点为一个,使用事件类型区分不同数据。
问题3:SSE连接被代理超时
解决方案:定期发送心跳注释(: heartbeat\n\n),保持连接活跃。
问题4:需要双向通信
解决方案:SSE + Fetch组合,SSE接收服务器推送,Fetch发送客户端请求。
总结
SSE是轻量级的服务器推送方案:
单向推送:服务器→客户端,适合推送和通知场景
简单API:EventSource接口,开箱即用
自动重连:内置重连机制和事件ID续传
事件类型:支持自定义事件类型,便于消息路由
基于HTTP:兼容代理、CDN,部署简单
与WebSocket对比:SSE更简单但只支持单向,WebSocket更强大但更复杂
选择SSE还是WebSocket取决于场景:单向推送选SSE,双向通信选WebSocket。
常见问题
什么是SSE服务器推送?
SSE服务器推送是HTML5开发中的重要技术,本教程详细介绍了其核心概念和实践方法。
如何学习SSE服务器推送的实际应用?
教程中提供了完整的代码示例和实践指导,建议结合示例代码动手练习。
SSE服务器推送有哪些注意事项?
常见注意事项包括兼容性、性能优化等,教程的注意事项与最佳实践部分有详细说明。
SSE服务器推送适合初学者吗?
本教程从基础概念讲起,循序渐进,适合有一定HTML和JavaScript基础的初学者。
SSE服务器推送的核心要点是什么?
核心要点包括教程简介等内容,建议按教程顺序逐步学习。
本文由小确幸生活整理发布,转载请注明出处
本文涉及AI创作
内容由AI创作,请仔细甄别