☰
实时消息推送方案对比:WebSocket、SSE、轮询等7种技术选型指南
2026/10/13 7:40:43 网站建设 项目流程

我之前在某个项目群里被问到过一个问题:“网页上怎么实时推送消息?”讨论的人里有写后端接口的,有做前端的,还有负责部署的,各说各的好处和坑,最后谁也说服不了谁。后来我把能想到的方案整理了一遍,列出来发现居然有7种,而且每一种都不是冷门偏方,是真有人在实际业务里用的。今天我就把这7种方案一次性讲透,包括原理、适用场景和我在真实项目里踩过的坑。

如果你正处于“需要一个实时消息功能,但不知道该怎么选”的阶段,这篇内容可以直接当决策参考用。文章里我不会只讲“用WebSocket”,因为轮询、长轮询、SSE在某些场景下反而更合适。我把话放在前面:所谓最优方案,是结合业务需求、并发规模、客户端环境、部署成本共同决定的。

1. 别急着写代码,先分清楚“实时”到底是什么

1.1 “实时”是个相对词,业务的延迟指标要先对齐

我遇到过不少需求方提“要实时推送”,但你问他多实时,其实没概念。真到了技术选型,第一件事不是选框架,而是确认延迟指标体系。

  • 如果是通知类应用,比如订单状态变化、系统报警,2秒内能收到已经满足需求;
  • 如果是聊天软件,理想状态是毫秒级,至少不能让用户明显感觉“卡了一下”;
  • 如果是协同编辑、游戏对战、K线行情这类场景,500毫秒的延迟都可能是事故。

延迟从低到高排列:WebTransport、WebSocket、SSE、长轮询、以及垫底的短轮询。但延迟越低,通常意味着架构越复杂、部署和运维成本越高。所以先别头脑一热做什么“全球实时推送”,先用一句话把延迟指标写清楚,再往下走。

1.2 消息方向决定了你的一半选型

除了延迟,第二件事是确认消息方向。

方向有三种:

一是服务端到客户端的单向推送,典型场景是通知、进度、日志流、K线图更新。这种场景,SSE几乎是性价比天花板,因为协议天然支持服务端主动推送,前端只需要一个EventSource对象。

二是客户端到服务端的单向上传,典型场景是埋点上报、终端监控,这本质上不需要服务端推,普通HTTP接口或批量提交就够,不讨论。

三是双向实时通信,典型场景是IM、客服、协同白板、在线游戏。这种必须上WebSocket,或者基于WebSocket的封装方案。SSE不行,因为它在设计上就是单向的。

很多人选型失败,就是没把方向想清楚。曾经有个同事坚持用WebSocket做服务端日志流,理由是“够实时”。实际用SSE,代码量能少一半,断线重连还是内置的,不需要自己撸心跳。

1.3 客户端环境和并发量都是隐藏的约束

再往下,你要问自己两个问题:用户在什么环境里打开页面?同一时间在线多少人?

如果面向PC端的内部管理系统,IE都还有存量用户,那WebSocket要考虑降级。如果面向普通移动浏览器,SSE和WebSocket都没问题。如果是小程序,WebSocket几乎是唯一选择,因为小程序的EventSource支持并不友好。

并发量也要问。几百个连接和几十万连接是完全两套玩法。前者单机部署就能跑,后者要做网关层、连接管理、消息分发集群。我之前在一家接入设备管理的项目里,就是高估了在线设备数,照着几万并发设计了一半,结果实际一天最高才几十个连接,白白增加维护成本。

所以,先把技术之外的选型变量做完,再进方案比较环节。以下七种方案,我按历史演进和实现复杂度排序来说。

2. 短轮询和长轮询:最原始但用对场景也不难用

2.1 短轮询:前端定时去问服务端

短轮询是最朴素也最容易想到的方案。页面里放一个定时器,每隔几秒或几十秒向后端接口发一次请求,问一句“有没有新数据”,有就把数据取回来,没有就等下一轮。

