
实时通信问题
当你的 Web 应用需要从服务器接收更新——聊天消息、实时比分、股票价格、通知——主要有三种选择:WebSocket、Server-Sent Events (SSE) 和长轮询。它们在连接方式、成本和失败场景上存在根本差异。
选择错误会导致资源浪费、扩展困难或用户体验不佳。本指南将详细解释每种技术的工作原理,并提供清晰的选择标准。

长轮询
工作原理
长轮询是最古老的技术,不需要特殊的浏览器 API。
- 客户端发送常规 HTTP 请求
- 服务器保持连接打开(不立即响应)
- 当新数据可用时,服务器响应
- 客户端立即发送另一个请求
- 重复
// 客户端
async function longPoll() {
while (true) {
try {
const res = await fetch('/api/events?lastId=' + lastEventId);
const data = await res.json();
processEvent(data);
lastEventId = data.id;
} catch (err) {
// 连接断开,等待后重试
await new Promise(r => setTimeout(r, 2000));
}
}
}
longPoll();
// 服务器 (Node.js / Express)
app.get('/api/events', async (req, res) => {
const lastId = req.query.lastId;
// 等待最多 30 秒获取新数据
const data = await waitForNewData(lastId, { timeout: 30000 });
if (data) {
res.json(data);
} else {
res.status(204).end(); // 超时,无新数据
}
});
优缺点
优点:
- 通用性强——任何 HTTP 服务器和客户端都支持
- 透明地通过防火墙和代理
- 利用现有基础设施易于实现
- 无需粘性会话即可与标准负载均衡器配合
缺点:
- 每次请求都有 HTTP 开销(头部、HTTP/1.1 的 TCP 握手开销)
- 延迟较高——每个事件至少一次往返
- 服务器保持连接打开,消耗线程/内存
- 并非真正的双向通信——客户端必须始终发起
最佳适用场景: 基础设施受限的环境、更新不频繁的简单通知系统或遗留系统。
Server-Sent Events (SSE)

工作原理
SSE 使用单个持久 HTTP 连接,服务器可以随时通过该连接推送数据。
- 客户端使用
EventSource打开一个 HTTP 连接 - 服务器流式传输
text/event-stream数据 - 连接保持打开;服务器在事件发生时发送事件
- 如果连接断开,浏览器自动重新连接
// 客户端——仅需两行
const es = new EventSource('/api/stream');
es.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Received:', data);
};
// 命名事件类型
es.addEventListener('user-joined', (event) => {
console.log('User joined:', event.data);
});
es.addEventListener('error', () => {
if (es.readyState === EventSource.CLOSED) {
console.log('Connection closed');
}
});
// 服务器 (Node.js / Express)
app.get('/api/stream', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// 发送注释以保持连接活跃
const keepAlive = setInterval(() => {
res.write(': ping\n\n');
}, 20000);
// 发送数据
const sendEvent = (type, data) => {
res.write(`event: ${type}\n`);
res.write(`data: ${JSON.stringify(data)}\n`);
res.write(`id: ${Date.now()}\n\n`); // 用于重连恢复的 id
};
// 订阅事件总线
const unsubscribe = eventBus.on('update', (data) => sendEvent('update', data));
req.on('close', () => {
clearInterval(keepAlive);
unsubscribe();
});
});
优缺点
优点:
- 原生浏览器重连,支持
Last-Event-ID头部 - 比 WebSocket 更简单——纯 HTTP,与标准基础设施兼容
- 出色的浏览器支持(所有现代浏览器)
- 事件可以包含类型、ID 和重试间隔
- 支持 HTTP/2 多路复用(每个连接多个流)
缺点:
- 单向——仅服务器到客户端;客户端需单独发送请求
- HTTP/1.1 下每个域名最多 6 个并发连接(HTTP/2 无此限制)
- 仅文本(UTF-8);无法高效传输二进制数据
- 某些企业代理会缓冲响应,破坏流式传输
最佳适用场景: 通知、实时信息流、仪表盘、实时日志——任何数据从服务器流向客户端,客户端通过常规 POST 请求偶尔发送更新的场景。
WebSocket
工作原理
WebSocket 以 HTTP 连接开始,然后通过 Upgrade 握手升级为全双工 TCP 连接。
- 客户端发送 HTTP Upgrade 请求
- 服务器响应
101 Switching Protocols - 双方现在可以随时发送消息,双向均可
- 消息可以是文本或二进制(ArrayBuffer / Blob)
// 客户端
const ws = new WebSocket('wss://api.example.com/ws');
ws.onopen = () => {
console.log('Connected');
ws.send(JSON.stringify({ type: 'subscribe', channel: 'prices' }));
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
handleMessage(msg);
};
ws.onclose = (event) => {
console.log('Closed:', event.code, event.reason);
// 实现指数退避重连
setTimeout(reconnect, Math.min(30000, 1000 * 2 ** retryCount++));
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
// 从客户端发送
function sendMessage(data) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(data));
}
}
// 服务器 (Node.js / ws 库)
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws, req) => {
console.log('Client connected from', req.socket.remoteAddress);
ws.on('message', (message) => {
const data = JSON.parse(message);
// 处理 data.type === 'subscribe', 'unsubscribe', 'message' 等
handleClientMessage(ws, data);
});
ws.on('close', (code, reason) => {
console.log('Client disconnected:', code, reason.toString());
cleanupClient(ws);
});
ws.on('error', (err) => {
console.error('WebSocket error:', err);
});
// Ping 检测失效连接
ws.isAlive = true;
ws.on('pong', () => { ws.isAlive = true; });
});
// 心跳——终止断开的连接
setInterval(() => {
wss.clients.forEach(ws => {
if (!ws.isAlive) return ws.terminate();
ws.isAlive = false;
ws.ping();
});
}, 30000);

