简单来说:SSE 是一种基于 HTTP 的技术标准,用于实现服务器向客户端的单向实时通信。
下面我们分点详细解释:
1. 什么是 SSE?
SSE 的全称是 Server-Sent Events(服务器发送事件)。
-
目的 :它的主要目的是允许服务器在任何时候主动向客户端(通常是浏览器)推送数据。
-
特性 :它是一种单向通信通道。数据流只能从服务器流向客户端。客户端无法通过这个连接向服务器发送数据(除了最初的连接请求)。
-
协议 :它是一个Web API ,在浏览器端通过 JavaScript 的
EventSource
接口来实现。同时,它也是一种简单的、基于文本的数据格式协议。 -
本质 :SSE 本质上是对 HTTP 协议的一种创新使用,它没有创造一个新的协议,而是充分利用了 HTTP 的长连接和流式传输特性。
2. SSE 和 HTTP 的关系
SSE 与 HTTP 的关系可以概括为:SSE 构建于标准 HTTP 之上,是 HTTP 的一种特定用法。
具体体现在以下几个方面:
a. 基于 HTTP 协议
SSE 连接是通过发起一个普通的 HTTP GET 请求 来建立的。客户端(浏览器)使用 EventSource
API 向服务器的一个特定 URL 发送请求。这个请求看起来和任何其他网页、图片或 API 请求没有任何区别。
b. 使用标准的 HTTP 头
为了启动一个 SSE 连接,服务器在响应中必须设置一个特殊的 HTTP 头:
Content-Type: text/event-stream
这个头信息告诉客户端:"接下来的响应体不是一个一次性返回的完整文档,而是一个遵循 SSE 格式的事件流。" 一旦设置了这个头,连接就会保持打开状态。
c. 长连接(Long-Lived Connection)
与传统 HTTP 的"请求-响应-断开"模式不同,SSE 要求服务器保持这个 HTTP 连接处于打开状态。这样,服务器就可以通过这个持久的连接,连续地、多次地发送数据片段,而不是在发送一次响应后立即关闭连接。
d. 数据格式是纯文本流
通过这个保持打开的 HTTP 连接,服务器发送的数据遵循一个简单的文本格式。每条消息由几个字段组成,最常见的是 data
和 event
。
示例:
服务器发送的数据可能看起来像这样:
text
event: message
data: 这是一条普通消息
data: 这是一条多行的
data: 消息内容
event: update
data: {"userId": 123, "status": "online"}
客户端上的 EventSource
会解析这个流,并根据 event
字段触发相应的事件处理函数。
3. 与类似技术(如 WebSocket)的对比
为了更好地理解 SSE,通常会把它和 WebSocket 进行比较:
特性 | Server-Sent Events (SSE) | WebSocket |
---|---|---|
通信方向 | 单向(服务器 -> 客户端) | 双向(全双工通信) |
协议 | 基于 HTTP | 独立的协议 (ws:// 或 wss:// ) |
协议开销 | 非常轻量,使用纯文本格式 | 有独立的帧结构,比 SSE 略复杂 |
自动重连 | 内置支持,客户端自动处理 | 需要手动实现 |
适用场景 | 实时通知、新闻推送、状态更新、仪表盘 | 聊天应用、在线游戏、协同编辑等需要高频双向通信的场景 |
浏览器支持 | 良好(除 IE 外) | 非常好(包括现代 IE) |
总结
-
关系 :SSE 不是 一个独立于 HTTP 的协议,而是 HTTP 协议的一种高级应用形式。它利用一个长时间保持打开的 HTTP 连接,来实现服务器到客户端的单向数据流。
-
核心 :SSE 的核心在于
Content-Type: text/event-stream
这个 HTTP 响应头和特定的文本数据格式。 -
优势:它非常简单易用,尤其适用于那些只需要服务器向客户端推送数据的场景(如实时新闻feed、股票价格更新、服务器任务进度通知等),并且享受 HTTP 的所有好处,如身份验证、安全性(HTTPS)和兼容现有的基础设施。
因此,当你使用 SSE 时,你实际上是在以一种更高效、更实时的方式使用 HTTP。
SSE 如何实现服务器推送
SSE 只使用 HTTP GET 方法 建立连接,之后的所有数据推送都是通过保持这个GET连接开放来实现的,不需要使用POST或其他HTTP方法。
详细解释
1. 连接建立阶段
-
客户端使用 GET 请求发起SSE连接
-
服务器响应时设置特殊头部,表明这是一个事件流
-
服务器不关闭这个连接,而是保持它开放
2. 数据传输阶段
-
服务器通过这个保持开放的连接多次写入数据
-
数据格式遵循SSE规范:
data: 内容\n\n
-
整个过程都在同一个GET连接中进行
3. 连接终止
-
服务器或客户端可以关闭连接
-
客户端自动尝试重新连接(使用新的GET请求)
完整示例
前端代码 (HTML + JavaScript)
html
<!DOCTYPE html>
<html>
<head>
<title>SSE HTTP 方法演示</title>
<style>
body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
.container { border: 1px solid #ddd; padding: 20px; border-radius: 5px; margin-top: 20px; }
.request-info { background-color: #f8f9fa; padding: 15px; border-radius: 5px; margin-bottom: 20px; }
.connection-status { padding: 10px; border-radius: 4px; margin-bottom: 15px; }
.connected { background-color: #d4edda; color: #155724; }
.disconnected { background-color: #f8d7da; color: #721c24; }
.event-log { height: 300px; overflow-y: auto; border: 1px solid #ddd; padding: 10px; }
.event { margin-bottom: 8px; padding: 5px; border-bottom: 1px solid #eee; }
.get-badge { background-color: #007bff; color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px; }
</style>
</head>
<body>
<h1>SSE HTTP 方法演示</h1>
<p>此演示展示SSE如何仅使用HTTP GET方法实现服务器向客户端推送数据。</p>
<div class="request-info">
<h3>HTTP 请求信息</h3>
<p>方法: <span class="get-badge">GET</span></p>
<p>端点: <code>/sse-stream</code></p>
<p>响应头: <code>Content-Type: text/event-stream</code></p>
</div>
<div class="container">
<div id="status" class="connection-status disconnected">
状态: 未连接
</div>
<div>
<button οnclick="connectSSE()">建立SSE连接</button>
<button οnclick="disconnectSSE()">断开连接</button>
<button οnclick="sendMessage()" id="send-btn" disabled>发送消息到服务器</button>
</div>
<h3>事件日志:</h3>
<div id="event-log" class="event-log"></div>
</div>
<script>
let eventSource = null;
let messageCount = 0;
function connectSSE() {
addLog('系统', '发起 GET 请求到 /sse-stream');
// 创建 SSE 连接 - 使用GET方法
eventSource = new EventSource('/sse-stream');
eventSource.onopen = function() {
updateStatus('connected', '状态: 已连接 (GET请求已建立)');
addLog('系统', 'GET连接已建立,等待服务器推送数据...');
document.getElementById('send-btn').disabled = false;
};
eventSource.onmessage = function(event) {
messageCount++;
const data = JSON.parse(event.data);
addLog('服务器推送', `序号: ${messageCount}, 时间: ${data.time}, 消息: "${data.message}"`);
};
eventSource.onerror = function() {
updateStatus('disconnected', '状态: 连接错误');
addLog('错误', '连接发生错误');
document.getElementById('send-btn').disabled = true;
};
}
function disconnectSSE() {
if (eventSource) {
eventSource.close();
eventSource = null;
updateStatus('disconnected', '状态: 已断开');
addLog('系统', 'GET连接已关闭');
document.getElementById('send-btn').disabled = true;
}
}
function sendMessage() {
// 注意:这不是通过SSE发送的,SSE是单向的
// 这里使用单独的POST请求模拟向服务器发送消息
fetch('/api/message', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ message: '客户端消息' })
})
.then(response => response.json())
.then(data => {
addLog('客户端发送', '使用POST方法发送消息到服务器');
})
.catch(error => {
addLog('错误', '发送消息失败: ' + error);
});
}
function updateStatus(status, message) {
const statusElem = document.getElementById('status');
statusElem.textContent = message;
statusElem.className = `connection-status ${status}`;
}
function addLog(type, message) {
const logElem = document.getElementById('event-log');
const time = new Date().toLocaleTimeString();
logElem.innerHTML += `<div class="event"><b>[${time}] ${type}:</b> ${message}</div>`;
logElem.scrollTop = logElem.scrollHeight;
}
</script>
</body>
</html>
后端代码 (Node.js + Express)
javascript
const express = require('express');
const app = express();
const port = 3000;
// 中间件
app.use(express.json());
app.use(express.static('public')); // 提供静态文件
// 允许跨域
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
next();
});
// 存储所有活动的SSE连接
const clients = new Map();
// SSE端点 - 只使用GET方法
app.get('/sse-stream', (req, res) => {
console.log('新的SSE连接建立 - 方法:', req.method);
// 设置SSE必需的头部
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('Access-Control-Allow-Origin', '*');
// 生成客户端ID
const clientId = Date.now();
clients.set(clientId, res);
console.log(`客户端 ${clientId} 已连接,当前连接数: ${clients.size}`);
// 发送欢迎消息
res.write(`data: ${JSON.stringify({
time: new Date().toISOString(),
message: "欢迎! 连接已通过GET方法建立",
clientId: clientId
})}\n\n`);
let messageCount = 0;
// 定期发送消息
const intervalId = setInterval(() => {
messageCount++;
const data = {
time: new Date().toISOString(),
message: `这是服务器推送的消息 #${messageCount}`,
clientId: clientId
};
// 通过保持开放的GET连接发送数据
res.write(`data: ${JSON.stringify(data)}\n\n`);
// 10秒后发送特殊消息
if (messageCount === 5) {
res.write(`data: ${JSON.stringify({
time: new Date().toISOString(),
message: "注意: 所有数据都是通过同一个GET连接推送的!",
clientId: clientId
})}\n\n`);
}
}, 2000);
// 当客户端关闭连接时清理资源
req.on('close', () => {
console.log(`客户端 ${clientId} 断开连接`);
clearInterval(intervalId);
clients.delete(clientId);
console.log(`当前连接数: ${clients.size}`);
});
});
// 普通API端点 - 使用POST方法接收客户端消息
app.post('/api/message', (req, res) => {
console.log('收到客户端消息 - 方法:', req.method);
console.log('消息内容:', req.body);
res.json({
status: 'success',
message: '消息已接收',
receivedAt: new Date().toISOString()
});
});
app.listen(port, () => {
console.log(`SSE演示服务器运行在 http://localhost:${port}`);
console.log('前端页面: http://localhost:3000/index.html');
});
关键理解点
-
SSE只使用GET方法:连接建立后,所有数据推送都通过这个保持开放的GET连接进行
-
单向通信:SSE设计为服务器到客户端的单向通信
- 如果需要客户端向服务器发送数据,需要使用单独的HTTP请求(如POST)
-
连接保持:服务器通过不结束响应来保持连接开放,从而能够多次写入数据
-
与WebSocket的区别:
-
WebSocket是双向通信,使用自己的协议(ws://)
-
SSE是单向通信,基于标准HTTP
-
运行说明
-
创建项目文件夹,添加两个文件:
-
index.html
(前端页面) -
server.js
(Node.js服务器)
-
-
安装Express:
bash
npm install express
-
启动服务器:
bash
node server.js
-
在浏览器中访问
http://localhost:3000/index.html
这个示例清晰地展示了SSE如何仅使用HTTP GET方法实现服务器向客户端的实时数据推送。