☰
实时消息推送三剑客:轮询、WebSocket、SSE原理对比与选型指南
2026/10/7 14:28:26 网站建设 项目流程

前端做多了就会发现,凡是涉及"实时消息推送"的需求,最后一定会回到轮询、WebSocket、SSE这三个选项上。直播间弹幕、客服聊天、运维大屏、订单状态刷新、AI 流式输出……产品开口都是"我要实时看到变化",可真到技术选型的时候,很多人连这三者的区别都说不清,更别谈为什么某个方案会把服务器打爆了。

这篇文章我就把自己这些年在这条路上踩过的坑、总结的经验全部倒出来,把短轮询、长轮询、WebSocket、SSE的原理、优缺点、适用场景掰开揉碎讲清楚,还会带上可以直接抄作业的代码片段和真实案例。无论你是刚入行的前端新手,还是要做技术方案评审的资深开发,这篇都能给你一个相对完整的参考坐标系。

1. 先搞清楚:HTTP 本身的"请求-响应"模型,是实时推送最大的敌人

想理解实时推送,先得明白一个基本前提:传统的 HTTP 协议是单向的、由客户端发起的。浏览器不发请求,服务器就不会搭理你,哪怕服务器那边已经炸成一锅粥,浏览器也毫不知情。这个机制在设计之初是为了简单可靠,但到了"实时"场景,就成了最尴尬的先天缺陷。

1.1 为什么"实时"在 Web 世界里这么难?

举一个生活化的例子:你让外卖小哥送餐,传统 HTTP 模式是你每过五分钟打电话问一次"送到了吗"——这就是轮询。你直接跟小哥加微信,他到了随时告诉你——这是 WebSocket。你打开外卖 App 的订单追踪页,平台主动往你屏幕上推更新——这是 SSE。

所以本质上,前端实时消息推送要解决的只有一个问题:**如何让服务器在有新数据时,能主动通知浏览器,而不是等浏览器反复来问。**而围绕"如何主动",业界给出的答案无非三条路:不断地问(轮询)、建立一条全双工管道(WebSocket)、建立一条服务器的单向广播管道(SSE)。

1.2 三种方案横评的第一印象

先把结论放在前面,方便你有个整体印象:

  • 轮询:兼容性满分,实现最简单,但浪费最严重,服务器压力随客户端数量线性上涨。
  • WebSocket:真正的全双工实时通道,功能最强大,但协议复杂、维护成本高、需要处理的心跳和重连都是额外负担。
  • SSE:轻量级单工推送,基于 HTTP,自动重连,对 AI 流式输出和通知类场景非常契合,但只能服务端往客户端推。

这三者没有绝对的好坏,只有合不合适。接下来的内容,我会把每个方案的原理、代码、坑点全部铺开来讲。

2. 轮询机制详解:短轮询与长轮询的伪装实时

轮询分两种:短轮询和长轮询。很多人以为轮询就是定时器加 setInterval,其实长轮询是一个完全不同的思路,两者的实时性和服务端压力差别很大。

2.1 短轮询:写起来最爽,炸起来也最爽

短轮询的实现几乎没有技术含量。前端每隔几秒发一个普通 HTTP 请求,后端查到数据就返回,没查到也返回空数据,然后前端继续下一轮:

// 前端短轮询示例 async function pollMessages() { try { const res = await fetch('/api/messages?after=12345'); const data = await res.json(); renderMessages(data); } catch (err) { console.error('轮询请求失败', err); } finally { // 无论成功失败,3 秒后发起下一次 setTimeout(pollMessages, 3000); } }

后端就是一个普通接口,毫无特殊处理。这种方式的优点是"向前兼容一切",哪怕你后端是个纯静态 JSON 文件都能配合,调试无比方便。但缺点也致命:大多数请求是无效请求。3 秒轮一次,如果数据每 30 秒才更新一次,那么 90% 以上的请求都在白白消耗带宽和服务端 CPU。

这里我吃过一个很大的亏。早期做过一个在线人数统计大屏,用的就是 1 秒一次短轮询。单个用户访问没问题,结果现场大屏一开、几百个客户端同时轮询,服务端直接被打到 CPU 100%。后来加了 200ms 到 800ms 的随机抖动(jitter),打散请求到来的节奏,才勉强撑住。所以如果非要用短轮询,请务必注意:轮询间隔不要固定,随机抖动一定要加,否则所有客户端在同一个时间点发起请求,会对服务端造成"惊群效应"。

