【后端开发|网络编程】—— 五种实时通信方案全解析:短轮询、长轮询、SSE、MQTT、WebSocket

从短轮询到 WebSocket:五种实时通信方案原理、对比与选型

做过"消息要立刻弹出来"这类需求的人都踩过同一个坑:第一版代码写成了 setInterval 每 3 秒拉一次接口。功能是通的,上线后发现接口 QPS 涨了 20 倍,而其中 99% 的请求返回的都是"没有新数据"。

这就是实时通信问题:服务端有了新数据,怎么尽快让客户端知道?

本文按"先搞懂范式 → 再逐个拆协议 → 最后给选型表"的顺序,把短轮询、长轮询、SSE、MQTT、WebSocket 这五种方案讲透。读完你应该能做到两件事:一是看别人架构图里的长连接方案不再懵;二是面对具体业务,能直接说出该选哪个、为什么。

🔴 一句话定位:这五种方案的差异,本质上只有两条------客户端多久问一次 ,以及连接能活多久


一、先厘清:三个范式和四个术语

不先把地基打好,后面讲每种协议都会是玄学。

1.1 万恶之源:HTTP 只会"拉"

HTTP 是请求-响应 模型:客户端发起请求,服务端应答,一问一答,服务端没有能力主动找客户端

两个现实约束加剧了这件事:

  1. 客户端通常在 NAT / 防火墙后面,没有公网可直连的地址,服务端想"反向连过去"基本不可能。
  2. 连接是有成本的:TCP 三次握手,HTTPS 还要加 TLS 握手,短连接下每次请求都要付这笔钱。

HTTP/1.1 的 keep-alive 能复用 TCP 连接,省掉重复握手,但它复用的只是"通道",语义上仍然是"客户端问、服务端答"的配对关系------服务端依旧不能凭空推一条消息下来。

所以要实现"实时",路只有两条:

思路 做法 代表方案
让客户端不停地问 客户端主动、高频、短请求 短轮询
让一条连接一直开着 服务端可以随时沿这条连接写数据 长轮询、SSE、WebSocket、MQTT

✅ 记住这条:推送不是"服务端凭空连过来",而是"客户端先开好一条长期存活的连接,服务端借着它说话"。

1.2 三种通信范式

范式 数据流向 特点 本文涉及
轮询(拉) 客户端定期拉取 实现最简单,实时性受周期限制,空请求多 短轮询、长轮询
长连接推送 连接建立后由服务端写 实时性高,但要处理连接保活、扩容 SSE、WebSocket
发布订阅 发布者 → broker → 订阅者 时间与空间解耦,一对多分发 MQTT

⚠️ 注意:长轮询是"伪装成拉的推",它借的还是 HTTP 请求-响应外形;SSE 和 WebSocket 才是真正意义上的长连接。

1.3 四个必懂术语

  • 单工 / 半双工 / 全双工:单工只能单向传输(如广播);半双工同一时刻只能一方发(如对讲机);全双工双方可同时收发。SSE 是单向的,WebSocket 是全双工的。
  • 长连接 vs 短连接:短连接一次请求用完就关,长连接建立后长期保持。长连接的代价从"QPS 高"变成了"连接数多"。
  • 心跳(Keep-alive / ping-pong):链路上的 NAT、负载均衡、反向代理会静默掐掉长时间空闲的连接。心跳就是定期发一个无业务含义的小包,证明"我还活着"。
  • 消息可靠语义:最多一次(可能丢)、至少一次(可能重复)、恰好一次(不丢不重,代价最高)。MQTT 的 QoS 就是这三个语义的标准实现。

二、短轮询:实现最简单,代价最贵

原理一句话:客户端定时发 HTTP 请求问"有新数据吗",服务端立刻回答"有"或"没有"。

