WebSockets 与 SSE 与长轮询 — 在你的聊天应用崩溃前选择正确的实时模式
大多数开发者默认使用WebSocket而未深思。本文详细介绍了WebSocket、服务器推送事件和长轮询的真正权衡——延迟、重连行为、基础设施复杂性、双向通信——帮助您选择合适的模式,从而避免过度设计数据流。
你构建了一个仪表板,用户需要实时数据更新。你选择了WebSocket——因为这是大家普遍的做法。但现在你在凌晨两点调试会话粘滞问题,因为你的负载均衡器不断将客户端发送到错误的Pod。你推送的数据?每10秒一个JSON数字。WebSocket完全过度了。
这种情况经常发生。开发者将WebSocket视为实时解决方案的默认选择,而SSE或长轮询实际上更早推出、更容易扩展、运维成本更低。以下是实际选择的指南。
决策表
在深入探讨之前,这是完整的对比。你可以先用它来做决策,如果需要推理过程,再阅读下面的章节。
| 特征 | 长轮询 | 服务器推送事件(Server-Sent Events) | WebSocket |
|---|---|---|---|
| 协议 | HTTP/1.1 | HTTP/1.1+(分块传输) | WS(HTTP升级) |
| 方向 | 客户端仅拉取 | 服务器到客户端单向 | 全双工双向通信 |
| 浏览器支持 | 所有浏览器 | 所有现代浏览器(不包括IE11) | 所有现代浏览器 |
| 自动重连 | 手动实现——你需要编写循环 | 通过EventSource内置支持 | 手动或通过库实现 |
| CDN/代理友好 | 是的 | 基本支持(注意超时监控) | 不 |
| 水平扩展能力 | 无状态,简单易实现 | 无状态,简单易实现 | 需要会话粘滞或发布/订阅代理 |
| 基础设施复杂度 | 低——纯HTTP | 低——纯HTTP | 高——需要状态连接、负载均衡器配置、代理服务 |
| 延迟 | 轮询间隔加往返时间 | 接近实时 | 接近实时 |
| 较小——只是一个 ID | 低频更新,老旧基础设施 | 仪表板、信息流、通知 | 聊天、协同编辑、游戏 |
长轮询:默默无闻的功臣
长轮询并非“过时”或“错误”——它只是HTTP。客户端发起请求,服务器保持连接直到有数据可发送(或超时触发),客户端获取响应后立即发起下一次请求。重复此过程。
它早在WebSocket出现之前就已存在,并仍在运行数百万个生产环境应用。原因在于:它就是HTTP。你的现有反向代理、CDN、负载均衡器和监控工具无需配置即可理解它。无需会话粘滞,无需协议升级,无需WebSocket专用基础设施。
尽管如此,缺点确实存在。每一次“更新”都是一次完整的HTTP请求周期——包括头部、TCP开销(除非使用HTTP/2)、服务器端请求队列。在高并发场景下,这会增加服务器开销。而延迟的下限是每次轮询的往返时间,而非事件本身的延迟。如果你每5秒轮询一次,而事件在第1秒触发,用户仍需等待4秒。
何时使用: 在添加持久连接层不值得投入基础设施变更的旧系统中;低频更新(每分钟一次或更少);在某些代理过于激进地拦截SSE的环境中;由客户端明确控制的轮询——如作业状态检查、异步任务结果。
服务器推送事件(SSE):被所有人遗忘的方案
SSE是一种标准(HTML Living Standard)允许服务器通过单一长连接推送文本事件流。浏览器会自动处理重连。你无需编写重连循环—— EventSource API会自动使用上次看到的事件ID重新连接,从而在网络中断后可以继续流式接收数据。
传输格式极其简单:
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
id: 1
event: price-update
data: {"symbol":"BTC","price":67420}
id: 2
event: price-update
data: {"symbol":"ETH","price":3521}
每个事件是纯文本,可包含可选 id, event, data,并且 retry 字段,各字段之间以空行分隔。你可以直接使用cURL测试SSE端点——使用 cURL 命令生成器 构造请求,设置正确的Accept头(text/event-stream)并流式传输响应。
客户端方面:
const es = new EventSource('/api/events');
es.addEventListener('price-update', (e) => {
const payload = JSON.parse(e.data);
updateDashboard(payload);
});
es.onerror = () => {
// EventSource will auto-reconnect. You don't need to do anything here
// unless you want to log or update UI state.
};
SSE有一个真实限制:它仅支持服务器到客户端的单向通信。客户端无法通过同一连接回传数据——任何客户端发起的操作都需通过普通HTTP请求完成。对于大多数仪表板、通知和信息流场景来说,这已经足够了。你本来就不需要客户端向服务器推送数据。
另一个陷阱:某些代理会缓冲响应体,仅在连接关闭后才转发,这会完全破坏流式传输。NGINX需要 proxy_buffering off。Cloudflare免费层级会缓冲SSE。如果你的基础设施中存在缓冲代理且无法配置,SSE将无法工作,你需要改用WebSocket或长轮询。
何时使用: 实时仪表板、通知流、活动日志、AI响应流(这正是ChatGPT的流式API所使用的场景)、股票行情、部署/构建日志尾随。任何服务器有数据可推送而客户端无需回传数据的场景。
WebSocket:何时真正值得使用
WebSocket提供了一条全双工、持久化的TCP连接。在HTTP升级握手之后,双方都可以随时发送帧消息。其延迟是网络中最低的——无需为每条消息创建新请求,无需为每个帧添加头部。
代价是其状态性。服务器需要跟踪每个打开的连接。这在大规模场景下立即带来问题:负载均衡器按连接(而非请求)进行路由,因此你需要会话粘滞或发布/订阅代理(如Redis等)来将消息广播到多个服务器实例。你的CDN无法缓存WebSocket流量。你的反向代理需要明确配置升级规则。你的监控系统若想实现消息级可见性,必须理解WebSocket帧。
这些都不是无法解决的问题,但确实需要付出大量工作。当你启动项目并评估选项时,“它只是HTTP”与“带有Redis发布/订阅层的状态性WebSocket连接”之间存在显著的复杂度差异。
WebSocket消息通常为JSON格式。如果你在调试WebSocket集成并需要查看或格式化消息, JSON 格式化程序 可以让你更轻松地在开发工具的网络标签中读取压缩后的消息内容。
何时使用: 聊天应用(真正双向——每条消息均由客户端发起)。协同编辑(如Figma、Google Docs——多个客户端同时写入共享状态)。多人游戏(高频、低延迟状态同步)。金融交易终端(订单簿延迟低于100毫秒)。基本上:任何需要客户端频繁推送数据而非仅接收数据的场景。
扩展性问题:没人会在问题出现前提及
长轮询和SSE在HTTP层是无状态的。每个请求都可以落在任何服务器实例上。你的水平扩展故事很简单,这其实是个优点:增加更多实例即可。连接状态仅存在于开放的HTTP连接期间,负载均衡器可以自由路由。
WebSocket是状态性的。客户端A的连接存在于服务器1上。当你想从服务器2向客户端A推送消息(因为事件在服务器2产生),服务器2必须知道该连接的存在。标准解决方案是使用发布/订阅代理——如Redis发布/订阅、Kafka,或托管服务如Pusher或Ably,这些服务会抽象掉所有复杂性。你只能自行构建该层,或支付相关费用。
Pusher对500个并发连接收取$49/月,Ably价格类似。如果你的“实时功能”只是一个当有人点赞时更新的通知徽章,那么使用SSE完全可以免费托管在你的现有API旁边,而无需支付高昂费用。
HTTP/2改变了长轮询的计算方式
经典的HTTP/1.1长轮询是每个连接一次请求,用于保持等待。在HTTP/2中,多路复用意味着一个TCP连接上可同时存在多个流。开销更低。这并不是长轮询优于SSE的理由,但它意味着“HTTP开销昂贵”这一批评在2024年比2012年时要弱得多,当时这些对比最初被撰写。
如果你在客户端到服务器之间全程使用HTTP/2(没有中间代理降级),长轮询的扩展性能优于大多数早期基准测试所显示的。
快速决策指南
如果你的用例符合以下情况之一,可以停止阅读:
- 更新频率低于每分钟一次,基础设施老旧,轮询语义简单: 选择长轮询。不要过度复杂化。
- 服务器推送数据,客户端主要读取,使用标准HTTP基础设施: 选择SSE。这覆盖了80%以上的“实时”功能。
- 客户端频繁发送数据,延迟低于100毫秒至关重要,且需要真正双向通信: 选择WebSocket。此时复杂性是值得的。
- 你正在使用AI流式处理(如OpenAI、Anthropic等): 选择SSE——这正是它们流式API所使用的方案。
- 多人游戏或协同编辑器: 选择WebSocket,或使用专门的框架(如Liveblocks、PartyKit等)。
默认答案不是WebSocket。而是SSE——除非你有明确理由认为它不够。真正需要全双工实时功能的生产应用数量,远少于使用WebSocket的应用数量。这个差距是工程债务,积累在凌晨两点面对负载均衡器配置时。 使用 WebSockets。那个差距是凌晨两点在负载均衡器配置前积累的工程债务。
