无尘阁日记

无尘阁日记

SSE 协议小白指南:服务器主动"喊"你一嗓子
2026-08-04

如果你做过网页,一定熟悉这种模式:浏览器问服务器要数据,服务器给了,对话就结束了。那如果服务器想主动告诉你”有新消息了”,怎么办?

刷新页面?太蠢。轮询(每隔几秒问一次)?浪费流量。WebSocket?有点重。

这时候就该 SSE(Server-Sent Events,服务器发送事件) 出场了。它专门解决一个简单问题:让服务器能主动、持续地给浏览器推消息,而且实现起来比 WebSocket 简单十倍。

下面用 6 个核心要点 + 6 段可直接运行的代码,带你彻底搞懂 SSE。


1. 它是个”单行道”:只能从服务器流向浏览器

SSE 最大的特点就是单向。浏览器连上之后,服务器可以随时往浏览器发消息,但浏览器不能通过这条通道回传数据(要回传得另开一条 HTTP 请求)。

类比:SSE 像广播电台,服务器是电台,浏览器是收音机。电台可以一直播,收音机只能听,不能对着电台喊话。

例子:一个最简单的 SSE 服务端(Node.js)

const http = require('http');

http.createServer((req, res) => {
  // 1. 设置响应头,这是 SSE 的"身份证"
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive'
  });

  // 2. 每隔 2 秒推一条消息
  let count = 0;
  const timer = setInterval(() => {
    count++;
    res.write(`data: \u7b2c${count}\u6761\u6d88\u606f\uff0c\u65f6\u95f4\uff1a${new Date().toLocaleTimeString()}\n\n`);
    if (count >= 5) {
      clearInterval(timer);
      res.end();
    }
  }, 2000);

}).listen(3000, () => console.log('SSE \u670d\u52a1\u8dd1\u5728 http://localhost:3000'));

关键看第 9 行Content-Type: text/event-stream,只要带上这个头,浏览器就知道”哦,这是 SSE,我要用 EventSource 来接收”。


2. 消息格式极其死板:必须以 data: 开头

SSE 的消息不是随便写的,它有一套固定格式。每条消息以 data: 打头,以两个换行符 \n\n 结尾。

data: \u8fd9\u662f\u4e00\u6761\u6d88\u606f\n\n

如果你要发多行,每行都要加 data:

data: \u7b2c\u4e00\u884c\ndata: \u7b2c\u4e8c\u884c\n\n

浏览器收到后,会自动把多行拼接成一个字符串,中间用换行符连接。

例子:服务端发多行数据

res.writeHead(200, {
  'Content-Type': 'text/event-stream'
});

res.write(`data: {\n`);
res.write(`data:   "user": "\u6c5f\u6960",\n`);
res.write(`data:   "action": "login",\n`);
res.write(`data:   "time": "${new Date().toISOString()}"\n`);
res.write(`data: }\n\n`);

浏览器 EventSource 收到的事件中,event.data 会是:

{
  "user": "\u6c5f\u6960",
  "action": "login",
  "time": "2026-08-04T12:34:56.789Z"
}

3. 浏览器用 EventSource 接收,三行代码搞定

浏览器原生提供了 EventSource 对象,不需要任何库。

例子:前端接收 SSE

<!DOCTYPE html>
<html>
<body>
  <h1>SSE \u5b9e\u65f6\u6d88\u606f</h1>
  <ul id="list"></ul>

  <script>
    // \u53ea\u9700\u8981\u8fd9\u4e00\u884c\uff0c\u8fde\u63a5\u670d\u52a1\u7aef
    const es = new EventSource('http://localhost:3000');

    // \u6bcf\u6b21\u6536\u5230\u6d88\u606f\u5c31\u89e6\u53d1
    es.onmessage = (event) => {
      const li = document.createElement('li');
      li.textContent = event.data;
      document.getElementById('list').appendChild(li);
    };

    // \u51fa\u9519\u5904\u7406
    es.onerror = (err) => {
      console.log('\u8fde\u63a5\u65ad\u5f00\uff0c\u6b63\u5728\u91cd\u8fde...');
    };
  </script>