javascript 复制代码
// 短轮询:每 3 秒问一次
setInterval(async () => {
  const res = await fetch('/api/messages?since=' + lastId);
  const list = await res.json();
  if (list.length) {
    render(list);          // 有新数据才渲染
    lastId = list.at(-1).id;
  }
}, 3000);

优点是真的简单:不需要任何服务端改造,不需要维护连接状态,网关、鉴权、日志体系全部照旧能用。

代价在服务端。算一笔账:

数值
在线用户 1000
轮询周期 3 秒
实际 QPS 1000 ÷ 3 ≈ 333
有效请求(真的拿到新数据) 假设 1%
无效请求 约 330 QPS 白跑

这 330 QPS 的空请求,每一次都会走完:网关路由 → 鉴权 → 业务逻辑 → 查库/查缓存 → 组装响应。用户越多,浪费越成比例放大。

⚠️ 很容易忘的一步 :如果只想知道"数据变了没",可以让服务端返回 ETag,客户端下次带 If-None-Match,没变化就回 304 Not Modified。响应体省了,但请求本身照样发生,只是止血,不是治本。

维度 评价
实时性 取决于周期,通常是秒级到分钟级
服务端压力 与"在线用户数 ÷ 周期"成正比,且大部分是无效请求
实现成本 极低
适用场景 实时性要求低(分钟级)、数据量小、内网管理后台、定时任务状态刷新

一句话总结:短轮询适合"能接受几秒延迟"的场景,别拿它做 IM 和订单状态。


三、长轮询:把"挂起"用到极致

原理一句话 :客户端发请求,服务端没有新数据就先不返回,把这个请求挂住;等有新数据、或者等到超时(比如 30 秒),才把响应写回去;客户端拿到响应后立刻再发一个新请求。

markdown 复制代码
客户端 → 服务端:有新消息吗?
服务端:(挂起,不响应......)
        20 秒后,来了一条新消息
服务端 → 客户端:有新消息,给你
客户端 → 服务端:那再问一次,还有吗?(立刻重发)

这一招把"服务端推送"伪装成了"客户端拉取",所以它对反向代理、防火墙、老网关极其友好------链路上所有设备看到的都只是一次"响应慢了点"的普通 HTTP 请求。

3.1 三个必须做对的地方

服务端必须能"挂起"请求,而不是阻塞线程。

如果你用的是同步阻塞的线程模型(每个请求占一个线程),长轮询会把线程池瞬间打满------1000 个在线用户就是 1000 个被占住的线程。正确做法是异步化:Servlet 3.0 的 AsyncContext、Netty 的 Channel、Node 的 await、Go 的 goroutine、Spring 的 DeferredResult 都属于这一类。

超时值要卡在中间。

超时设置 后果
太短(如 2 秒) 退化成短轮询,白折腾
太长(如 5 分钟) 连接和内存占用高,故障后恢复慢,重启时容易雪崩
业界常见 30 秒一档

hold 时间必须小于链路上所有超时配置。 这是最容易翻车的一点:

nginx 复制代码
# nginx:读超时必须大于服务端 hold 时间,否则 504
location /api/long-poll {
    proxy_read_timeout 60s;
    proxy_connect_timeout 10s;
}

⚠️ 顺带检查负载均衡的 idle timeout、云厂商 SLB 的连接空闲超时、客户端自己的请求超时。任何一层比服务端 hold 时间短,你都会看到规律性的 504。

3.2 谁在用长轮询

  • 配置中心:Nacos 1.x 的配置监听就是 30 秒长轮询------客户端带上 md5 问"配置变了吗",没变就挂住,变了立刻响应。到了 Nacos 2.x,这套机制升级成了 gRPC 长连接,同时保留约 5 分钟的全量拉取做兜底,防止推送丢失导致长期不一致。
  • 消息队列 :很多人以为 Kafka 是"broker 推给消费者",其实消费者一直是主动拉取的fetch.min.bytesfetch.max.wait.ms 组合出的效果就是长轮询:broker 没攒够数据就等到 fetch.max.wait.ms(默认 500ms)再返回。RocketMQ 的 push 消费模式,底层同样建立在长轮询之上。
