多人系统的"刷新":为什么选 SSE 而不是 WebSocket

2026 / 10 / 20#sse#node#架构

先分清需求

需求 合适的技术
单向通知"数据变了" SSE
双向实时协作(画板、协同编辑) WebSocket
秒级都嫌慢的行情推送 WebSocket / WebTransport

内部系统里 90% 的"实时"需求其实是第一行:服务端有事要说,客户端没什么要回的。这种场景用 WebSocket,等于为了发一条通知维护一套双向连接协议。

最小实现

const clients = new Map();  // clientId -> res

app.get("/api/events", (req, res) => {
  const id = req.query.cid;
  res.writeHead(200, {
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache",
    "X-Accel-Buffering": "no",
  });
  res.write(": ok\n\n");
  clients.set(id, res);
  req.on("close", () => clients.delete(id));
});

function broadcast(evt, from) {
  for (const [id, res] of clients) {
    if (id !== from) res.write(`data: ${JSON.stringify(evt)}\n\n`);
  }
}

浏览器侧只有三行:

const es = new EventSource("/api/events?cid=" + CID);
es.onmessage = (e) => handle(JSON.parse(e.data));  // 断线会自动重连

四个必须避开的坑

1. 反代默认会缓冲。 Caddy 要显式关掉,否则事件被攒着不发,看起来像"推送失败":

reverse_proxy localhost:3000 {
  flush_interval -1
}

2. 别把自己的操作也推给自己。 广播时排除发起的 clientId,否则一保存页面自己跳一下,像出 bug。

3. 粒度要细。 广播 project.updated:42 而不是"有数据变了",否则整页反复闪。

4. 正在编辑的表单不能静默覆盖。 这条最重要:检测到该区域有未保存改动时,只在角落提示"某某刚改了这条,点此加载最新",绝不自动重拉。丢别人十分钟的输入,比晚刷新十秒严重得多。

兜底

SSE 在代理后面偶尔会被悄悄掐断而前端不知道。叠一个 60 秒的低频轮询(只取 max(updated_at),几十字节),对不上就整块重拉。这个成本极低,但能兜住"只在别人电脑上出现"的连接假死。

本文为个人学习记录,可能存在错误或已过时,仅供参考。
← 返回笔记列表