“选型从来不是选最好的技术,而是选最合适的那一个。” — 每一位踩过坑的架构师

实时推送已经是现代 Web 应用的标配:股票行情、AI 流式输出、协同编辑、游戏状态同步……每一个场景背后,都藏着一道绕不开的选择题:WebSocket 还是 SSE?

这个问题在 2026 年有了新的背景——HTTP/3(QUIC)加速落地,Cloudflare Workers / Deno Deploy 等边缘运行时成为主流,浏览器对 SSE 的支持趋于完善。本文从原理到实战,帮你做出有据可查的技术决策。


一、先把概念说清楚

WebSocket

WebSocket 是一个全双工、持久化的 TCP 连接协议(RFC 6455)。握手借用 HTTP Upgrade 机制完成,之后双方可以随时互发二进制或文本帧,延迟极低。

Client ──── HTTP Upgrade ──▶ Server
       ◀──── 101 Switching ───
       ⟺  全双工帧流  ⟺

Server-Sent Events(SSE)

SSE 是基于 HTTP 的单向服务器推流协议,响应头 Content-Type: text/event-stream,服务端持续写入格式化的文本块,客户端通过浏览器内置的 EventSource API 订阅。

Client ──── GET /events ──▶ Server
       ◀── 200 text/event-stream ──
       ◀── data: {...}

 ──
       ◀── data: {...}

 ──

SSE 天然搭载自动重连retry: 字段)和断点续传Last-Event-ID 请求头),浏览器原生支持,无需任何第三方库。


二、2026 年的新变量:HTTP/3 与边缘运行时

HTTP/3 对 WebSocket 的影响

HTTP/3 基于 QUIC(UDP),原生多路复用,消除了 TCP 的队头阻塞。WebSocket over HTTP/3(RFC 9220)已于 2022 年标准化,主流浏览器在 2025 年底完成全面支持。

实测结论:

  • 弱网、移动网络场景下,WebSocket/H3 的连接建立速度比 /H1.1 快约 30-50ms(0-RTT 握手)。
  • SSE 作为普通 HTTP 响应,在 HTTP/3 下同样受益,且复用已有连接时无额外握手开销

边缘运行时的限制

Cloudflare Workers、Vercel Edge Functions 等平台对长连接有严格限制:

平台 WebSocket SSE
Cloudflare Workers ✅ 原生支持(Hibernation API) ✅ 支持(TransformStream)
Vercel Edge Functions ⚠️ 受限(需 Durable Objects 等方案) ✅ 较好支持
AWS Lambda ❌ 不适合(有执行时长限制) ❌ 不适合
Fly.io / Railway ✅ 完整支持 ✅ 完整支持

结论:在 Serverless / Edge 场景下,SSE 的部署门槛明显低于 WebSocket。


三、核心维度对比

维度 WebSocket SSE
通信方向 全双工(双向) 单向(服务端 → 客户端)
底层协议 TCP(或 QUIC over HTTP/3) HTTP/1.1、HTTP/2、HTTP/3
自动重连 ❌ 需手动实现 ✅ 浏览器原生支持
断点续传 ❌ 需业务层处理 Last-Event-ID 原生支持
防火墙兼容性 ⚠️ 部分企业防火墙会拦截 Upgrade ✅ 标准 HTTP,几乎无拦截风险
HTTP/2 多路复用 ❌ 每连接独占一个 TCP 连接 ✅ 多个 SSE 流共享一个 H2 连接
二进制支持 ✅ 原生支持(ArrayBuffer / Blob) ⚠️ 仅文本(需 Base64 编码二进制)
浏览器并发限制 无特殊限制 HTTP/1.1 下最多 6 个/域名(H2 无此限制)
服务端实现复杂度 中等(需维护连接状态) 低(普通 HTTP 长响应)
水平扩展难度 高(有状态,需 sticky session 或消息总线) 较低(可借助 HTTP 层负载均衡)

四、代码实战:同一功能的两种实现

以”服务端每秒推送一条实时日志”为例,用 Node.js 实现。

SSE 实现(推荐起点)

// server-sse.mjs
import { createServer } from 'node:http';

createServer((req, res) => {
  if (req.url !== '/events') {
    res.writeHead(404);
    res.end();
    return;
  }

  // SSE 响应头
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive',
    'Access-Control-Allow-Origin': '*',
  });

  // 发送初始重连间隔(毫秒)
  res.write('retry: 3000

');

  let id = 0;
  const timer = setInterval(() => {
    const payload = JSON.stringify({
      ts: Date.now(),
      level: 'info',
      msg: `Log entry #${++id}`,
    });
    // SSE 格式:id + event + data,以两个换行结束
    res.write(`id: ${id}
event: log
data: ${payload}

`);
  }, 1000);

  req.on('close', () => {
    clearInterval(timer);
    console.log('Client disconnected');
  });
}).listen(3000, () => console.log('SSE server on :3000'));
// client-sse.mjs(浏览器端等效逻辑)
const es = new EventSource('/events');