维度 评价
实时性 准实时,数据一到就能响应
服务端压力 无数据时不产生无效请求,但每个客户端要占一个挂起连接
实现成本 中等(必须异步化 + 调超时)
适用场景 想推送但改造受限(老网关、只允许 HTTP)、配置中心、消息消费

一句话总结:长轮询是"用连接换效率"的过渡方案,兼容性最好,但连接数等于在线人数。


四、SSE:服务端单向推送的轻量之选

原理一句话 :SSE(Server-Sent Events)是浏览器原生支持的单向 推送------客户端建立一条普通 HTTP 连接,服务端保持它不关闭,持续往里写文本。协议就是 text/event-stream,浏览器用 EventSource 对象接收。

4.1 协议长什么样

服务端响应头:

http 复制代码
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

响应体是一段一直往后追加的文本,每条消息由若干字段行组成:

vbnet 复制代码
event: notice
id: 1001
data: {"msg":"你有新的工单"}

data: 这一行是默认的 message 事件

字段含义(以 MDN 的说明为准):

字段 作用
data: 消息内容。连续多行 data: 会被拼接,中间插入换行符
event: 事件类型,客户端用 addEventListener('notice', ...) 单独监听
id: 事件 ID,会写进 EventSource 的 last event ID
retry: 断线后客户端的重连间隔(毫秒)

4.2 杀手锏:自动重连 + 断点续传

这是 SSE 相比"手写长轮询"最大的优势,而且是浏览器免费送的

  • 连接断了,EventSource 会自动重连(绝大多数浏览器默认约 3 秒,可用 retry: 字段覆盖)。
  • 重连时,浏览器会把最后收到的事件 ID 放进 Last-Event-ID 请求头发过去。服务端读到它,就能从正确的位置继续发,不丢不重。

⚠️ 注意:Last-Event-ID 只是"送到你手上的接力棒",服务端要真能续传,你还得自己保存事件历史或偏移量。协议帮你把 ID 传过来了,不代表数据替你存好了。

4.3 三个必须知道的限制

  1. 单向。SSE 只能服务端→客户端。客户端的反馈数据要另开一个普通 HTTP 请求。
  2. HTTP/1.1 下连接数有限。浏览器对同一个域名的并发连接通常限制在 6 个左右,一个标签页开一个 SSE 就占一个额度;多开几个标签页,其他请求会被排队。上 HTTP/2 之后多路复用,这个问题基本消失。
  3. 不能自定义请求头EventSource 不支持设置 Authorization,常见做法是用 Cookie 鉴权,或者干脆用 fetch + ReadableStream 自己解析 text/event-stream

🔴 头号部署坑 :链路中间的 nginx 默认开启 proxy_buffering,它会把你一条条写的消息攒着不发。表现为"本地一切正常,上了线上消息卡住半天才出来"。

nginx 复制代码
location /api/sse {
    proxy_buffering off;          # 关键
    proxy_cache off;
    proxy_read_timeout 3600s;     # 长连接别被读超时打断
    proxy_set_header Connection '';
    proxy_http_version 1.1;
}

也可以由应用在响应里加 X-Accel-Buffering: no,让 nginx 对这条响应关闭缓冲。

谁在用 :监控面板、日志实时流、任务进度条、站内通知,以及你每天都在用的------大模型流式输出 。OpenAI 兼容接口里的 stream: true,返回的就是 text/event-stream

维度 评价
实时性 高,服务端一写客户端就收到
服务端压力 每客户端一条长连接,比长轮询的占用更轻(无重复请求头)
实现成本 低,浏览器原生支持,自带重连
适用场景 单向流:日志、通知、进度、Token 流式生成

一句话总结:只需要服务端往客户端推,且不要求 IE 系浏览器,SSE 是性价比最高的选择。


五、WebSocket:一次握手后的全双工

