简单来说: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方法实现服务器向客户端的实时数据推送。