es.addEventListener('log', (e) => {
  const { ts, level, msg } = JSON.parse(e.data);
  console.log(`[${level}] ${msg} (latency: ${Date.now() - ts}ms)`);
});

es.onerror = () => console.warn('SSE error, browser will auto-reconnect...');

WebSocket 实现

// server-ws.mjs(使用标准库 ws)
import { WebSocketServer } from 'ws';

const wss = new WebSocketServer({ port: 3001 });

wss.on('connection', (ws, req) => {
  console.log('Client connected');

  let id = 0;
  const timer = setInterval(() => {
    if (ws.readyState !== ws.OPEN) return;
    ws.send(JSON.stringify({
      id: ++id,
      ts: Date.now(),
      level: 'info',
      msg: `Log entry #${id}`,
    }));
  }, 1000);

  // WebSocket 支持接收客户端消息(SSE 做不到)
  ws.on('message', (data) => {
    const { cmd } = JSON.parse(data);
    if (cmd === 'pause') clearInterval(timer);
  });

  ws.on('close', () => {
    clearInterval(timer);
    console.log('Client disconnected');
  });
});

console.log('WS server on :3001');
// client-ws.mjs
const ws = new WebSocket('ws://localhost:3001');

ws.onmessage = (e) => {
  const { ts, level, msg } = JSON.parse(e.data);
  console.log(`[${level}] ${msg} (latency: ${Date.now() - ts}ms)`);
};

// 需要手动实现重连
ws.onclose = () => setTimeout(() => location.reload(), 3000);

// 双向:客户端可发指令
document.getElementById('pause').onclick = () =>
  ws.send(JSON.stringify({ cmd: 'pause' }));

两段代码对比的直观感受:SSE 服务端是一个”普通 HTTP 路由 + 定时写入”,WebSocket 服务端需要管理连接状态和消息帧,复杂度更高但能力也更强。


五、选型决策框架

优先选 SSE,如果你的场景是:

  • AI 流式输出(ChatGPT / Claude 风格的打字机效果)
  • 实时通知、Feed 流、进度条
  • 日志流、监控大盘
  • 部署在 Serverless / Edge 平台
  • 团队对 WebSocket 运维经验不足
  • 需要穿越企业防火墙或代理

优先选 WebSocket,如果你的场景是:

  • 协同编辑(双向 OT/CRDT 操作流)
  • 在线游戏、实时竞价、交易撮合
  • 音视频信令(WebRTC SDP/ICE 交换)
  • 需要传输二进制帧(音频包、图像差量)
  • 客户端需要主动发送高频消息(>10 次/秒)

2026 年的新建议

如果你在构建 AI Agent 工作流的前端界面,SSE 几乎是唯一合理选择:流式 token 输出、工具调用进度、多 step 状态更新,全部适合单向推流。WebSocket 在这里只会增加复杂度。


六、生产级注意事项

SSE 的两个常见陷阱:

  1. Nginx 反向代理需关闭缓冲

    location /events {
      proxy_pass http://backend;
      proxy_buffering off;          # 关键!
      proxy_cache off;
      proxy_read_timeout 86400s;
      proxy_set_header Connection '';
      proxy_http_version 1.1;
    }
    
  2. HTTP/1.1 下的连接数限制:同域名最多 6 个并发 SSE。升级到 HTTP/2 即可解除,或用 SharedWorker 在多个 Tab 间共享单条 SSE 连接。

WebSocket 的常见陷阱:

  1. 心跳保活:大量云平台(AWS ALB、Cloudflare)会对空闲 WebSocket 连接 60-300s 后强制断开,必须实现 ping/pong 心跳。
  2. 水平扩展:多实例部署时需要 Redis Pub/Sub 或 NATS 等消息总线路由消息,否则不同实例的客户端收不到彼此的广播。

七、总结

场景 推荐方案
AI 流式输出 SSE
实时通知 / 消息推送 SSE
协同编辑 / 游戏 WebSocket
高频双向交互 WebSocket
Serverless / Edge 部署 SSE
需穿越企业防火墙 SSE

2026 年的核心判断标准只有一个:客户端是否需要主动、高频地向服务端发送数据?

  • 需要 → WebSocket
  • 不需要 → SSE,它更简单、更稳健、HTTP 生态无缝集成

不要因为 WebSocket “听起来更厉害”就无脑选它。SSE 在绝大多数推送场景下已经足够,而且你的运维团队会感谢你。


延伸阅读


本文代码已在 Node.js 22 LTS + Chrome 134 环境下验证。如有勘误欢迎在评论区指出。