2.2 长轮询:用"挂起"换来接近实时的体验

长轮询的思路比较巧妙。客户端发出请求后,服务端并不立即返回,而是把请求挂起来,等有新数据了再返回。客户端拿到数据后立刻发起下一次请求,形成一个"永远挂着一个请求"的状态:

// 前端长轮询示例 async function longPoll() { try { const res = await fetch('/api/messages/long-poll'); const data = await res.json(); renderMessages(data); } catch (err) { console.error('长轮询连接异常,3 秒后重新连接', err); await new Promise(r => setTimeout(r, 3000)); } // 关键:处理完数据后立即发起下一次请求 longPoll(); }

服务端侧,Java 可以用 DeferredResult,Node.js 可以暂存请求的 response 对象,等有数据到达时再 end。这样一来,客户端不需要反复发无效请求,数据的到达延迟被压缩到极低——只要服务端一有新数据,挂着的请求立刻返回。

长轮询的实时性其实相当不错,几年前的 Web 聊天室基本都是这个方案。但它的代价是把压力从"客户端频繁请求"转移到了"服务端大量挂起连接"。每个挂起的请求都占用一个连接、一部分内存,如果同时在线人数多,连接数会非常恐怖,部分服务器或云厂商的负载均衡还有连接数上限,很容易触发"连接耗尽"的问题。

2.3 轮询方案的适用场景与红线

根据我的实践经验,轮询比较适合以下场景:

  • 数据更新频率低、实时性要求不高,比如订单状态、审批进度。
  • 短生命周期项目或内部工具,不想引入额外复杂架构。
  • 目标用户环境老旧,不支持 WebSocket。

但有两个红线,碰了就翻车:

  1. 轮询间隔小于 1 秒——这个频率下大部分请求都在打空气,不如直接上长连接。
  2. 固定轮询间隔且客户端量大——一定要加随机抖动,否则流量峰值会把服务端打崩。

3. WebSocket 全解析:全双工管道的强大与代价

如果说轮询是"反复打电话问",WebSocket 就是"加微信实时聊"。它是目前唯一能让服务器主动、双向、实时收发数据的方案,也是 IM、协同编辑、游戏这类强交互应用的首选。

3.1 WebSocket 的握手与双工原理

WebSocket 的建立过程其实是在 HTTP 之上"借道"完成的。浏览器先发一个带 Upgrade 头的 HTTP 请求,服务端同意后返回 101 Switching Protocols,然后这条 TCP 连接就从 HTTP 协议切换成了 WebSocket 协议。之后双方都可以随时向对方发送数据帧,不再受"一问一答"的束缚。

// 前端 WebSocket 基础用法 const ws = new WebSocket('wss://api.example.com/ws'); // 连接建立 ws.addEventListener('open', () => { console.log('连接已建立'); ws.send(JSON.stringify({ type: 'join', room: '001' })); }); // 收到消息 ws.addEventListener('message', (event) => { const msg = JSON.parse(event.data); handleMessage(msg); }); // 连接关闭 ws.addEventListener('close', (event) => { console.log('连接关闭', event.code, event.reason); }); // 连接异常 ws.addEventListener('error', (err) => { console.error('WebSocket 错误', err); });

注意,WebSocket 原生支持发送文本和二进制数据(ArrayBuffer、Blob),这是 SSE 做不到的。做直播弹幕、实时画板、文件传输这类需要二进制传输的场景,基本只能靠它。

3.2 心跳机制的实现与作用

WebSocket 最大的坑在于:连接断开时,你和服务器常常都不知道。比如用户网络切到了 Wi-Fi,或者中间代理把空闲连接回收了,TCP 连接可能已经断了,但双方还傻乎乎地以为连接活着。这时候消息就会静默丢失。

解决办法就是心跳机制。我的通用做法是:每 30 秒发一个 ping,服务端回一个 pong,超过 3 次没收到 pong 就判定连接断开,触发重连。

