在实际开发中,我们经常会遇到只需要服务器单向给客户端发消息的场景,比如实时行情推送、系统通知、日志流展示等。这类需求看似简单,但选错技术方案往往会让系统变得复杂又昂贵。今天我们就来聊聊两个常用的推送技术——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-streamCache-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 对比一下,你就能感受到两者的区别。