前端代码大概是这个样子:

setInterval(async () => { const res = await fetch('/api/messages'); const data = await res.json(); if (data.list.length) { renderMessages(data.list); } }, 5000);

短轮询的优点极其直观:实现成本几乎为零,不需要复杂协议,后端就是一个普通接口。而且它在不同浏览器、不同网络环境里都能正常工作。

它的缺点也是摆在台面上的:绝大多数请求都是无意义请求。假设业务没有新消息,前端还是每5秒来问一次,服务端要白白处理这些空请求。如果在线人数上到一定程度,这种无效请求会把服务端资源吃得很厉害。同时延迟也不理想,要求5秒内收到,轮询间隔就至少要3秒,意味着一天下来要向服务端发出大量请求。

所以我的建议是:短轮询只适合临时方案、Demo、内部工具,以及消息频率极低且对延迟不敏感的场景。比如后台管理系统的任务状态刷新,5秒轮一次,用户完全没有感知,这样的场景用短轮询完全没问题,没必要引入额外的实时架构。

2.2 长轮询:服务端学会“憋着不回复”

长轮询是短轮询的改良版,解决的就是无效请求过多的问题。

它的大体流程是这样:客户端发起请求,服务端收到请求后不立刻返回,而是把请求挂起。如果服务端在这段时间内收到新消息,就立刻把这个消息作为响应返回;如果等了一段时间(比如20秒)还是没有新消息,就返回一个超时响应,客户端收到后再立刻发起下一次请求。

连接的处理方式变了,但前端代码依然很简单:

