实时通信方案对比:从轮询到WebSocket与SSE的选型指南

发布时间:2026/10/21 5:17:34
实时通信方案对比:从轮询到WebSocket与SSE的选型指南 做后台服务这几年我几乎每隔一段时间就会被同一个问题卡住服务端的数据变了怎么让客户端第一时间知道最典型的就是消息通知、订单状态变化、后台任务跑完、行情刷新。客户端不会自己刷页面它只会等在那里而HTTP又天生是“你问我答”的模型。想做“服务端主动推送”就得绕开这个模型。这篇文章我想把市面上真正能落地的几种推送方式从头到尾捋一遍轮询、长轮询、WebSocket、SSE顺带聊一下MQTT和WebTransport这类进阶选择以及我实际选型时踩过的一些坑。适合刚接触实时通信的前后端开发也适合正在纠结“到底该用哪种方案”的架构师。看完你至少能回答两个问题每种方案的本质是什么我当前这个业务到底该用谁。1. 先说清楚HTTP凭什么不能主动推送1.1 请求-响应模型与“推送”需求的根本矛盾HTTP协议从设计之初就是客户端发起请求服务端返回响应然后连接要么复用、要么关闭全程由客户端主导。这个模型天然适合“用户主动查数据”却不适合“服务端主动给数据”。服务端想推送本质上是要在某个事件发生时让服务端能主动向客户端“发起通信”——这跟HTTP的底层假设是拧着的。所以你能见到的所有推送方案本质上都是绕路要么让客户端频繁来问要么把连接挂住不释放要么直接升级协议。理解了这一点后面所有技术选型和方案细节都有了解释依据。很多人一上来就选WebSocket理由只是“它能推送”但没想过WebSocket的连接管理成本、代理兼容问题、心跳保活成本最后上线还是出幺蛾子。1.2 最朴素的“假推送”客户端轮询先别笑轮询真的是最简单可靠的“推送”方案。它的逻辑就是把“服务端主动通知”改写成“客户端反复来问”。前端用一个定时器每隔几秒请求一次接口// 短轮询示例前端每5秒拉一次订单状态 async function pollOrder(orderId) { setInterval(async () { const res await fetch(/api/order/${orderId}); const data await res.json(); if (data.status ! pending) { // 拿到最终结果停止轮询 clearInterval(this); } }, 5000); }服务端完全不需要额外设计普通HTTP接口就行。它的优点非常明显实现难度为零、调试方便、任何网络环境都能跑、服务器也不需要维护长连接状态。缺点同样致命实时性取决于轮询间隔间隔设太短服务端压力大设太长用户又觉得“怎么没反应”。更隐蔽的问题是无效请求占比极高。比如订单十分钟内可能只变一次状态客户端却每5秒拉一次全是在拉同一份数据。要减轻负担就得配合HTTP缓存和条件请求响应头带ETag或Last-Modified下次轮询时客户端把If-None-Match带上服务端判断没变化就返回304减少body传输另外可以把多个查询合并成一个批量接口减少请求次数。1.3 什么场景现在还能接受轮询别觉得轮询老土它反而是我最常推荐的“偷懒方案”。实时性要求在10秒以上、客户端数量不大、快速交付优先的项目轮询完全够用。比如后台任务进度、日报生成状态、扫描任务结果用户本来就不会盯着刷新一分钟内知道结果就够了。轮询的另一个优势是状态恢复简单拉数据是幂等的断网恢复后重新拉一次一定能拿到最新状态不用维护连接状态也不用担心断线重连。所以我的经验是当你能用一个普通HTTP接口解决“等结果”的问题时永远不要引入WebSocket。等哪天产品明确说“服务端要实时推送且不能等5秒”再升级方案也不迟。2. 长轮询把“拉”伪装成“推”的极限2.1 长轮询的握手逻辑服务端“挂住”请求短轮询的问题是每次请求都得立刻返回哪怕没有数据。长轮询的思路反过来了客户端发请求服务端不急着响应而是把这个请求挂着hold住等有新数据了再响应如果等太久还没数据就返回一个超时或“无变化”让客户端立刻发起下一次请求。这个机制的好处是实时性明显提升——数据一产生服务端就能立刻通过那个挂着的连接把响应发出去客户端几乎同时收到。请求次数也少了一个时间段内只有一个挂起的请求在等而不是每5秒一次。2.2 用Node.js写一个最小可用的长轮询Node.js的异步模型挺适合演示长轮询。服务端维护一个“挂起请求”集合有数据来了就把对应请求响应掉const pendingClients new Set(); app.get(/events, (req, res) { // 先把这次请求挂住 pendingClients.add(res); // 客户端断开时从集合里移除避免泄漏 req.on(close, () { pendingClients.delete(res); }); // 超时保护最多30秒没有数据也要返回一次 const timer setTimeout(() { if (pendingClients.has(res)) { pendingClients.delete(res); res.end(JSON.stringify({ type: timeout })); } }, 30000); }); function broadcast(data) { for (const res of pendingClients) { res.end(JSON.stringify(data)); } pendingClients.clear(); }这里有个容易被忽略的细节close事件必须监听否则客户端断开后res还留在集合里等广播时写一个已经断开的连接轻则报错重则内存泄漏。另一个细节是超时清理没有超时机制僵尸请求会在服务端无限堆积。2.3 长轮询的坑资源占用、代理超时与重连风暴长轮询最大的问题在服务端资源上。虽然异步模型下“一个挂起请求”不占一个线程但它仍然会占用连接文件描述符、请求上下文内存、事件监听器。当一个服务同时挂几万个请求资源开销就很可观了。更麻烦的是长轮询对部署环境要求高中间的Nginx默认proxy_read_timeout是60秒连接挂久了会被掐断浏览器对并发连接数也有限制——HTTP/1.1每个域最多6个并发连接如果一个页面同时有多个长轮询连接数很容易就占满。还有一个特别容易翻车的点是“重连风暴”。长轮询在规定时间内没等到数据客户端需要立刻发起下一个请求如果服务端刚重启或发生发布所有客户端几乎同时收到超时或断开然后同时发起请求瞬时流量能把服务端打趴。解决办法是客户端在重连时加随机延迟比如0到3秒之间随机等待一下。长轮询现在使用频率确实低了但在某些内网环境、旧系统兼容场景里仍然存在。比如我维护过的一套老系统出于兼容老旧浏览器脚本的限制只能用长轮询。排查线上问题时我们遇到过连接挂在半路一直没返回最后发现是中间代理闲置超时把连接掐断但客户端并不知道。这种“连接看似存在实际早已失效”的问题是长轮询排查里最耗时的部分只能靠服务端加更短的超时主动断开并让客户端尽快重连来缓解。3. WebSocket真正把连接“升级”成全双工3.1 握手不是玄学从HTTP Upgrade说起短轮询和长轮询走得再远本质还是请求响应。WebSocket则完全不同它把HTTP连接升级成了一种独立的、全双工的长连接协议双方在一条TCP连接上可以随时互发消息。握手过程其实很朴素客户端发一个普通HTTP GET请求带上Upgrade: websocket和Sec-WebSocket-Key服务端验证后返回101 Switching Protocols之后就切换成WebSocket协议通道。这个设计其实很聪明它复用HTTP的端口和握手机制所以能走大多数现有的HTTP服务器、防火墙和代理只是后续流量不再是普通HTTP。这里有个容易疏忽的细节握手请求是可以携带Cookie和Header的所以鉴权可以在握手完成前做也可以做在应用层消息里。实际项目里我推荐尽量在握手阶段做鉴权——校验失败直接拒绝升级这样就不会建立无用的连接。3.2 帧、掩码与消息分片协议里值得了解的细节WebSocket连接建立后消息以“帧”frame为单位在网络上传输。帧里有关键标志FIN表示这是不是消息的最后一帧opcode表示帧类型文本帧、二进制帧、连接关闭、ping、pong等以及一个“掩码”位。掩码是很多人忽略但很有意思的机制客户端发送到服务端的帧必须进行掩码处理。为什么因为要防止一种攻击场景如果客户端发送的帧不掩码攻击者可能通过构造恶意TCP包把数据伪装成HTTP请求塞给中间代理让代理误以为这是一次新的HTTP请求缓存投毒。客户端的帧经过掩码后中间代理无法解析出可预判的字节流攻击难度就大了。服务端到客户端的帧不需要掩码因为服务器端的输出相对可信而且解掩码也算一次不必要的CPU开销。消息分片也是要点一个大消息可以被拆成多个数据帧发送第一个帧是起始帧FIN0opcode为首帧类型中间帧是继续帧最后一帧FIN1。这块平时写业务代码不用关心但做底层库或排查“消息怎么发不完整”时会遇到。3.3 一个最小WebSocket服务端长什么样用Node.js的ws库写一个广播服务五分钟就能跑通const WebSocket require(ws); const wss new WebSocket.Server({ port: 8080 }); // 维护在线客户端集合 const clients new Set(); wss.on(connection, (ws) { clients.add(ws); ws.isAlive true; ws.on(pong, () { ws.isAlive true; }); ws.on(message, (data) { // 收到客户端消息按业务处理 // 比如加入房间、发聊天消息等 console.log(received:, data.toString()); }); ws.on(close, () { clients.delete(ws); }); }); // 每30秒检查一次心跳 const heartbeat setInterval(() { for (const ws of clients) { if (!ws.isAlive) { ws.terminate(); continue; } ws.isAlive false; ws.ping(); } }, 30000);ws.isAlive这个标记配合ws.ping()的组合是WebSocket服务端最基础的心跳保活模板。它的逻辑是定期对客户端发ping收到pong的客户端说明还活着没收到pong的就调用terminate()把死连接清理掉。没有这个机制TCP连接的“半开”状态会让服务端堆积一堆垃圾连接连接数越涨越高迟早把文件描述符耗尽。3.4 心跳与断线重连上线之后真正要命的事很多初次接触WebSocket的团队会忽略心跳直到线上出现“连接没断但消息发不过去”的现象才反应过来。原因通常是这样的客户端和服务器之间可能有Nginx、云负载均衡、防火墙之类的中间层这些节点会主动清理空闲连接。默认参数下Nginx的proxy_read_timeout是60秒意味着60秒内没有任何双向数据连接就被回收了。客户端看不到断开事件因为TCP的断开通知在多数场景下不会立刻触发——除非客户端主动发数据或收发超时。解决办法只有一个应用层心跳。客户端每隔20~30秒发一次心跳包可以是ping帧也可以是业务层的保活消息服务端配合超时清理。心跳间隔必须小于中间层超时的最小值。如果Nginx超时60秒心跳就得在30秒左右发一次而不是掐着60秒发。这算是我踩过最经典的坑第一个版本心跳设的30秒以为没问题结果代理超时是20秒线上连接全部被静默杀掉页面看起来还挂着但推送已经全断了。3.5 WebSocket不是架起来就完事鉴权、负载均衡与内存WebSocket部署环境比普通HTTP接口苛刻。首先是负载均衡一个连接建立后不能随便迁移到另一台后端所以Nginx或负载均衡器需要开启会话保持比如ip_hash或基于WebSocket连接的亲和策略否则同一个用户前后两次请求落到不同机器推送就发不到了。其次是内存每个WebSocket连接除了TCP连接本身还得为应用层维护会话对象、发送缓冲区、心跳定时器。按一个连接几十KB算一台8GB内存的服务器撑不了多少连接。真要支撑大规模在线通常得把网关和业务拆开网关层维护连接业务层通过消息队列向网关投递推送指令。鉴权方面常见做法有两种握手阶段校验Cookie或Header里的token失败就拒绝升级或者握手时先不校验建立连接后客户端发认证消息服务端在有限时间内校验超时未通过就断开。第二种能支持更多客户端类型比如原生App不走Cookie但要注意必须在服务端加“认证超时断开”逻辑避免一堆未认证连接占着资源。光这一条就能排掉线上不少问题。4. SSE最容易被低估的单向推送利器4.1 SSE的本质还是HTTP只是响应不结束Server-Sent EventsSSE其实是HTTP协议内的一种“作弊”用法服务端返回一个Content-Type: text/event-stream的响应然后不结束这个响应持续往里面写数据。客户端浏览器通过EventSource对象来接收这个流。比起WebSocketSSE最大的特点是“自动重连”。EventSource在连接断开后会自己重新发起连接还能通过Last-Event-ID头告诉服务端“我上次收到哪条消息了”服务端可以据此补发。对大多数“服务端单向给客户端发通知”的业务来说这一整套机制几乎白送不用自己写重连逻辑也不用处理心跳——服务端定期往流里写一个注释行以:开头的行就能维持连接活跃。4.2 一个最小的SSE服务端用Node.js写SSE特别直白app.get(/events, (req, res) { res.setHeader(Content-Type, text/event-stream); res.setHeader(Cache-Control, no-cache); res.setHeader(Connection, keep-alive); // 这条注释行是心跳防止代理把空闲连接断开 const heart setInterval(() { res.write(: heartbeat\n\n); }, 15000); const timer setInterval(() { const data { time: new Date().toISOString() }; res.write(data: ${JSON.stringify(data)}\n\n); }, 1000); req.on(close, () { clearInterval(timer); clearInterval(heart); }); });客户端配合超简单一个API搞定比WebSocket的前端代码少一截const source new EventSource(/events); source.onmessage (event) { console.log(收到推送:, event.data); };SSE的线格式有几个关键点每条消息以data:开头消息之间用空行\n\n分隔如果需要事件类型加一行event: order-status如果要携带ID加id: xxx浏览器会在重连时自动带上Last-Event-ID。很多初学SSE的人会忘记空行结果数据全粘在一起解析不出来。4.3 SSE的坑编码、代理缓冲与连接数限制SSE用起来简单坑也不少。第一个坑是数据格式text/event-stream只支持UTF-8文本想传二进制得自己base64或转成JSON字符串这是协议限制不能绕。第二个坑是中间代理缓冲Nginx默认会缓冲响应导致数据迟迟不落给客户端需要配置proxy_buffering off或者响应头带上X-Accel-Buffering: no。第三个坑是连接数限制HTTP/1.1下浏览器对同一域名的连接数限制通常是6个一个页面开多个EventSource会把连接数占满影响其他请求。HTTP/2下这个问题会好很多因为多路复用允许一条连接承载多个流但实际部署时还要看代理和浏览器兼容。第四个坑和WebSocket有点像SSE看起来是长连接但一样会被代理超时掐断。好在这里有协议内的心跳办法——定期写一行注释:开头就行浏览器收到注释行不会产生任何事件但连接上有数据流动代理就不会判定超时。把心跳间隔设在低于代理超时基本可以稳跑。4.4 哪些场景别再用WebSocket了SSE适合一切“服务端单向推送、客户端不需要通过同一连接发送数据”的场景。比如后台任务进度条、新闻/公告推送、行情页面的价格更新、AI聊天场景里的流式输出——现在很多大模型的对话响应就是SSE实现的因为LLM生成文本是一个持续的流客户端只是被动接收完全不需要启动WebSocket。有人会问既然SSE是单向的那客户端想发消息怎么办简单用普通HTTP POST发就行推送走SSE、上行求情走HTTP各干各的。这套组合在很多场景下比WebSocket更省事服务端不用维护双向协议解析客户端有自动重连连接也走的80端口代理穿透性好。我自己的原则是只要没有“服务端也要实时收到客户端消息”的硬需求一律先考虑SSE而不是WebSocket。5. 进阶选择MQTT、WebTransport与系统级推送5.1 MQTT物联网和弱网环境的主场MQTT是一种轻量级的发布/订阅消息协议用在移动端和物联网设备尤其多。它和WebSocket不是同一个层次的东西WebSocket是连接通道MQTT是消息协议需要建立在TCP或WebSocket之上通过Broker转发消息。MQTT的核心价值在于三个设计一是QoS等级消息可以至少一次、正好一次或最多一次投递业务可以在可靠性和开销之间做取舍二是主题订阅发送方不需要知道接收方在哪发到主题就行接收方订阅主题双方解耦三是保留消息Broker能给新上线的订阅者立即发送最新一条消息这对状态类数据非常友好。前端其实也能跑MQTT——通过WebSocket连接Broker。但在浏览器里跑MQTT同样要处理连接保活、重连订阅恢复等问题并没有比直接WebSocket省事。它更适合的场景是大量设备、弱网环境、需要消息可靠投递的物联网业务。如果只是Web页面的实时通知没必要引一个Broker进来。5.2 WebTransportQUIC时代的下一站WebTransport是基于QUIC协议的新一代传输协议支持多路复用、双向流、不可靠数据报。和WebSocket相比它最明显的优势是不再受TCP队头阻塞影响且通过封装的数据报可实现低延迟的“尽力投递”通信适合游戏、音视频、高实时互动这类场景。但它的浏览器兼容和中间代理支持目前仍然有限生产环境大量使用还需时间。我倾向于把WebTransport理解成一个“值得持续观察”的方向不适合现在就拿来做主力推送方案。绝大多数业务实时通信WebSocket和SSE已经足够除非你的产品对延迟和吞吐有极致要求、并且有专门团队愿意折腾底层。5.3 别忘了系统级推送App在后台/离线时怎么办还有一类容易被遗漏的“推送”移动端App在后台被系统挂起、或者完全离线时WebSocket和SSE都无能为力。此时只能靠系统级推送通道——iOS的APNsAndroid的系统推送服务以及各厂商的推送通道。这类推送由移动操作系统统一维护连接应用本身不参与长连接管理也就不会因为进程被杀而失去通知能力。实际项目里通常的架构是“在线走长连接离线走系统通道”App在前台且网络正常时用WebSocket或SSE收发实时数据切后台后服务端判断连接断开转而通过推送通道发一条通知把用户拉回前台。这个双轨设计看起来简单做起来却是另一门学问涉及厂商通道的token管理、消息有效期、厂商配额等等。如果产品要的是“用户不在线也能收到提醒”那WebSocket解决不了必须接系统推送。6. 按场景选型怎么想清楚“到底用哪个”6.1 一张表看穿四种主流方案维度短轮询长轮询WebSocketSSE实时性差取决于间隔较好事件立刻返回极好全双工双向好服务端流式写通信方向只拉近似推挂起请求双向单向服务端到客户端连接开销低但请求量大中挂起请求占资源高长连接心跳会话中一条长HTTP响应浏览器兼容极好好好较好EventSource全支持重连机制自己控制自己控制需自行实现浏览器自带Last-Event-ID中间代理友好度极好一般需调超时一般需开upgrade一般需关缓冲实现复杂度最低中高低适用业务低频状态老系统、受限环境聊天、游戏、双向实时通知、进度、流式输出举几个典型场景作为参考在线聊天、多人协作编辑一定选WebSocket因为需要双向实时站内通知、活动弹窗、任务进度优先SSE代码量和运维成本小物联网设备上报和下发控制选MQTT更合适旧系统或受限于内网代理的项目长轮询依然是备选。6.2 选型前先问自己四个问题第一问服务端到底需不需要实时接收客户端消息需要就WebSocket或MQTT不需要就SSE别为了“以后可能要用”多背WebSocket的运维成本。第二问实时性要求到多少级别毫秒级、秒级还是分钟级分钟级的用户感知轮询就够了秒级优先SSE只有“事件发生到客户端看到必须几乎同时”才需要WebSocket。第三问连接数和客户端类型是什么纯浏览器页面、同时在线几万SSE或WebSocket要看运维能力移动App还得叠一套离线推送通道海量IoT设备MQTT的Broker模型比自建WebSocket网关省心太多。第四问你的部署环境撑得住吗多个后端实例有没有会话保持Nginx代理层有么有调整过超时有没有监控连接数的看板这几个问题往往决定了方案上线后的稳定性比协议本身的选型更关键。我见过不少项目需求只是“订单状态变了要提醒用户”结果上了WebSocket前后端开发量翻倍线上连接数却没监控和告警最后心跳没配、死连接堆积一个月后服务OOM。冷静评估后这类需求用SSE反而干净得多。7. 上线之后最常翻车的三件事与排查思路7.1 连接看似正常推送却悄无声息这是我见过最多的线上故障症状是客户端连接显示正常服务端日志也有在线连接但消息发出后客户端收不到或者过一段时间后连接才断开。最常见的根因就是中间层静默断连Nginxproxy_read_timeout默认60秒云负载均衡也可能有TCP空闲超时连接长时间没数据传输就被回收两边都不知道。排查链路可以这样走先看Nginx错误日志和访问日志确认有没有101升级记录、有没有超时主动断开再在服务端给每个WebSocket连接记录最后一次收发数据的时间超过阈值就主动断开并告警最后确认应用层心跳间隔是否小于所有中间层超时。强烈建议在开发环境就把Nginx和负载均衡配置一起搭起来别等线上才暴露这类问题。7.2 连接数和内存异常增长推送服务刚上线时可能只有几百连接一切正常搞一次大促或活动连接数几分钟冲上来内存和文件描述符双双告警。这里要检查的不只是连接数还有并发写缓冲区当服务端向大量慢客户端推送时发送缓冲区会积压内存占用远高于连接本身的固定开销。经验做法是做三层防护第一层是连接数上限和内存监控告警第二层是推送风控——向所有用户广播消息时别一次性全量发送要分片推送观察各片响应第三层是服务端把“生成消息”和“发送消息”分离消息生成后丢进队列发送端按连接吞吐能力消费避免一个慢客户端拖垮整个推送进程。7.3 服务端重启引发的“重连风暴”服务端发布重启所有客户端连接同时断开如果客户端没有合理的重连策略它们会在同一瞬间重新发起握手服务端刚起来就被打爆。这个现象在WebSocket和SSE里都可能发生SSE因为有浏览器自带重连更明显。解决思路其实不复杂客户端重连时加指数退避和随机抖动第一次失败等1秒、第二次等2秒、第三次等4秒最多加到30秒每次叠一个0到1000毫秒的随机量。服务端发布时可以分批重启让连接分波次重连。这些策略看起来是客户端的事但一旦线上发生服务端和客户端开发都得坐下来一起改提前做好能少熬好多夜。7.4 怎么测试推送链路才算完整单元测试只能验证逻辑推送链路的问题多半出在网络和代理层。我常用的验证方式有几种用弱网工具模拟丢包、延迟和抖动观察客户端有没有重连把手机切后台、锁屏、切Wi-Fi到流量看连接恢复后有没有做到断线重连和消息补偿在服务端定期主动断开一部分连接模拟设备挂断看客户端能否在预期时间内重新建立连接。另外一定要在预发环境跑一轮“活动流量”压测高并发下心跳定时器、广播循环、内存回收这些平时看不见的问题都会暴露出来。说到最后推送方案永远没有“一招通吃”。WebSocket适合双向高频通信SSE省心省力适合单向通知轮询和长轮询看似古老但限定场景下依然好使。我个人的习惯是产品提“实时推送”需求时先不急着上WebSocket而是把方向、实时粒度、客户端数量、部署环境全部问清楚再决定走哪条路。这类坑我踩过不少写这篇文章也是想帮你少走一遍。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询