在实际开发中,我们经常会遇到只需要服务器单向给客户端发消息的场景,比如实时行情推送、系统通知、日志流展示等。这类需求看似简单,但选错技术方案往往会让系统变得复杂又昂贵。今天我们就来聊聊两个常用的推送技术——WebSocket 和 Server-Sent Events(简称 SSE),看看在单向推送这种特定场景下,到底该怎么选。
一、两个家伙到底长什么样
1.1 先看 Server-Sent Events(SSE)
SSE 是 HTML5 标准的一部分,它让服务器可以向客户端持续发送事件流。客户端只需要通过一个普通的 HTTP 连接,然后监听事件就行。整个过程是单向的:服务器推,客户端收,客户端不需要发任何额外消息。
让我们用 Node.js 和浏览器前端写个完整例子。
// 文件名:sse-server.js
// 技术栈:Node.js + 原生 HTTP
const http = require('http');
const server = http.createServer((req, res) => {
// 只处理路径为 /events 的请求
if (req.url === '/events') {
// 设置 SSE 必需的响应头
res.writeHead(200, {
'Content-Type': 'text/event-stream', // 告诉浏览器这是事件流
'Cache-Control': 'no-cache', // 不让浏览器缓存
'Connection': 'keep-alive', // 保持长连接
});
// 每 2 秒发一条消息
const timer = setInterval(() => {
const data = { time: new Date().toISOString(), value: Math.random() };
// SSE 格式:以 "data: " 开头,后面跟 JSON 字符串,最后两个换行
res.write(`data: ${JSON.stringify(data)}\n\n`);
}, 2000);
// 客户端断开时清理定时器
req.on('close', () => {
clearInterval(timer);
console.log('客户端断开了连接');
});
} else {
res.writeHead(404);
res.end();
}
});
server.listen(3000, () => {
console.log('SSE 服务器运行在 http://localhost:3000');
});
前端代码更简单,直接用浏览器自带的 EventSource 对象:
<!-- 文件名:sse-client.html -->
<!DOCTYPE html>
<html>
<head>
<title>SSE 示例</title>
</head>
<body>
<h2>服务器推送的行情数据:</h2>
<ul id="messages"></ul>
<script>
// 技术栈:浏览器原生 JavaScript
// 创建 EventSource 连接到后端
const source = new EventSource('http://localhost:3000/events');
// 监听消息事件,收到数据后把内容显示在页面上
source.onmessage = function(event) {
const data = JSON.parse(event.data);
const listItem = document.createElement('li');
listItem.textContent = `${data.time} - 价格: ${data.value.toFixed(4)}`;
document.getElementById('messages').appendChild(listItem);
};
// 监听连接错误,比如服务器挂了或网络断掉
source.onerror = function(err) {
console.error('EventSource 连接出错:', err);
};
</script>
</body>
</html>
跑起来之后,浏览器会每隔两秒收到一条推送,不需要轮询,也不需要任何复杂配置。注意 SSE 的数据格式很严格:每行以 data: 开头,后面跟数据,然后两个换行符表示一条消息结束。也可以加 event: 字段来定制事件类型,或者用 id: 字段做断线重连。
1.2 再看 WebSocket
WebSocket 是一个全双工协议,由 HTTP 升级而来。一旦建立连接,服务器和客户端可以互发任意数据。很多人一提到“推送”就想到 WebSocket,实际上它是个双向通道,用来做单向推送有点“大材小用”。
同样用 Node.js 实现一个简单的 WebSocket 推送服务:
// 文件名:ws-server.js
// 技术栈:Node.js + ws 库(需要 npm install ws)
const WebSocket = require('ws');
const http = require('http');
// 先创建一个普通 HTTP 服务器
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('WebSocket 服务器已启动\n');
});
// 把 WebSocket 服务挂载到 HTTP 服务器上
const wss = new WebSocket.Server({ server });
// 监听连接事件
wss.on('connection', (ws, req) => {
console.log('新客户端连接');
// 每 2 秒向这个客户端推送一条消息
const timer = setInterval(() => {
const data = { time: new Date().toISOString(), value: Math.random() };
// WebSocket 可以直接发送字符串(会自动转为 UTF-8)
ws.send(JSON.stringify(data));
}, 2000);
// 监听客户端发来的消息(虽然我们不需要,但可以忽略)
ws.on('message', (msg) => {
console.log('收到客户端消息:', msg.toString());
});
// 连接关闭时清理定时器
ws.on('close', () => {
clearInterval(timer);
console.log('客户端断开');
});
});
server.listen(4000, () => {
console.log('WebSocket 服务器运行在 http://localhost:4000');
});
前端的 WebSocket 代码稍微复杂一点,需要自己处理连接、重连、心跳等:
<!-- 文件名:ws-client.html -->
<!DOCTYPE html>
<html>
<head>
<title>WebSocket 示例</title>
</head>
<body>
<h2>WebSocket 推送的行情数据:</h2>
<ul id="messages"></ul>
<script>
// 技术栈:浏览器原生 JavaScript
// 创建 WebSocket 连接(注意协议是 ws://)
const ws = new WebSocket('ws://localhost:4000');
// 连接成功后的回调
ws.onopen = function() {
console.log('WebSocket 连接已建立');
};
// 收到消息时的回调
ws.onmessage = function(event) {
const data = JSON.parse(event.data);
const listItem = document.createElement('li');
listItem.textContent = `${data.time} - 价格: ${data.value.toFixed(4)}`;
document.getElementById('messages').appendChild(listItem);
};
// 连接关闭时的回调(比如服务器挂了或网络断开)
ws.onclose = function() {
console.log('WebSocket 连接已关闭');
};
// 连接出错时的回调
ws.onerror = function(err) {
console.error('WebSocket 出错:', err);
};
</script>
</body>
</html>
看起来两者似乎都能满足单向推送的需求,但背后的差异可不小。
二、单向推送的场景拆解
2.1 哪些场景属于单向推送
- 实时股票行情:服务器不断推送价格变动,客户端只接收不提交。
- 系统监控告警:服务器检测到异常后发送通知消息,客户端无需回复。
- 日志实时流:后端把应用日志一行行推到前端展示,用户只是看。
- 新闻推送:重大新闻事件发生时服务器主动推送到所有在线用户。
这些场景的共同点:服务器主动发,客户端被动收。客户端除了初始化连接,后续不再向服务器发送任何实质性数据(心跳之类可以忽略)。
2.2 双向 vs 单向的思维差异
很多人觉得买一送一更划算,WebSocket 既然能双向,想单向用单向就行呗。但实际架构中,多出来的功能往往意味着多出来的代价。比如 WebSocket 需要处理客户端可能发送的各种消息(哪怕你不需要),而 SSE 天然就不支持客户端发消息,所以协议本身更轻量,安全风险也更低。
三、正面硬刚:优缺点大比拼
3.1 SSE 的优势
- 简单好用:直接用 HTTP 协议,不需要额外库(现代浏览器都自带 EventSource),后端也只需要写普通的 HTTP 响应流,不用处理升级握手、帧解析等等。看上面的示例,SSE 后端代码比 WebSocket 简洁不少。
- 自动重连:如果连接断了,浏览器端的 EventSource 会自动尝试重新连接,而且还会带上上一次收到的
last-event-id,让服务器知道从哪里接着发。WebSocket 的断线重连要自己写逻辑,比如监听onclose后用 setTimeout 重试。 - 传输文本天生友好:SSE 原生就是文本流,非常适合发送 JSON 字符串或者纯文本。WebSocket 虽然也可以传文本,但底层是二进制帧,多了一层封装。
- 兼容 HTTP 基础设施:因为 SSE 走的是普通 HTTP 连接,所以可以轻松搭配反向代理(Nginx、CDN)、负载均衡、HTTP 缓存等。WebSocket 在这些地方经常遇到问题,比如某些代理不支持 WebSocket 升级,或者连接空闲超时断开。
3.2 SSE 的劣势
- 单向限制:客户端不能往服务器发数据。如果需要客户端确认消息或发送指令,就得另开一个 HTTP 请求。不过对于纯粹的单向推送,这不叫缺点,叫设计。
- 连接数限制:浏览器对同域名的 HTTP 连接数有限制(通常是 6 个),如果同一个页面开了多个 SSE 连接,可能会达到上限。不过通常一个页面一个推送连接就够了。
- 数据格式单一:SSE 只能发送文本,二进制数据(比如图片、音频流)需要先转成 Base64 或分块传输。WebSocket 原生支持二进制帧,更高效。
- 浏览器支持:虽然现代浏览器基本都支持,但 IE 全系列不支持(Edge 早期也不支持)。对于需要兼容老旧 IE 的应用,SSE 就不太现实了。好在现在这种场景越来越少。
3.3 WebSocket 的优势
- 全双工通信:双方可以随时互相发送消息,适合在线聊天、实时协作编辑等场景。
- 二进制支持:可以发送 ArrayBuffer、Blob 等二进制数据,适合游戏、音视频流。
- 低延迟:建立连接后是长连接,没有 HTTP 的头部开销。但 SSE 同样是长连接,实际延迟差异不大。
3.4 WebSocket 的劣势
- 协议复杂:需要先 HTTP 升级握手,然后遵循 WebSocket 帧格式解析数据。如果自己实现协议,很容易踩坑;即便用库,也比 SSE 多了很多配置项(心跳、最大帧大小、子协议等)。
- 断线重连麻烦:客户端必须手动实现重连逻辑,还要处理消息的幂等性和顺序。SSE 的 EventSource 自带这个功能。
- 防火墙和代理问题:不少企业防火墙会拦截 WebSocket 的 Upgrade 请求,或者代理服务器不认识 WS 协议导致连接失败。SSE 走的普通 HTTP 基本不会被拦。
- 资源开销更大:每个 WebSocket 连接在服务器端通常需要维护一个连接对象和缓冲区。虽然 SSE 也是如此,但 WebSocket 因为要支持双向和二进制,复杂程度更高,同等条件下可能消耗更多内存。
四、实战中的注意事项
4.1 对于 SSE 要特别注意的点
- 响应头必须正确:
Content-Type: text/event-stream和Cache-Control: no-cache缺一不可,否则浏览器不会按照事件流处理。 - 坚持使用两个换行符:每条消息结尾必须是
\n\n,否则浏览器会认为消息还没发完,一直等待。同样,data:后面要有一个空格,格式要求严格。 - 处理连接断开:虽然浏览器会自动重连,但服务器端要及时清理断开的连接(比如监听
close事件),否则会留下死连接浪费资源。 - 考虑缓冲区:如果推送频率很高,数据会产生积压。可以在服务端加一个缓冲队列,控制发送速度,防止客户端来不及处理。
4.2 对于 WebSocket 要特别注意的点
- 心跳机制不能少:很多防火墙或负载均衡器会在一段时间无活动后关闭空闲连接。需要定期发送心跳消息(比如一个 ping 帧)来保持连接活跃。
- 处理粘包和分包:WebSocket 底层是帧格式,虽然库已经帮我们处理了,但如果自己写协议解析,要小心消息边界问题。
- 跨域问题:WebSocket 虽然不受同源策略限制,但服务器端在握手时可以校验 Origin 头,防止恶意网站连接。
- 性能监控:大量 WebSocket 连接时,要监控服务器的文件描述符限制和内存使用情况。每个连接都占用一个 socket,不像普通 HTTP 可以复用。
五、到底该怎么选:一张决策表
我们可以用一张表格(虽然规则说不穿插图片,但此处用文字描述表格)来帮助决策:
| 场景特征 | 推荐方案 | 理由 |
|---|---|---|
| 纯服务器推送,客户端只接收 | SSE | 简单,自动重连,成本低 |
| 需要双向实时通信(聊天、游戏) | WebSocket | 双向能力刚需 |
| 数据量大且是二进制(视频流、股票实时K线图) | WebSocket | 二进制传输高效 |
| 浏览器需要兼容老旧 IE | WebSocket(polyfill)或轮询 | SSE 不支持 IE |
| 想利用现有 HTTP 基础设施(CDN、代理) | SSE | 兼容性好 |
| 需要多个推送频道(不同主题) | SSE(通过 event 字段区分类别) | 原生支持事件类型 |
| 需要服务器主动给不同客户端发不同内容 | 两者均可,但 SSE 更易管理 | 连接按路径分发 |
注意:如果应用后来可能需要双向通信,一开始就选 WebSocket 也没问题,但成本要算清楚。如果确定以后也是单向,那用 SSE 带来的简化效果非常明显。
六、总结
回到最初的问题:在单向推送场景下,SSE 和 WebSocket 到底怎么取舍?我的建议非常明确:优先选择 SSE,除非有必须使用 WebSocket 的理由。
SSE 就像一把朴素的菜刀,专门用来切菜,锋利顺手;WebSocket 则像一把多功能瑞士军刀,虽然也能切菜,但用起来笨重,而且很多功能你根本用不上。实际项目里,我见过不少团队因为“感觉 WebSocket 更高级”就用了它,结果花了大量时间处理断线重连、心跳、兼容性,最后后悔不已。
当然,如果你已经用了 WebSocket 做其他双向功能,顺便用它做单向推送是没问题的。但如果是全新项目且确定单向,请务必尝试 SSE。它的简单会让你惊喜,而它的自动重连、HTTP 兼容性会让你少熬夜。
最后提醒一句:技术没有银弹,适合自己的场景才是最好的。动手写个小 demo 对比一下,你就能感受到两者的区别。
Comments