// 心跳 + 断线重连完整示例 class RealtimeClient { constructor(url) { this.url = url; this.ws = null; this.heartbeatTimer = null; this.missCount = 0; this.reconnectAttempts = 0; this.reconnectDelay = 1000; this.connect(); } connect() { this.ws = new WebSocket(this.url); this.ws.addEventListener('open', () => { this.reconnectAttempts = 0; this.startHeartbeat(); console.log('连接建立,开始心跳'); }); this.ws.addEventListener('message', (event) => { const msg = JSON.parse(event.data); if (msg.type === 'pong') { // 收到 pong,重置连续未响应计数 this.missCount = 0; return; } this.handleMessage(msg); }); this.ws.addEventListener('close', () => { clearInterval(this.heartbeatTimer); this.scheduleReconnect(); }); this.ws.addEventListener('error', () => { // error 之后一般会触发 close,所以重连逻辑放在 close 里 console.error('连接异常'); }); } startHeartbeat() { this.missCount = 0; clearInterval(this.heartbeatTimer); this.heartbeatTimer = setInterval(() => { if (this.ws.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify({ type: 'ping' })); this.missCount++; // 连续 3 次未收到 pong,主动断开并重连 if (this.missCount >= 3) { console.warn('心跳超时,主动断开'); this.ws.close(); } } }, 30000); } scheduleReconnect() { // 指数退避,避免断连风暴 const delay = Math.min(this.reconnectDelay * Math.pow(2, this.reconnectAttempts), 30000); setTimeout(() => { this.reconnectAttempts++; this.connect(); }, delay); } }

这里有个细节值得多说一句:重连必须用指数退避(1s、2s、4s、8s……封顶 30s)。如果不这样做,服务端一旦抖动重启,所有客户端都会在同一秒疯狂重连,造成"重连风暴",直接把刚起来的服务再次打死。我见过不止一次线上事故是这么发生的。

3.3 WebSocket 的真实成本:Nginx、负载均衡、鉴权

WebSocket 看起来美好,但落地时有一堆基础设施要配合。

首先,如果用了 Nginx 反向代理,必须配置 Upgrade 头:

location /ws/ { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 3600s; }

其次,云厂商的负载均衡(SLB)默认可能不支持 WebSocket,需要单独开启相关选项。我遇到过几次线上连接 100% 失败,排查到最后发现是压测环境少开了 SLB 的 websocket 支持。

还有鉴权问题。WebSocket 的 URL 无法自定义 Header,只能通过 query 参数传 token:wss://api.example.com/ws?token=xxx。token 放在 URL 里会出现在日志中,有泄露风险,需要配合短期有效期 token 使用。

4. SSE 深度剖析:被低估的单向推送利器

SSE(Server-Sent Events)是三种方案里最容易被人忽略的一个。很多人一听到"服务端推送"就想到 WebSocket,却不知道 SSE 在某些场景下比 WebSocket 更适合、更省事。

4.1 SSE 到底是什么?

SSE 其实很简单:客户端用 EventSource 对象发起一个 HTTP 请求,服务器端不关闭响应,而是源源不断往响应体里写数据。数据格式有固定规范:

data: 这是一条普通消息\n\n event: orderStatus data: {"id": 123, "status": "shipped"}\n\n

每个消息以空行结尾,data 表示数据内容,event 表示自定义事件名。前端代码非常简洁:

// 前端 SSE 用法 const source = new EventSource('/api/events'); // 监听默认 message 事件(服务端只写了 data) source.addEventListener('message', (event) => { console.log(event.data); }); // 监听自定义事件(服务端写了 event: orderStatus) source.addEventListener('orderStatus', (event) => { console.log('订单状态变更', event.data); }); // 连接会由 EventSource 自动处理重连 source.addEventListener('error', () => { console.log('连接断开,EventSource 会自动重连'); });

最让我欣赏的一点是:SSE 的断线重连是浏览器原生支持的。只要连接断开,EventSource 会自动按服务端返回的 retry 时间(默认几秒)重新发起连接,完全不需要自己写重连逻辑。相比之下,WebSocket 的重连要自己折腾半天。

4.2 SSE 的保活机制与数据格式

这里必须讲一个非常容易踩的坑:SSE 连接如果一段时间没有任何数据,中间代理设备(Nginx 默认 60 秒)就会认为连接闲置,直接把连接断开。这种断开会表现成前端事件循环里不停触发 error 事件,而服务端那边其实什么都没干。

线上最常见的报错就是"stream disconnected before completion: idle timeout waiting for sse",这就是 Nginx 的 proxy_read_timeout 默认 60 秒无人数据传输导致断开的直接表现。

解决办法有二:

  1. Nginx 放宽超时时间:
location /api/events { proxy_pass http://backend; proxy_set_header Connection ''; proxy_http_version 1.1; proxy_read_timeout 3600s; proxy_buffering off; }

注意 proxy_buffering off 也必须加上,否则 Nginx 默认会把上游数据缓冲起来一次性转发,SSE 的实时性就没了。

  1. 服务端发送注释行保活:在服务端每 15~30 秒输出一个冒号开头的注释行,按 SSE 规范,这种行会被客户端忽略,但能起到保持连接活跃的作用:
: keep-alive\n\n

Node.js 服务里可以这样实现:

res.write(': keep-alive\n\n'); setInterval(() => res.write(': keep-alive\n\n'), 15000);

4.3 SSE 适合什么场景?

SSE 虽然是单向的,但"服务器往浏览器推"这个场景覆盖了大量业务需求:

  • AI 流式输出:ChatGPT 式打字机效果,服务端生成一段推一段。
  • 站内通知、消息提醒:右上角小红点、未读消息数。
  • 行情看板、监控数据:股票价格、服务器指标刷新。
  • 日志实时滚动:构建日志、任务执行进度。

这几个场景的共同点是:前端只需被动接收,不需要给服务器发送大量实时数据。如果业务需求恰好落在这个区间,直接用 SSE 会比 WebSocket 省掉大量协议层面的复杂代码。

5. 三种方案对比:选型其实有章可循

前面分别讲了三个方案的原理和代码,这一节我把它们放在同一张桌子上做一次全面对比,再给出我自己的选型决策思路。

5.1 核心特性对比表

对比维度短轮询长轮询WebSocketSSE
通信方向客户端→服务端客户端→服务端全双工双向服务端→客户端单向
实时性取决于轮询间隔,通常有秒级延迟几乎实时实时实时
连接开销每次请求都需要新建/销毁连接持续挂起一个连接一次握手,长期占用 TCP一次 HTTP 请求,长期占用
服务端压力高,请求频繁且大量无效中高,连接挂起占资源中,连接持久但消息可管控低,纯被动推送
二进制支持支持(HTTP 普通上传下载)支持支持不支持(纯文本)
自动重连无无,需自己实现无,需自己实现原生支持
协议复杂度无低高(有握手、帧协议、心跳)低(纯文本协议)
兼容性所有环境所有环境现代浏览器现代浏览器(IE 不支持)

从这张表能很清楚地看到:没有哪个方案是全面碾压的,每个方案都有自己的"主客场"。

5.2 我的选型决策流程

这几年前端方案做多了,我总结了一套非常朴素的选择方法,遇到实时推送需求,按这个顺序走基本不会翻车:

  1. 先问:服务器需要主动往浏览器推数据吗?
  • 不需要、只是定时刷新 → 短轮询就够了,别过度设计。
  • 需要 → 进入下一步。
  1. 再问:浏览器需要向服务器发送高频实时数据吗?
  • 需要(聊天、协同、游戏操作) → 选 WebSocket。
  • 不需要或频率很低 → 进入下一步。
  1. 问题三:数据内容是文本为主,还是可能有二进制?
  • 纯文本、JSON → SSE 是性价比最高的方案。
  • 有二进制(音频流、图像帧) → WebSocket 没得跑。
  1. 最后问:团队维护能力如何?
  • 小团队、追求开发效率、不想写心跳重连 → 优先 SSE。
  • 已有完善的基础设施、需要上生产级 IM → WebSocket。

5.3 一个具体的选型案例

举个例子,之前有个项目要做 AI 对话功能,需求是:用户发一个问题,服务端流式返回答案,期间前端要打字机式展示 token,不需要用户向服务端发送任何长连接数据。当时团队里有同事直接上了 WebSocket,我拦住他,理由很简单:这事的实时数据流只有一个方向,WebSocket 的双向能力完全用不上,反而还要自己实现心跳、重连、二进制帧解析。最后用 SSE 一个 EventSource 加 30 行后端代码搞定,上线之后跑得很稳,Nginx 层把超时调长就再没出过问题。

这就是典型的"杀鸡用了牛刀"。避免过度设计,是这一节最想传达的工程价值观。

6. 常见问题与排查技巧实录

最后这部分,我把三个方案在实际运行中会遇到的高频问题整理成一份速查表,全部来自真实线上事故,不是书上的理论。

6.1 问题排查速查表

现象涉及的方案原因解决方案
SSE 连接周期性断开,每隔约 60 秒重连一次SSENginx proxy_read_timeout 默认 60s 超时,且没配 proxy_buffering off调大超时并关闭缓冲;服务端加注释行保活
WebSocket 连接经常断开,close event 的 code 是 1006WebSocket1006 表示连接被异常关闭,常见原因是中间代理/防火墙强制断开加心跳保活;检查 Nginx Upgrade 配置;调整代理空闲超时
大量客户端同时轮询导致服务端 CPU 飙升短轮询客户端请求同步并发,形成惊群效应引入随机抖动(jitter)打散请求发起时间
长轮询连接数缓慢上涨,最终达到上限长轮询部分请求失败后客户端没有正确结束,或者服务端挂起后一直未释放服务端挂起请求必须设置超时兜底,超时返回空,让客户端重连
页面切到后台,WebSocket 一段时间后消息全丢WebSocket浏览器对后台标签页的 JavaScript 定时器降频,导致心跳发送变慢结合页面可见性 API(visibilitychange)在页面重新可见时立即检查连接状态
Nginx 返回 403 或 502,WebSocket 握不上WebSocket负载均衡或网关层未开启 Upgrade 支持检查 Nginx/网关配置,确认带 Upgrade 头的请求被正确转发
切换网络(Wi-Fi 切蜂窝)后,前端一直连不上WebSocket浏览器缓存了旧的 WebSocket 连接,或底层网络切换导致连接失效监听 ononline 事件,主动关闭旧连接并触发重连
SSE 收到的数据不实时,一推一整块SSENginx 默认开启 buffering,会把响应缓冲后再转发设置 proxy_buffering off
服务端推送频率高,前端事件触发顺序乱SSEEventSource 消息事件和 HTTP 响应顺序虽然固定,但自定义事件混在一起可能产生理解歧义统一报文结构,携带自增序号或时间戳,便于前端排序

6.2 关于"前端 SDK"与封装的一点体会

最后想多说一句和实时推送相关的前端工程化问题。不管选哪种方案,我都强烈建议封装一个独立的前端 SDK 或工具模块,不要在业务组件里直接 new WebSocket 或 EventSource。

我把自己的 SDK 设计成了三个统一的方法:connect()负责建立连接和心跳、on(event, callback)注册订阅回调、disconnect()主动断开。内部可以随意替换底层实现——今天用 WebSocket,明天换 SSE,业务方只需要改一行配置。这样做的最大好处是:当后端从 WebSocket 切换到 SSE,或者从长轮询升级到 WebSocket 时,业务代码不用改一行。

这个设计让我在好几个项目里都受益了。有一次客户的网络环境明文禁止 WebSocket,我们只改了几行配置就把整个实时通道从 WebSocket 降级成了 SSE,前端页面完全无感。这就是封装的价值,也是我建议每个前端团队在做实时推送功能前先想清楚的事情。

6.3 一个小型实时系统的实践记录

再分享一个最近做的实际案例吧,刚好把前面所有技术点串起来。系统是一个客服工作台,要求坐席端实时接收客户消息,同时有客服在相互转接时的高频状态变更。

我的最终方案是双通道并行:主要消息走 WebSocket,因为坐席要发送消息、接收消息、同时还要同步"正在输入"这类双向实时状态;系统通知和工单状态更新走 SSE,因为它只是服务器单向推送给坐席,EventSource 自动重连又省了我不少事。

上线前踩了一个印象很深刻的坑。压测时发现当 2000 个坐席同时在线时,WebSocket 的消息延迟从 5ms 飙升到 300ms。排查后发现是后端在处理心跳消息时触发了全量广播,把每条 ping 都广播给了所有人。后来调整心跳的发送频率到 30 秒一次,并且心跳消息只在服务端处理、不广播,延迟立刻降回正常。

这件事给我的教训是:在实时系统里,"消息的路由设计"比"连接层用什么协议"更影响性能。全双工的 WebSocket 固然能力最强,但如果业务上不区分消息类型、不加控制地广播,再好的协议也扛不住流量洪峰。

写在最后

我个人在实际项目里的默认选择是:**能上 SSE 就不上 WebSocket,能用长轮询就别碰短轮询。**实时推送最怕的不是技术难,而是过度设计和错误选型。上 WebSocket 之前先问自己两遍——真的需要双向吗?真的需要心跳重连的复杂度吗?如果答案是否定的,SSE 这个"半个 HTTP 请求"的轻量方案往往会给你带来惊喜。

再分享一个小技巧:不管用哪个方案,上线前都建议做一次真实的弱网测试,用 Chrome DevTools 的 Network 模拟 3G 网络,重点观察断线重连的表现。很多线上事故都不是逻辑错了,而是大家漏了"网络不是永远可靠"这个最基本的假设。把这个假设补上,你的实时系统就稳了一大半。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询