原理一句话 :客户端先用一个 HTTP 请求申请"协议升级",服务端同意后(101 Switching Protocols),这条 TCP 连接就不再走 HTTP 了,双方改用通信,任何一方都能随时主动发数据。

5.1 握手:从 HTTP 丝滑切换到另一个协议

客户端请求(RFC 6455 里的标准示例):

http 复制代码
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

服务端响应:

http 复制代码
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

设计得挺讨巧:借用 HTTP 完成握手 ,所以能天然穿过只认 HTTP 的代理和 80/443 端口;握手完就脱离 HTTP 语义,所以之后不再有请求头开销。Sec-WebSocket-Accept 是服务端用固定算法把客户端的 Sec-WebSocket-Key 算出来的,用来证明"我确实懂 WebSocket 协议",不是随便哪个 HTTP 服务器都能答应的。

5.2 帧、掩码与心跳

握手之后,数据以**帧(frame)**为单位传输。一个数据帧的头部最小只有 2 字节,对比 HTTP 每次请求动辄几百字节的头部------这就是 WebSocket 做高频双向通信时开销小的根本原因。

  • 掩码(Mask):客户端发往服务端的帧必须做掩码处理,这是协议强制的,用来防止恶意的代理缓存投毒。
  • 控制帧 :协议自带 ping / pong / close 三种控制帧,ping 就是标准心跳。

⚠️ 必须自己加心跳。NAT、负载均衡、反向代理会掐掉"长时间没有数据"的连接(常见是 60 秒空闲就断)。如果你的业务平时很安静,不心跳就会被静默断开,用户看到的现象是"莫名其妙就不推消息了"。

javascript 复制代码
// 应用层心跳 + 指数退避重连,这是长连接客户端的标配
let retry = 1000;
function connect() {
  const ws = new WebSocket('wss://example.com/ws');
  ws.onopen = () => { retry = 1000; setInterval(() => ws.send('{"type":"ping"}'), 30000); };
  ws.onclose = () => { setTimeout(connect, retry); retry = Math.min(retry * 2, 30000); };
}

5.3 真正的代价在运维

WebSocket 的连接是有状态的:用户 A 连在节点 1,用户 B 连在节点 2,A 给 B 发消息时,节点 1 必须把消息转给节点 2。

问题 常见解法
跨节点广播 Redis Pub/Sub、NATS、Kafka 做消息总线
会话粘性 网关按连接做一致性路由,或干脆让节点间互相转发
单机连接数 调文件描述符、内存、内核参数,做压测确定容量
鉴权 握手阶段校验 Token(连上之后再鉴权会很别扭)
安全 必须用 wss://,并校验 Origin 防跨站劫持

谁在用:IM 聊天、协同编辑(多个光标实时同步)、实时行情、在线游戏、需要客户端高频上报的场景(如白板、直播弹幕)。

维度 评价
实时性 最高,双向、低延迟
服务端压力 连接数即成本,扩容需要配套的消息总线和网关
实现成本 中高(客户端重连、服务端集群、监控都要自己做)
适用场景 双向高频交互

一句话总结:WebSocket 是能力最强、也最贵的一个------如果你的场景其实只需要单向推,先想想 SSE。


六、MQTT:物联网的发布订阅标准

原理一句话 :MQTT 是 OASIS 标准、专为物联网设计的轻量发布订阅 协议。设备不直接互相通信,而是统一连到一个 broker (消息代理),发布者往主题发消息,broker 负责转发给所有订阅了该主题的客户端。

6.1 先搞懂发布订阅

text 复制代码
发布者 ──publish(topic: sensor/room1/temp, payload: 26.5)──▶ broker
                                                             │
订阅者 A ◀── sensor/room1/+ ─────────────────────────────────┤
订阅者 B ◀── sensor/# ───────────────────────────────────────┘

