流式响应SSE方案EventSource
目录
SSE基础概念
Server-Sent Events (SSE) 是HTML5规范的一部分,它允许服务器主动向客户端推送数据。SSE基于HTTP协议,通过保持一个长连接来实现服务端到客户端的单向数据流传输。
核心特性
- 单向通信:仅支持服务端向客户端推送数据
- 基于HTTP:使用标准HTTP协议,无需特殊协议支持
- 自动重连:客户端断线后会自动尝试重连
- 事件驱动:支持自定义事件类型
- 轻量级:相比WebSocket更加轻量,适合单向推送场景
技术原理
SSE基于HTTP的text/event-stream媒体类型,服务器通过特定格式发送数据流。数据格式遵循以下规范:
// SSE数据格式
data: 消息内容\n
event: 事件类型\n
id: 消息ID\n
retry: 重连时间\n
\n
SSE消息结构
graph TD
A[SSE消息] --> B[data: 消息数据]
A --> C[event: 事件类型]
A --> D[id: 消息标识]
A --> E[retry: 重连间隔]
A --> F[comment: 注释]
style A fill:#e1f5fe
style B fill:#f3e5f5
style C fill:#f3e5f5
style D fill:#f3e5f5
style E fill:#f3e5f5
style F fill:#f3e5f5
EventSource API
EventSource是浏览器提供的JavaScript API,用于建立和管理SSE连接。
基本用法
// 创建EventSource连接
const eventSource = new EventSource('/api/events');
// 监听默认消息
eventSource.onmessage = function(event) {
console.log('收到消息:', event.data);
const data = JSON.parse(event.data);
updateUI(data);
};
// 监听自定义事件
eventSource.addEventListener('notification', function(event) {
console.log('通知事件:', event.data);
showNotification(JSON.parse(event.data));
});
// 监听连接状态
eventSource.onopen = function() {
console.log('SSE连接已建立');
};
eventSource.onerror = function(error) {
console.error('SSE连接错误:', error);
if (eventSource.readyState === EventSource.CLOSED) {
console.log('SSE连接已关闭');
}
};
// 手动关闭连接
// eventSource.close();
连接状态管理
class SSEManager {
constructor(url, options = {}) {
this.url = url;
this.options = options;
this.eventSource = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = options.maxReconnectAttempts || 5;
}
connect() {
try {
this.eventSource = new EventSource(this.url);
this.setupEventListeners();
this.reconnectAttempts = 0;
} catch (error) {
console.error('创建EventSource失败:', error);
this.handleReconnect();
}
}
setupEventListeners() {
this.eventSource.onopen = () => {
console.log('SSE连接成功');
this.reconnectAttempts = 0;
};
this.eventSource.onmessage = (event) => {
this.handleMessage(event);
};
this.eventSource.onerror = (error) => {
console.error('SSE错误:', error);
this.handleReconnect();
};
}
handleMessage(event) {
try {
const data = JSON.parse(event.data);
this.onMessage(data);
} catch (error) {
console.error('解析消息失败:', error);
}
}
handleReconnect() {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
const delay = Math.pow(2, this.reconnectAttempts) * 1000;
console.log(`${delay}ms后重连,第${this.reconnectAttempts}次尝试`);
setTimeout(() => {
this.connect();
}, delay);
} else {
console.error('达到最大重连次数,停止重连');
}
}
onMessage(data) {
// 由子类或外部实现
console.log('收到消息:', data);
}
close() {
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
}
}
}
服务端实现
Node.js实现
const express = require('express');
const app = express();
// SSE端点
app.get('/api/events', (req, res) => {
// 设置SSE响应头
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Cache-Control'
});
// 发送初始连接消息
res.write('data: {"type":"connected","timestamp":' + Date.now() + '}\n\n');
// 模拟定时推送数据
const intervalId = setInterval(() => {
const data = {
id: Date.now(),
message: '实时数据',
timestamp: new Date().toISOString()
};
res.write(`data: ${JSON.stringify(data)}\n\n`);
}, 5000);
// 客户端断开连接时清理资源
req.on('close', () => {
console.log('客户端断开连接');
clearInterval(intervalId);
res.end();
});
req.on('aborted', () => {
console.log('请求被中止');
clearInterval(intervalId);
res.end();
});
});
app.listen(3000, () => {
console.log('SSE服务启动在端口3000');
});
高级服务端实现
class SSEController {
constructor() {
this.clients = new Map();
this.messageQueue = [];
}
// 处理SSE连接
handleConnection(req, res) {
const clientId = this.generateClientId();
// 设置响应头
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*'
});
// 注册客户端
this.clients.set(clientId, { res, connectedAt: Date.now() });
// 发送连接确认
this.sendToClient(clientId, {
type: 'connected',
clientId,
timestamp: Date.now()
});
// 发送历史消息
this.sendQueuedMessages(clientId);
// 处理断开连接
req.on('close', () => {
this.clients.delete(clientId);
console.log(`客户端 ${clientId} 断开连接`);
});
return clientId;
}
// 发送消息给指定客户端
sendToClient(clientId, data, eventType = 'message') {
const client = this.clients.get(clientId);
if (client) {
const message = this.formatMessage(data, eventType);
client.res.write(message);
}
}
// 广播消息给所有客户端
broadcast(data, eventType = 'message') {
const message = this.formatMessage(data, eventType);
this.clients.forEach((client) => {
client.res.write(message);
});
// 保存到消息队列
this.messageQueue.push({ data, eventType, timestamp: Date.now() });
if (this.messageQueue.length > 100) {
this.messageQueue.shift();
}
}
// 格式化SSE消息
formatMessage(data, eventType, id = null) {
let message = '';
if (eventType !== 'message') {
message += `event: ${eventType}\n`;
}
if (id) {
message += `id: ${id}\n`;
}
message += `data: ${JSON.stringify(data)}\n\n`;
return message;
}
// 发送队列中的消息
sendQueuedMessages(clientId) {
this.messageQueue.forEach(({ data, eventType }) => {
this.sendToClient(clientId, data, eventType);
});
}
generateClientId() {
return `client_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
getClientCount() {
return this.clients.size;
}
}
通信流程
SSE连接建立流程
sequenceDiagram
participant C as Client
participant S as Server
C->>S: GET /events (Accept: text/event-stream)
S->>C: 200 OK (Content-Type: text/event-stream)
Note over S,C: 建立长连接
S->>C: data: 连接成功消息
loop 实时数据推送
S->>C: data: 实时数据
Note over C: 处理接收到的数据
end
alt 连接异常
C->>S: 自动重连
S->>C: 重新建立连接
else 主动断开
C->>S: 关闭连接
Note over S: 清理客户端资源
end
数据传输流程
flowchart TD
A[服务端产生数据] --> B[格式化SSE消息]
B --> C[发送到客户端]
C --> D[客户端接收]
D --> E{解析消息}
E -->|成功| F[触发onmessage事件]
E -->|失败| G[触发onerror事件]
F --> H[更新UI]
G --> I[错误处理]
I --> J{是否重连}
J -->|是| K[等待重连]
J -->|否| L[连接关闭]
K --> A
style A fill:#e3f2fd
style F fill:#e8f5e8
style G fill:#ffebee
应用场景
1. 实时通知系统
// 实时通知管理器
class NotificationManager extends SSEManager {
constructor() {
super('/api/notifications');
}
onMessage(data) {
switch (data.type) {
case 'system':
this.showSystemNotification(data);
break;
case 'user':
this.showUserNotification(data);
break;
case 'alert':
this.showAlertNotification(data);
break;
}
}
showSystemNotification(data) {
// 系统通知处理逻辑
console.log('系统通知:', data.message);
}
}
const notificationManager = new NotificationManager();
notificationManager.connect();
2. 实时数据监控
// 监控数据展示
class MonitoringDashboard {
constructor() {
this.sseManager = new SSEManager('/api/monitoring');
this.sseManager.onMessage = this.handleMonitoringData.bind(this);
this.charts = {};
}
handleMonitoringData(data) {
switch (data.metric) {
case 'cpu':
this.updateCPUChart(data.value);
break;
case 'memory':
this.updateMemoryChart(data.value);
break;
case 'traffic':
this.updateTrafficChart(data.value);
break;
}
}
updateCPUChart(value) {
// 更新CPU图表
if (this.charts.cpu) {
this.charts.cpu.addData(value);
}
}
}
3. 实时聊天系统
// 聊天室实现
class ChatRoom {
constructor(roomId) {
this.roomId = roomId;
this.sseManager = new SSEManager(`/api/chat/${roomId}/events`);
this.sseManager.onMessage = this.handleChatMessage.bind(this);
}
handleChatMessage(data) {
switch (data.type) {
case 'message':
this.displayMessage(data);
break;
case 'user_joined':
this.displayUserJoined(data);
break;
case 'user_left':
this.displayUserLeft(data);
break;
case 'typing':
this.displayTypingIndicator(data);
break;
}
}
displayMessage(data) {
const messageElement = document.createElement('div');
messageElement.innerHTML = `
<div class="message">
<span class="user">${data.user}</span>
<span class="content">${data.content}</span>
<span class="time">${new Date(data.timestamp).toLocaleTimeString()}</span>
</div>
`;
document.getElementById('chat-messages').appendChild(messageElement);
}
}
最佳实践
1. 错误处理和重连机制
class RobustSSEManager extends SSEManager {
constructor(url, options = {}) {
super(url, {
maxReconnectAttempts: 10,
reconnectInterval: 1000,
heartbeatInterval: 30000,
...options
});
this.setupHeartbeat();
}
setupHeartbeat() {
// 定期发送心跳检测
this.heartbeatTimer = setInterval(() => {
if (this.eventSource && this.eventSource.readyState === EventSource.OPEN) {
// 可以通过发送特殊请求来检测连接状态
this.sendHeartbeat();
}
}, this.options.heartbeatInterval);
}
sendHeartbeat() {
// 实现心跳检测逻辑
fetch('/api/heartbeat', { method: 'POST' })
.catch(() => {
// 心跳失败,可能需要重连
this.handleReconnect();
});
}
close() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
}
super.close();
}
}
2. 内存管理和性能优化
// 服务端连接管理
class SSEConnectionManager {
constructor() {
this.connections = new Map();
this.maxConnections = 1000;
this.cleanupInterval = 300000; // 5分钟
this.startCleanupTimer();
}
addConnection(clientId, connection) {
// 检查连接数限制
if (this.connections.size >= this.maxConnections) {
this.removeOldestConnection();
}
this.connections.set(clientId, {
...connection,
lastActivity: Date.now()
});
}
removeOldestConnection() {
let oldestClient = null;
let oldestTime = Date.now();
for (const [clientId, conn] of this.connections) {
if (conn.connectedAt < oldestTime) {
oldestTime = conn.connectedAt;
oldestClient = clientId;
}
}
if (oldestClient) {
this.removeConnection(oldestClient);
}
}
startCleanupTimer() {
setInterval(() => {
const now = Date.now();
const timeout = 600000; // 10分钟无活动超时
for (const [clientId, conn] of this.connections) {
if (now - conn.lastActivity > timeout) {
this.removeConnection(clientId);
}
}
}, this.cleanupInterval);
}
removeConnection(clientId) {
const conn = this.connections.get(clientId);
if (conn) {
conn.res.end();
this.connections.delete(clientId);
}
}
}
3. 安全考虑
// 带认证的SSE连接
app.get('/api/secure-events', authenticateToken, (req, res) => {
// 验证用户权限
const user = req.user;
if (!hasSSEPermission(user)) {
return res.status(403).json({ error: '无权限访问SSE' });
}
// 设置安全响应头
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Connection': 'keep-alive',
'X-Content-Type-Options': 'nosniff',
'Access-Control-Allow-Origin': process.env.ALLOWED_ORIGIN || 'localhost:3000'
});
// 注册用户特定的SSE连接
const clientId = sseController.handleConnection(req, res, user);
// 记录连接日志
logger.info(`用户 ${user.id} 建立SSE连接,客户端ID: ${clientId}`);
});
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.sendStatus(401);
}
jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, user) => {
if (err) return res.sendStatus(403);
req.user = user;
next();
});
}
与其他方案对比
SSE vs WebSocket vs 轮询
graph TD
A[实时通信需求] --> B{通信方向}
B -->|单向推送| C[SSE]
B -->|双向通信| D[WebSocket]
B -->|简单查询| E[轮询]
C --> F[优势:简单实现<br/>自动重连<br/>基于HTTP]
D --> G[优势:全双工<br/>低延迟<br/>支持二进制]
E --> H[优势:实现简单<br/>兼容性好<br/>调试容易]
F --> I[适用:通知推送<br/>实时监控<br/>单向数据流]
G --> J[适用:聊天应用<br/>游戏<br/>协作编辑]
H --> K[适用:数据更新频率低<br/>对实时性要求不高]
style C fill:#e8f5e8
style D fill:#e3f2fd
style E fill:#fff3e0
技术对比表
| 特性 | SSE | WebSocket | 轮询 |
|---|---|---|---|
| 通信方向 | 单向(服务端→客户端) | 双向 | 双向 |
| 协议基础 | HTTP | WebSocket协议 | HTTP |
| 连接开销 | 低 | 中等 | 高 |
| 实时性 | 高 | 最高 | 低 |
| 实现复杂度 | 简单 | 中等 | 最简单 |
| 自动重连 | 内置支持 | 需要手动实现 | 不适用 |
| 防火墙友好 | 是 | 可能有问题 | 是 |
| 浏览器支持 | 现代浏览器 | 现代浏览器 | 全部浏览器 |
总结
SSE作为一种轻量级的实时通信解决方案,在以下场景中表现出色:
核心优势
- 简单易用:基于标准HTTP协议,实现简单
- 自动重连:内置断线重连机制,提高可靠性
- 事件驱动:支持多种事件类型,灵活性强
- 性能优秀:相比轮询大幅减少不必要的请求
- 兼容性好:现代浏览器原生支持,无需额外依赖
适用场景
- 实时通知推送:系统通知、用户消息等
- 数据监控面板:实时图表、状态监控等
- 内容更新推送:新闻推送、评论更新等
- 进度追踪:文件上传、任务处理进度等
使用建议
- 单向推送场景优选:当只需要服务端向客户端推送数据时,SSE是最佳选择
- 注意连接管理:服务端需要合理管理连接数量和资源清理
- 实现错误处理:客户端应实现完善的错误处理和重连机制
- 考虑安全性:对敏感数据传输要实现适当的认证和授权
- 性能监控:监控连接数量、消息发送频率等关键指标
通过合理运用SSE技术,可以为Web应用提供高效、稳定的实时数据推送能力,显著提升用户体验。