async function longPolling() { try { const res = await fetch('/api/long-polling'); const data = await res.json(); if (data.message) { renderMessage(data.message); } } catch (e) { // 网络波动时等待后重连 } finally { longPolling(); } }

服务端要做的关键事情就是“挂起请求”和“按时返回”。在Node.js里可以写成一个等待队列:

const pendingClients = []; app.get('/api/long-polling', (req, res) => { const timer = setTimeout(() => res.json({ timeout: true }), 20000); pendingClients.push({ res, timer }); }); // 有新消息时,把所有挂起的请求立刻解锁 function dispatch(message) { pendingClients.forEach(({ res, timer }) => { clearTimeout(timer); res.json({ message }); }); pendingClients.length = 0; }

长轮询相比短轮询,实时性好了非常多,消息到达后服务端能立刻返回,客户端再发新请求,中间只有极短的切换空窗。但它依旧有瓶颈:

一是每个挂起的请求都会占用服务端连接资源和内存。在连接数大的情况下,服务端线程池或者事件循环的任务数很容易被打满。二是HTTP连接反复建立和释放,虽然比短轮询少很多请求,但相比WebSocket仍然有额外开销。三是很多中间件、网关对挂起的HTTP请求有超时限制,比如某些反向代理默认60秒就切断连接,需要配置调整。

长轮询在WebSocket普及之前,是很多IM应用的底层方案。现在它的定位更像是一种兜底策略:当WebSocket不可用、SSE又因为并发连接数受限时,长轮询依然能给出接近实时的体验。

2.3 轮询这组方案给我的教训

我用长轮询做过一个客服工作台。当时前端同时开了多个页面,系统里还有些旧代码也用轮询,结果服务端活跃连接数直接顶到了上限。排查了很久才意识到,不是代码Bug,是每个挂起的请求都在占资源。后来做了连接合并,同一个用户的多个页面尽量共用一条长轮询链路,才算缓解。

这个教训给我的经验是:轮询类方案的资源账单不是看你写了多少接口,而是看同时挂起多少个连接。部署的时候一定要把网关层的连接超时、进程的并发模型都考虑进去,否则上线第一天就可能被打爆。

3. 隐藏iframe和HTTP流式输出:被遗忘的老实人

3.1 iframe流:用脚本标签“灌”数据

在WebSocket出现之前,除了长轮询,还有一种思路很特别:用隐藏的iframe加载一个不关闭的HTTP长连接,服务端把数据以chunked编码方式持续输出,每次输出一段<script>标签,iframe里执行的脚本通过parent对象调用父页面的回调函数。

渲染出来的页面大概是这样的:

<iframe style="display:none" src="/api/comet-stream"></iframe>

服务端每一次推送,就往流里写一段这样的内容:

res.write(`<script>parent.pushMessage(${JSON.stringify(data)})</script>\n`);

因为<script>标签在HTML解析过程中会被立刻执行,所以parent.pushMessage就能实时更新页面。这个方案在当年确实能用,而且不需要任何现代API,兼容性极好。

3.2 它为什么被淘汰了

如果你细想一下,就会发现这种方案的问题很严重。

首先,iframe的URL会一直处于加载状态,浏览器页面上那个标签页的加载图标会一直转个不停,用户观感极差。其次,服务端需要保持一个长连接持续输出,如果中途网络断开,这个iframe并不会自动重连,需要额外写逻辑处理。再者,它依赖<script>标签执行这种暗黑技巧,数据不是通过标准消息接口传入的,调试、权限控制都很别扭。

后来SSE出现以后,这套基于iframe的方案迅速被替代。SSE提供了标准的事件流协议,天然支持断线重连,前端API也简洁。我在前几年接手过一个遗留项目,里面就有这种iframe流,用户反馈页面时不时卡顿,标签栏一直在转圈。后来改成SSE,体验直接提升了一个档次。

所以iframe流在今天的价值主要是当“反面教材”或者简历里的冷知识。如果你正在维护老系统,看到这种方法,应该考虑把它替换掉,而不是继续叠加补丁。

4. SSE:看着方向是单向,但能扛下绝大多数推送业务

4.1 EventSource是怎么做到“服务端推”的

SSE全称Server-Sent Events,中文叫服务器发送事件。它的核心思想是:客户端通过EventSource对象发起一个HTTP请求,服务端返回text/event-stream类型的响应并保持连接不关闭,之后服务端可以随时往这条连接里写数据。

前端用起来极其简单:

const source = new EventSource('/api/stream'); source.onmessage = (event) => { console.log('收到消息', event.data); }; source.onerror = () => { // EventSource内置自动重连机制,一般不用自己处理 };

服务端也很直接。在Node.js里,可以这样:

app.get('/api/stream', (req, res) => { res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', }); const timer = setInterval(() => { res.write(`data: ${JSON.stringify({ time: Date.now() })}\n\n`); }, 1000); req.on('close', () => clearInterval(timer)); });

协议格式用一句话解释:每一条消息分成若干行,字段名是data、id、retry等,消息之间用空行隔开。常规推送只需要关注data和id,前者是消息内容,后者用来追踪最后一条收到的事件ID,配合Last-Event-ID请求头可以实现断点续传。

4.2 SSE的自动断线重连是个省心的设计

我发现很多第一次用SSE的开发者会忽略一个高价值特性:EventSource对象在连接断开后会按retry字段指定的时间自动重新连接,而且可以带上Last-Event-ID告诉服务端“我最后收到的是哪条数据”,服务端据此补发遗漏的消息。

这个能力在消息通知里极其好用。用户网络闪断一下,SSE能自己恢复。WebSocket不行,需要开发者在业务里自己管理重连状态、心跳检测、丢弃消息的补偿逻辑。单是这一点,就让我在很多非聊天场景里坚决不用WebSocket。

当然,使用SSE有两个明显的约束需要提前说清。

一个约束是推送方向单向。服务端可以推,客户端不能用EventSource发业务消息给服务端。如果需要发,就另外发HTTP请求。好在大多数业务本来就不要求双向。

另一个约束是浏览器并发连接数限制。HTTP/1.1时代,一个域名下浏览器默认最多维持6个连接。如果你同时开了多个SSE流,或者页面里还加载了其他资源,连接可能会被占满。解决办法有两个:一是用HTTP/2让多个EventSource连接复用同一个TCP连接;二是把SSE集中成一个订阅通道,前端再做一个轻量的事件分发,避免每个组件都开一个EventSource。

4.3 SSE在今天的黄金应用场景

基于最近几年的项目经验,SSE最适合的是和“内容产出”相关的实时流式场景。比如大模型对话时逐字输出、后台任务日志实时滚动、监控面板指标刷新、活动页面中奖通知飘窗。这些场景的共同特点就是:不需要用户反向发消息,服务端单向推送就够,且断线重连要求高。

之前做过一个数据报表系统,需要实时推送几十台设备的指标变化。当时的初版方案选了WebSocket,结果不仅后端要维护一堆连接状态,前端还要处理重连逻辑,开发周期拉得很长。后来改成SSE,一个EventSource订阅统一通道,服务端每条消息按设备ID分发给前端对应组件,代码量骤减,稳定性也上去了。

这里插一句踩坑经验:SSE在反向代理后面有个大坑,就是缓冲被打开。某些反代默认会缓存上游响应,导致SSE数据积压到一定量才一次性发给浏览器,实时性完全被破坏。必须在代理配置里关掉缓冲,比如proxy_buffering off。我遇到过好多次运维配错了参数,前端明明没断,但消息延迟几分钟。

5. WebSocket:双向实时通信的标准答案

5.1 一次握手,永久双工

WebSocket从本质上看是一次HTTP协议的Upgrade升级。客户端先发一个正常的HTTP请求,请求头里带上Upgrade: websocket,服务端返回101状态码,之后这条TCP连接就从HTTP协议切换成了WebSocket协议,双方都能随时向对方发送数据。

这种设计带来的体验是跨越式的。在WebSocket之前,双向通信要么靠轮询模拟,要么靠两个独立连接拼凑。在WebSocket之后,聊天、游戏、协同编辑这些高实时业务终于有了体面的实现方案。

前端使用WebSocket的代码大家应该都写过:

const ws = new WebSocket('wss://example.com/ws'); ws.onopen = () => { ws.send(JSON.stringify({ type: 'join', room: 'demo' })); }; ws.onmessage = (event) => { console.log('收到', event.data); };

在后端,Node.js里比较流行的是ws库:

import { WebSocketServer } from 'ws'; const wss = new WebSocketServer({ port: 8080 }); wss.on('connection', (socket) => { socket.on('message', (data) => { // 广播给所有客户端 wss.clients.forEach((client) => { if (client.readyState === 1) { client.send(data.toString()); } }); }); });

协议层面,WebSocket的消息帧分为文本帧和二进制帧,文本帧通常承载JSON数据,二进制帧适合传输图片、音频、协议缓冲区等数据。消息还自带掩码、分片机制,这些都是底层库处理好的,业务层只需要关心收发字符串或字节。

5.2 别把HTTP的无状态习惯带进WebSocket

很多人第一次从HTTP转型到WebSocket会犯一个习惯性错误:认为每次消息都可以独立鉴权。HTTP确实是这样,但WebSocket连接是长期的,鉴权只在握手时做一次。如果连接建立之后没有额外的校验逻辑,一旦一个连接被劫持,所有消息都处于裸奔状态。

所以在WebSocket项目里,设计上有两个绕不开的核心问题。

第一个是心跳检测。TCP连接不会自动告诉你“对端还活着”,网络断开、设备休眠、代理超时都可能导致连接对象还挂在服务端,实际上已经死了。业界通行做法是客户端每隔一段时间发送一个Ping帧,服务端回复Pong帧,超过一定时间没收到就在应用层把连接清掉。在ws库里可以设置心跳参数,如果连接长时间没有数据交互,就主动判断它断了。

第二个是关连后的重连和消息补偿。WebSocket没有内置自动重连,前端断线后需要自己封装重连逻辑。重连之后,断线期间产生的消息缺失怎么处理,也需要消息队列或请求补偿机制配合。

5.3 和SSE的正面比较

WebSocket和SSE不是替代关系,它们各有各的主场。

从通信方向来看,WebSocket是全双工,SSE是半双工。从协议复杂度看,WebSocket是独立协议,SSE仍然跑在HTTP之上。从功能丰富度看,WebSocket能收发二进制,SSE天然只支持文本。从断线重连看,SSE内置重连,WebSocket需要自己实现。从服务端实现成本看,WebSocket需要额外的库支持,SSE一个普通HTTP接口就能完成。

所以我的决策方式是这样的:所有涉及用户主动发消息的场景,比如聊天、客服、白板协作,直接WebSocket。纯服务端推送场景,比如通知、日志、指标刷新,先看SSE能不能覆盖,能用SSE就不碰WebSocket,省下的复杂度不是一点半点。

Real-time消息量特别大,或者需要服务端主动下发指令时,WebSocket还是最终走向,因为它不只是“推送工具”,而是一条双向管道,你可以基于它做RPC、做协议扩展、做弹幕、做远程控制。

6. 两种特殊思路:Socket.IO封装与WebTransport新选择

6.1 Socket.IO:把实时通信的脏活累活包了

严格说Socket.IO并不是一种独立的底层协议,它在内部优先使用WebSocket,同时实现了HTTP长轮询降级。但它足够常用,业界也一直把它当作一套完整的实时通信方案来讨论,值得单独算一种。

Socket.IO最突出的价值是让开发者的心智负担大幅降低。它自带命名空间和房间,方便做分组广播。自带ACK回调,可以让服务端确认“我收到了你的消息”。自带心跳保活和断线重连,不需要自己实现这些繁琐逻辑。还支持携带元数据的事件收发架构,写业务代码更像在写事件监听。

前端使用是这样的:

import { io } from 'socket.io-client'; const socket = io('https://example.com', { transports: ['websocket', 'polling'], }); socket.emit('joinRoom', 'room-1'); socket.on('newMessage', (msg) => { console.log(msg); });

后端在Node.js里的房间广播也很直接:

import { Server } from 'socket.io'; const io = new Server(3000); io.on('connection', (socket) => { socket.on('joinRoom', (room) => socket.join(room)); socket.on('sendMessage', (payload) => { io.to(payload.room).emit('newMessage', payload.message); }); });

如果项目需要在短时间内搭建一个多房间、多事件类型的实时服务,Socket.IO的成长曲线非常平缓。但要注意:它体积不小,而且如果你用框架自带的事件机制做超大数据量广播,性能的瓶颈会来得比较快。更复杂的大型项目,依然倾向于直接用WebSocket自研协议,按需进行二进制协议优化和连接管理。

建议是:中小型实时业务直接考虑Socket.IO,追求极致性能和连接控制的大型系统,再考虑裸WebSocket。

6.2 WebTransport:面向未来的低延迟选项

WebTransport是一个比较新的网络传输协议,基于HTTP/3和QUIC运行。它在设计上支持在同一连接上复用多个独立的流,支持单向流和双向流,底层使用UDP传输,绕开了TCP的队头阻塞问题。

和其他方案相比,WebTransport最大的优势是更低的连接建立延迟,以及内建的多路复用。在游戏指令、高频金融市场数据、音视频信令这类场景,它理论上比WebSocket更有优势。

但现实情况是它还比较年轻。不只是浏览器兼容性覆盖不够全面,服务端基础设施、反向代理、负载均衡都需要额外适配。在生产环境中大规模落地WebTransport的项目还不多,很多团队也还在观望。

我给WebTransport的评价是:值得关注,但近一两年内别指望它能全面替代WebSocket。如果你是在做创新项目,性能敏感度极高,可以考虑试点尝试。如果只是做常规业务,老老实实用WebSocket更稳妥。

7. 七种方案的同场对比和实战选型模型

7.1 一张表说清楚它们的差别

下面这张表是我在写设计文档和方案评审时常用的对比口径,今天直接放出来:

方案消息方向实时性浏览器兼容性重连机制服务端实现成本适用场景
短轮询客户端拉取差,取决于间隔极好天然循环请求最低低频状态刷新、临时Demo
长轮询服务端应答拉取较好极好循环请求中有天然断线重试较低老系统兼容、WebSocket不可用降级
iframe流服务端推送到脚本好较好但体验差无低已被淘汰,不建议新项目使用
SSE服务端单向推送优现代浏览器均支持内置自动重连低通知、日志、指标、流式输出
WebSocket双向通信优广泛但需考虑降级需自行实现中等IM、客服、协作、交互游戏
Socket.IO双向通信优自动协商降级内置中低短周期快速上线的实时业务
WebTransport双向流/多路复用优有限尚不成熟高研究型项目、超低延迟场景

7.2 遇到具体业务时,怎么快速做决策

这里我给出一个选型顺序,适合绝大多数常规业务:

第一步,确定通信方向。如果服务端不需要主动推,就用普通接口;如果只需要服务端推,先选SSE;如果需要双向,进入第二步。

第二步,根据延迟和开发周期判断。延迟要毫秒级、客户端可控、需要做复杂交互,用WebSocket,第三方库优先考虑Socket.IO。延迟2~3秒能接受、客户端环境老旧,用长轮询。

第三步,看运维能力与基础设施。WebSocket要求网关、负载均衡器都能支持长连接,如果内部设施比较陈旧,选SSE或长轮询更省心。

第四步,如果业务面向的未来场景极低频,又追求极致性能,可以立项调研WebTransport。

这套顺序我给过不少同事,基本能帮人在没有架构师的情况下做出合理判断。

7.3 从我项目经历里总结的几个关键提醒

最后,把我在实际项目里踩过的坑集中说一遍,每一个都是真金白银换来的教训。

第一个坑,是SSE连接数。某个管理后台同时在线三百多人,每个用户开两个标签页,因为没用HTTP/2,连接数直接把浏览器限制打满,页面里其他接口的请求全部排队堵塞。后来把SSE集中成一个共享通道,前端做事件总线分发才解决。

第二个坑,是WebSocket服务端没有及时清理死连接。用户关掉电脑显示器,TCP连接没有正常断开,服务端维护的连接对象越来越膨胀,内存增长肉眼可见。加了心跳检测和超时清理之后才稳下来。

第三个坑,是长轮询在反代层被截断。内部网关默认60秒空闲超时,长轮询挂起45秒没问题,但一旦消息恰好过了某个时间点才到达,响应已经在网关层被掐断了,前端收到的是连接错误。这个问题的排查方向一度让人头秃,后面看到抓包数据才意识到是网关层干的。

第四个坑,是消息可靠性。任何推送方案都只能保证“送达连接”,不能保证“业务成功处理”。客户端收到消息后,解析失败、渲染出错、存库失败,该丢还是丢。所以业务上需要重要消息时,最终兜底一定是一个普通查询接口,让客户端在关键节点主动拉取一次做对账。

写给你的最后一张实操地图

现在的实时消息推送技术栈,真正该组合使用,而不是单靠某一种方案走天下。以我个人经验来看,值得默认配置的一套组合是:

常规服务端推送走SSE,业务双向通信走WebSocket,存量老页面用长轮询兜底,消息可靠性依赖“推送加对账接口”。这个组合能覆盖绝大多数场景,而且每种方案的复杂度都被限制在最小的范围内。

真到了新项目启动时,建议先花半天时间把业务延迟、消息方向、客户端环境、并发规模这四个问题问清楚,再打开这篇文章对照选择。技术方案没有高低之分,能在合适场景里解决问题的,才是好方案。

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

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

立即咨询