这套模型解耦了两件事:

  • 空间解耦:发布者不知道订阅者是谁、有几个、在不在线。
  • 时间解耦:订阅者掉线重连后,可以补收离线期间的消息(持久会话)。

主题(Topic)用 / 分层,支持两种通配符:+ 匹配单层,# 匹配多层。

6.2 QoS:消息可靠性分三档

等级 语义 交互 适用
QoS 0 最多一次 发完就忘,不确认 传感器周期上报,丢一条无所谓
QoS 1 至少一次 PUBLISH → PUBACK,未确认会带 DUP 重发 大部分业务指令,能接受重复
QoS 2 恰好一次 PUBLISH → PUBREC → PUBREL → PUBCOMP 四次握手 计费、扣款等不能重复的场景

⚠️ 两个容易踩的点:

  1. QoS 是分段协商的 。它分别存在于"发布者→broker"和"broker→订阅者"两段链路上,由发布时的 QoS 和订阅时申请的最大 QoS 共同决定。订阅端申请的 QoS 更低时,消息会被降级转发。
  2. QoS 1 的重复是协议层面无法避免的。重传机制决定了接收端无法区分"这是一条新消息"还是"重传的旧消息",所以业务侧必须自己做幂等,别指望 QoS 1 帮你消重。反过来,无脑全用 QoS 2 会显著增加握手开销和延迟,吞吐直接掉一档。

6.3 为什么物联网选它

  • 报文极小:最小固定头部只有 2 字节,弱网和低功耗设备扛得住。
  • 弱网友好:持久会话 + 遗嘱消息(设备异常掉线时由 broker 代发通知),专门为不稳定的蜂窝网络设计。
  • 一对多天然分发:一次上报,多个订阅方各取所需,不用点对点建连。
  • 安全:支持 TLS 加密和 OAuth 等现代认证方式。

和 WebSocket 是什么关系? 不冲突,是两层东西:MQTT 是应用层的消息模型 (跑在 TCP 上),WebSocket 是传输通道 。现实里最常见的组合是:设备端用 MQTT over TCP,浏览器前端用 MQTT over WebSocket------同一套 broker、同一套主题,Web 端也能收发。

谁在用:智能家居、车载、工业设备、物流追踪、共享设备,以及几乎所有"海量低功耗终端 + 不稳定网络"的场景。

维度 评价
实时性 高,broker 即时转发
服务端压力 broker 需支撑海量连接,但单连接开销极小
实现成本 中(要引入并运维 broker,规划主题与 QoS)
适用场景 IoT 设备接入、弱网、需要分级可靠性的消息分发

七、横向对比与选型

7.1 一张表看完