</body>
</html>

就这么简单。new EventSource(url) 创建连接,onmessage 收消息,onerror 处理断开。


4. 支持自定义事件名:不只是 message

默认情况下,所有消息都触发 onmessage。但你可以给消息分类,比如有的叫 price,有的叫 notice

例子:服务端发送带事件名的消息

// \u63a8\u9001\u80a1\u7968\u4ef7\u683c
res.write(`event: price\n`);
res.write(`data: 38.50\n\n`);

// \u63a8\u9001\u7cfb\u7edf\u901a\u77e5
res.write(`event: notice\n`);
res.write(`data: \u60a8\u7684\u8ba2\u5355\u5df2\u53d1\u8d27\n\n`);

前端用 addEventListener 监听不同事件

const es = new EventSource('http://localhost:3000');

es.addEventListener('price', (e) => {
  document.getElementById('price').textContent = e.data;
});

es.addEventListener('notice', (e) => {
  alert('\u7cfb\u7edf\u901a\u77e5\uff1a' + e.data);
});

这样业务逻辑可以分门别类处理,代码更清爽。


5. 自带断线重连:浏览器比你更操心

SSE 有一个非常贴心的机制:如果连接断了,浏览器会自动帮你重连,默认间隔约 3 秒。你什么都不用写。

更妙的是,服务端可以控制重连间隔。比如你发一条 retry: 10000,浏览器就会在断开后等 10 秒再重连。

例子:服务端设置重连间隔 + 消息 ID

// \u544a\u8bc9\u6d4f\u89c8\u5668\uff1a\u65ad\u5f00\u540e 10 \u79d2\u518d\u8bd5
res.write(`retry: 10000\n`);

let id = 0;
setInterval(() => {
  id++;
  // \u6bcf\u6761\u6d88\u606f\u90fd\u5e26\u4e0a ID\uff0c\u65ad\u7ebf\u540e\u670d\u52a1\u7aef\u53ef\u4ee5\u4ece\u8fd9\u4e2a ID \u540e\u7eed\u4f20
  res.write(`id: ${id}\n`);
  res.write(`data: \u6d88\u606f\u7f16\u53f7${id}\n\n`);
}, 2000);

浏览器重连时,会在请求头里带上 Last-Event-ID: 5,服务端读到后就知道”哦,客户端已经收到第 5 条了,我从第 6 条开始发”,实现断点续传


6. SSE vs WebSocket:别拿大炮打蚊子

很多人一听到”实时推送”就想到 WebSocket。但 SSE 在单工场景下更轻量。

特性 SSE WebSocket
通信方向 服务器→客户端(单工) 全双工
协议 HTTP(兼容性好) ws/wss(独立协议)
自动重连 705 原生支持 74c 需自己写
消息 ID/断点续传 705 原生支持 74c 需自己实现
二进制支持 74c 只能传文本 705 支持
实时游戏/双向高频通信 不合适 合适
股票推送/日志流/通知 完美 过重

例子:日志实时推送(SSE 比 WebSocket 更合适)

const fs = require('fs');
const http = require('http');

http.createServer((req, res) => {
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache'
  });

  // \u5b9e\u65f6\u8ffd\u8e2a app.log \u6587\u4ef6
  const stream = fs.createReadStream('/var/log/app.log', { start: fs.statSync('/var/log/app.log').size });
  stream.on('data', chunk => {
    const lines = chunk.toString().split('\n');
    lines.forEach(line => {
      if (line) res.write(`data: ${line}\n\n`);
    });
  });

}).listen(3000);

前端几行代码就能看实时日志,无需任何第三方库,也不需要处理 WebSocket 的握手和心跳。


一句话总结

SSE 就是基于 HTTP 的服务器单向推送协议。格式简单(data: + \n\n),浏览器原生支持(EventSource),自带重连和断点续传。如果你只需要服务器往浏览器发消息,完全没必要上 WebSocket,SSE 才是那把顺手的螺丝刀。