优缺点
优点:
- 真正的双向通信——客户端和服务器均可随时推送
- 初始握手后每条消息开销低(2–10 字节帧头 vs HTTP 的数百字节)
- 支持二进制数据(图片、音频、文件块)
- 高频消息场景下延迟极低(游戏、交易)
缺点:
- 基础设施需支持 WebSocket(负载均衡器、代理需配置)
- 无自动重连——需自行实现
- 有状态连接使水平扩展复杂化(需要粘性会话或发布/订阅层)
- 在受限企业网络中可能遇到防火墙/代理问题
最佳适用场景: 聊天、多人在线游戏、实时协作(类似 Google Docs)、金融交易仪表盘、任何客户端频繁发送消息的应用。
并排对比
| 特性 | 长轮询 | SSE | WebSocket |
|---|---|---|---|
| 方向 | 客户端 → 服务器 → 客户端 | 服务器 → 客户端 | 双向 |
| 协议 | HTTP | HTTP | WebSocket (TCP) |
| 二进制数据 | 通过编码 | 不支持 | 支持 |
| 自动重连 | 手动 | 内置 | 手动 |
| 浏览器支持 | 通用 | 所有现代浏览器 | 所有现代浏览器 |
| HTTP/2 支持 | 是 | 多路复用 | 不适用 |
| 代理/防火墙友好 | ✅ 最佳 | ✅ 良好 | ⚠️ 需配置 |
| 消息开销 | 高(完整 HTTP) | 低 | 最小 |
| 扩展复杂度 | 低 | 低-中 | 高 |
决策指南
使用长轮询如果:
- 需要最大的基础设施兼容性
- 更新不频繁(少于每 30 秒一次)
- 位于严格代理或旧负载均衡器之后
- 需要快速实现,无需新基础设施
使用 SSE 如果:
- 数据主要从服务器流向客户端
- 希望简单且具有原生浏览器重连
- 构建通知、实时信息流、进度更新
- 使用 HTTP/2(SSE 变得非常高效)
使用 WebSocket 如果:
- 需要真正的双向消息传递(聊天、游戏、协作)
- 发送二进制数据
- 消息频率高(每秒几次以上)
- 延迟至关重要(交易、游戏)
扩展考虑
长轮询和 SSE 更容易扩展,因为它们使用标准 HTTP——任何无状态负载均衡器均可工作。WebSocket 连接是有状态的,因此负载均衡器必须将同一客户端路由到同一服务器(粘性会话),或者需要发布/订阅层(Redis、NATS),以便任何服务器都可以向任何已连接的客户端发布消息。
对于大规模 WebSocket 部署,可考虑:Socket.IO(处理重连、房间)、Ably、Pusher 或托管的 WebSocket 服务。
→ 使用 URL 解析器 检查 WebSocket 端点 URL 及其查询参数。