维度 短轮询 长轮询 SSE WebSocket MQTT
通信方向 客户端拉 服务端"伪推" 服务端→客户端单向 双向全双工 双向(经 broker)
底层协议 HTTP HTTP HTTP(text/event-stream TCP(HTTP 握手升级) TCP(可跑在 WebSocket 上)
实时性 周期级 准实时 实时 实时 实时
无效请求
连接开销 无长连接 每客户端 1 个挂起连接 每客户端 1 条长连接 每客户端 1 条长连接 每客户端 1 条连接
浏览器原生支持 是(EventSource 是(WebSocket 否(需 SDK)
自动重连 不涉及 需自己写 内置 需自己写 SDK 内置
代理/网关友好度 最好 好(注意关闭缓冲) 需支持 Upgrade
运维复杂度 高(集群/广播/保活) 中高(broker 集群)
典型场景 状态刷新、后台看板 配置中心、消息消费 日志流、通知、LLM 输出 IM、协同编辑、行情 IoT 设备接入

7.2 按场景给结论

你的场景 选它 理由
实时性只要分钟级,接口能扛住 短轮询 一行 setInterval 就能跑,改造成本最低
想推送,但网关/客户端只认 HTTP 长轮询 兼容性最好,所有基础设施不用动
只有服务端往客户端推,要自动重连 SSE 原生支持、自带重连和断点续传、运维最轻
双向、低延迟、浏览器或 App 长连接 WebSocket 能力最全,但要接受运维权衡
海量设备、弱网、消息可靠性要分级 MQTT 专为此设计,单连接开销最小、支持持久会话

八、常见误区与避坑

误区 后果 纠正
把短轮询周期设为 1 秒当"实时"用 QPS 线性爆炸,服务端被打满 周期和实时性要求对齐;能改推送就改
在同步阻塞模型里做长轮询 线程池被挂起的请求占满,服务整体雪崩 必须异步化(AsyncContext / Netty / goroutine)
长轮询 hold 时间大于网关超时 规律性 504,用户以为是网络问题 逐层检查 nginx、SLB、客户端超时,hold 时间要最小
SSE 前面有 nginx 没关缓冲 消息攒着不发,本地正常线上卡顿 proxy_buffering off;X-Accel-Buffering: no
WebSocket 不做心跳 空闲 60 秒被中间设备静默掐断,消息"莫名丢了" 应用层 ping/pong + 指数退避重连
MQTT 全链路无脑 QoS 2 四次握手开销,吞吐和延迟双输 按业务语义选:能重复的用 QoS 1 + 幂等
只用 WebSocket 做单向推送 白白背上集群、广播、保活的复杂度 单向流优先评估 SSE

九、总结:你真正需要记住的 6 件事

  1. HTTP 只能拉,推送必须靠"一直开着的连接"。理解这一条,五种方案就都不是黑盒了。
  2. 它们的差异只有两个变量:客户端多久问一次、连接能活多久。
  3. 选型的速记:短轮询 = 简单、长轮询 = 兼容、SSE = 单向省心、WebSocket = 双向、MQTT = 设备规模。
  4. 连接一长,运维成本就从 QPS 转移到连接数与保活:心跳、超时配置、集群广播、容量压测,一样都不能少。
  5. 任何长连接方案都必须配齐三件套 :心跳、重连、消息补发(SSE 的 Last-Event-ID / MQTT 的持久会话 / 业务层的自增 seq)。
  6. 先看方向(单向还是双向)和规模(几千还是百万连接),再看实时性要求。顺序反了,多半会选重。

验证清单

  • 能用一句话说清你选的方案:数据方向是什么、连接能活多久、靠什么保活
  • 长轮询/SSE 服务端的 hold 时间 小于 链路上所有层(nginx、SLB、客户端)的超时
  • SSE 路径的 nginx 已关闭 proxy_buffering,并实测"服务端写一条,客户端立刻收到"
  • WebSocket 有应用层心跳,并手动断网验证过自动重连与消息补发
  • MQTT 每个主题的 QoS 都有明确理由,业务侧对 QoS 1 做了幂等处理
  • 用真实并发压测过长连接容量,知道单机上限在哪

参考资源

相关推荐
坤岭1 小时前
LLM应用安全护栏实战
后端
程序猿阿越1 小时前
containerd如何创建Pod
后端·kubernetes·源码阅读
名字还没想好☜1 小时前
Python sqlite3 实战:事务提交、参数化防注入、WAL 并发与 row_factory 取字典
后端·python·编程语言
花间相见1 小时前
【AI应用开发|Agent记忆】—— Codex 与 Claude Code 持久化记忆对比:两条不用向量库的路线
后端·agent
葡萄城技术团队1 小时前
GcExcel V9.2 新特性揭秘:公式对象的无损往返
后端
蜗牛互联网1 小时前
Claude放宽生命科学限制:代价是验证、分级和30天留存
java·人工智能·后端
上进小菜猪1 小时前
被 32 位整数卡了 30 年脖子:PostgreSQL 事务号困局与 V9 的 64 位解法
后端
知识的搬运工旺仔3 小时前
CREATE INDEX CONCURRENTLY:线上建索引不阻塞 DML 的代价与坑
数据库·后端·sql