服务器发送事件(Server-Sent Events,SSE)详解
引言
1.1 SSE 简介
服务器发送事件(Server-Sent Events,SSE)是一种允许服务器向浏览器推送实时更新的技术。与 WebSocket 不同,SSE 是单向的,即服务器向客户端推送数据。SSE 基于 HTTP 协议,实现简单且易于使用。
1.2 使用场景
- 实时通知:如股票行情、新闻更新等。
- 实时数据流:如日志监控、实时数据分析等。
- 进度更新:如文件上传进度、任务进度等。
SSE 架构设计
2.1 核心组件
- 服务器:负责生成和发送事件流。
- 客户端:通过
EventSource
对象接收事件流。
2.2 数据格式
SSE 使用纯文本格式,每个事件包含一个或多个字段,常见的字段包括 data
、event
、id
和 retry
。
2.3 连接管理
- 持久连接:服务器保持与客户端的连接,持续发送事件。
- 自动重连:客户端在连接断开后自动重连。
实现方法
3.1 服务器端实现
3.1.1 设置响应头
服务器需要设置 Content-Type
为 text/event-stream
,并保持连接打开。
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
3.1.2 发送事件
服务器通过发送格式化的文本流来推送事件。
data: Hello, SSE!
data: This is a second event.
event: customEvent
id: 123
retry: 1000
示例代码(Node.js + Express):
const express = require('express');
const app = express();
const PORT = 3000;
app.get('/sse', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// 发送初始事件
res.write('data: Hello, SSE!\n\n');
// 定期发送事件
const intervalId = setInterval(() => {
const event = {
data: 'This is a second event',
event: 'customEvent',
id: '123',
retry: 1000
};
res.write(`data: ${event.data}\n`);
res.write(`event: ${event.event}\n`);
res.write(`id: ${event.id}\n`);
res.write(`retry: ${event.retry}\n\n`);
}, 2000);
// 处理客户端关闭连接
req.on('close', () => {
clearInterval(intervalId);
res.end();
});
});
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
3.2 客户端实现
3.2.1 创建 EventSource 对象
客户端使用 EventSource
对象连接到服务器。
const eventSource = new EventSource('http://example.com/sse');
3.2.2 处理事件
客户端可以监听不同的事件类型。
eventSource.onmessage = function(event) {
console.log('New message:', event.data);
};
eventSource.addEventListener('customEvent', function(event) {
console.log('Custom event:', event.data);
});
3.2.3 错误处理
客户端可以监听错误事件。
eventSource.onerror = function(err) {
console.error('EventSource failed:', err);
};
示例代码(HTML + JavaScript):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SSE Example</title>
</head>
<body>
<h1>Server-Sent Events Example</h1>
<div id="messages"></div>
<script>
const eventSource = new EventSource('http://localhost:3000/sse');
eventSource.onmessage = function(event) {
const messagesDiv = document.getElementById('messages');
const newMessage = document.createElement('div');
newMessage.textContent = 'New message: ' + event.data;
messagesDiv.appendChild(newMessage);
};
eventSource.addEventListener('customEvent', function(event) {
const messagesDiv = document.getElementById('messages');
const newMessage = document.createElement('div');
newMessage.textContent = 'Custom event: ' + event.data;
messagesDiv.appendChild(newMessage);
});
eventSource.onerror = function(err) {
console.error('EventSource failed:', err);
};
</script>
</body>
</html>
实际应用案例
4.1 实时通知系统
- 场景:股票行情、新闻更新等。
- 实现:服务器定期推送最新的股票价格或新闻内容到客户端。
示例代码(Node.js + Express):
const express = require('express');
const app = express();
const PORT = 3000;
app.get('/sse/stock', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// 模拟股票行情更新
const intervalId = setInterval(() => {
const stockData = {
symbol: 'AAPL',
price: (Math.random() * 100).toFixed(2)
};
res.write(`data: ${JSON.stringify(stockData)}\n\n`);
}, 5000);
req.on('close', () => {
clearInterval(intervalId);
res.end();
});
});
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
客户端代码(HTML + JavaScript):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Stock Price SSE</title>
</head>
<body>
<h1>Stock Price Updates</h1>
<div id="stock"></div>
<script>
const eventSource = new EventSource('http://localhost:3000/sse/stock');
eventSource.onmessage = function(event) {
const stockDiv = document.getElementById('stock');
const stockData = JSON.parse(event.data);
stockDiv.textContent = `Symbol: ${stockData.symbol}, Price: $${stockData.price}`;
};
eventSource.onerror = function(err) {
console.error('EventSource failed:', err);
};
</script>
</body>
</html>
4.2 实时数据监控
- 场景:日志监控、实时数据分析等。
- 实现:服务器实时推送日志数据或分析结果到客户端。
示例代码(Node.js + Express):
const express = require('express');
const app = express();
const PORT = 3000;
app.get('/sse/logs', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// 模拟日志数据
const intervalId = setInterval(() => {
const logData = {
timestamp: new Date().toISOString(),
message: `Log entry ${Math.floor(Math.random() * 100)}`
};
res.write(`data: ${JSON.stringify(logData)}\n\n`);
}, 3000);
req.on('close', () => {
clearInterval(intervalId);
res.end();
});
});
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
客户端代码(HTML + JavaScript):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Log Monitoring SSE</title>
</head>
<body>
<h1>Log Monitoring</h1>
<div id="logs"></div>
<script>
const eventSource = new EventSource('http://localhost:3000/sse/logs');
eventSource.onmessage = function(event) {
const logsDiv = document.getElementById('logs');
const logData = JSON.parse(event.data);
const newLog = document.createElement('div');
newLog.textContent = `Timestamp: ${logData.timestamp}, Message: ${logData.message}`;
logsDiv.appendChild(newLog);
};
eventSource.onerror = function(err) {
console.error('EventSource failed:', err);
};
</script>
</body>
</html>
4.3 进度更新
- 场景:文件上传进度、任务进度等。
- 实现:服务器在文件上传或任务执行过程中,定期推送进度信息到客户端。
示例代码(Node.js + Express):
const express = require('express');
const app = express();
const PORT = 3000;
app.get('/sse/progress', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
let progress = 0;
const intervalId = setInterval(() => {
progress += 10;
if (progress > 100) {
clearInterval(intervalId);
res.write('data: { "progress": 100, "status": "completed" }\n\n');
res.end();
} else {
res.write(`data: { "progress": ${progress}, "status": "in progress" }\n\n`);
}
}, 1000);
req.on('close', () => {
clearInterval(intervalId);
res.end();
});
});
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
客户端代码(HTML + JavaScript):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Progress Update SSE</title>
</head>
<body>
<h1>Progress Update</h1>
<div id="progress"></div>
<script>
const eventSource = new EventSource('http://localhost:3000/sse/progress');
eventSource.onmessage = function(event) {
const progressDiv = document.getElementById('progress');
const progressData = JSON.parse(event.data);
progressDiv.textContent = `Progress: ${progressData.progress}%, Status: ${progressData.status}`;
};
eventSource.onerror = function(err) {
console.error('EventSource failed:', err);
};
</script>
</body>
</html>
优缺点
5.1 优点
- 简单易用:基于 HTTP 协议,实现简单。
- 单向通信:服务器向客户端单向推送数据,适合实时通知场景。
- 浏览器支持:现代浏览器普遍支持
EventSource
。
5.2 缺点
- 单向通信:不支持客户端向服务器发送数据。
- 跨域限制:需要处理跨域请求。
- 浏览器兼容性:部分旧版浏览器不支持
EventSource
。
性能优化与最佳实践
6.1 性能优化
6.1.1 压缩传输
使用 Gzip 压缩数据,减少传输量。
server {
gzip on;
gzip_types text/event-stream;
}
6.1.2 连接管理
合理管理连接,避免频繁重连。
const eventSource = new EventSource('http://example.com/sse');
eventSource.onerror = function(err) {
console.error('EventSource failed:', err);
setTimeout(() => {
eventSource.close();
const newEventSource = new EventSource('http://example.com/sse');
// 重新绑定事件处理程序
}, 5000);
};
6.1.3 事件格式
优化事件格式,减少不必要的数据传输。
data: {"type":"stock","symbol":"AAPL","price":150.75}
6.2 最佳实践
6.2.1 错误处理
实现完善的错误处理机制,确保连接的稳定性。
eventSource.onerror = function(err) {
console.error('EventSource failed:', err);
eventSource.close();
setTimeout(() => {
const newEventSource = new EventSource('http://example.com/sse');
// 重新绑定事件处理程序
}, 5000);
};
6.2.2 跨域支持
配置 CORS(跨域资源共享)以支持跨域请求。
const express = require('express');
const cors = require('cors');
const app = express();
const PORT = 3000;
app.use(cors());
app.get('/sse', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.write('data: Hello, SSE!\n\n');
const intervalId = setInterval(() => {
const event = {
data: 'This is a second event',
event: 'customEvent',
id: '123',
retry: 1000
};
res.write(`data: ${event.data}\n`);
res.write(`event: ${event.event}\n`);
res.write(`id: ${event.id}\n`);
res.write(`retry: ${event.retry}\n\n`);
}, 2000);
req.on('close', () => {
clearInterval(intervalId);
res.end();
});
});
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
6.2.3 事件命名
使用有意义的事件名称,便于客户端处理。
event: stockUpdate
data: {"symbol":"AAPL","price":150.75}
总结
服务器发送事件(SSE)是一种简单且有效的技术,适用于需要从服务器向客户端推送实时更新的场景。通过本文的介绍,读者可以了解 SSE 的工作原理、使用场景、实现方法以及实际应用案例,从而更好地利用 SSE 提升应用的实时性和用户体验。