从 HTTP Hack 到 IM 基础设施:长轮询技术原理与实践解析

目录

[一、HTTP 请求-响应模型与"服务端推送"的天然冲突](#一、HTTP 请求-响应模型与“服务端推送”的天然冲突)

[(一)HTTP 的设计初衷](#(一)HTTP 的设计初衷)

(二)短轮询:最直接,也最粗暴的解法

[1. 最早的工程解法](#1. 最早的工程解法)

[2. 短轮询的致命问题](#2. 短轮询的致命问题)

[二、长轮询:对 HTTP 的一次"工程级 Hack"](#二、长轮询:对 HTTP 的一次“工程级 Hack”)

(一)核心思想:把"等消息"的时间放到服务端

(二)长轮询的交互特征

(三)长轮询的典型应用场景

[三、Java 服务端的挑战:线程模型与异步 Servlet](#三、Java 服务端的挑战:线程模型与异步 Servlet)

[(一)同步 Servlet 的瓶颈](#(一)同步 Servlet 的瓶颈)

[(二)异步 Servlet 的引入(Servlet 3.0)](#(二)异步 Servlet 的引入(Servlet 3.0))

四、长轮询系统架构设计:从"伪长连接"到服务解耦

[(一)接口模型设计:Send / Polling 的职责划分与工程语义](#(一)接口模型设计:Send / Polling 的职责划分与工程语义)

[1. Send 接口:标准 HTTP 上行通道](#1. Send 接口:标准 HTTP 上行通道)

[1.1 接口特征](#1.1 接口特征)

[1.2 设计原因](#1.2 设计原因)

[2. Polling 接口:基于异步 Servlet 的下行通道](#2. Polling 接口:基于异步 Servlet 的下行通道)

[2.1 基本交互语义](#2.1 基本交互语义)

[3. 异步 Servlet:Polling 接口的技术基石](#3. 异步 Servlet:Polling 接口的技术基石)

[3.1 常见的异步 Servlet 实现方案](#3.1 常见的异步 Servlet 实现方案)

[3.2 异步 Servlet 带来的核心价值](#3.2 异步 Servlet 带来的核心价值)

[4. 超时机制:长轮询系统的"安全阀"](#4. 超时机制:长轮询系统的“安全阀”)

[4.1 服务端超时策略](#4.1 服务端超时策略)

[4.2 客户端超时策略](#4.2 客户端超时策略)

[4.3 双向超时的工程意义](#4.3 双向超时的工程意义)

[5. Send / Polling 拆分的整体收益](#5. Send / Polling 拆分的整体收益)

(二)服务解耦:拆分长轮询与长连接的职责边界

[1. 引入代理服务与缓存层,实现职责解耦](#1. 引入代理服务与缓存层,实现职责解耦)

[2. 缓存 Redis 中的核心数据结构设计](#2. 缓存 Redis 中的核心数据结构设计)

[3. 解耦带来的工程价值](#3. 解耦带来的工程价值)

五、消息收发核心设计说明

[(一)Token 化的"伪长连接"模型](#(一)Token 化的“伪长连接”模型)

[1. Token 的作用](#1. Token 的作用)

[2. 消息收发流程总结](#2. 消息收发流程总结)

(二)性能优化:避免"无脑轮询"

(三)稳定性设计:超时与保活

[1. 超时机制](#1. 超时机制)

[2. Token 心跳](#2. Token 心跳)

[六、长轮询 vs WebSocket:工程视角的理性对比](#六、长轮询 vs WebSocket:工程视角的理性对比)

七、结语:技术会演进,工程思维不会过时

参考资料


干货分享,感谢您的阅读!

在即时通信(Instant Messaging,IM)系统中,"消息的实时性"几乎是一条不可妥协的底线。无论是即时聊天、系统通知,还是扫码登录、实时监控,用户都期望服务端一旦产生事件,客户端能够立刻感知

然而,在 Web 技术早期,这个看似简单的需求,却长期面临一个根本性的技术矛盾:

浏览器是被动的,而即时通信要求主动推送。

在原生桌面应用或移动 App 中,客户端可以直接使用操作系统提供的 Socket API,与服务端建立一条基于 TCP 的长连接。一旦连接建立,双方即可在这条连接上进行真正的双向、实时通信

但浏览器不同。

早期浏览器严格受限于 HTTP 协议的请求-响应模型:

  • 浏览器只能主动发起请求

  • 服务端不能主动向浏览器推送数据

  • 一次请求只能对应一次响应

这使得 Web 端的即时通信,在很长一段时间里只能"逆着协议设计的方向"实现。

正是在这样的背景下,长轮询(Long Polling*作为一种工程妥协方案,被广泛采用,并支撑了包括 Facebook IM、Web QQ、早期微博私信系统在内的大规模 Web 即时通信。

本文将结合 通用IM通信服务 在真实业务中的工程实践,系统解析长轮询技术:

  • 它为何出现

  • 它是如何工作的

  • 它解决了什么问题,又引入了哪些新的问题

  • 在大规模 IM 系统中,如何通过架构设计规避其天然缺陷

一、HTTP 请求-响应模型与"服务端推送"的天然冲突

(一)HTTP 的设计初衷

HTTP 是一种典型的 Request--Response 协议,其核心假设是:

  • 客户端发起请求(Request)

  • 服务端立即返回响应(Response)

  • 连接生命周期短暂、明确

这套模型非常适合:

  • 页面加载

  • 表单提交

  • REST API 调用

但它并不适合事件驱动模型

在即时通信场景中,事件的发生往往由服务端触发,例如:

  • 对方发送了一条新消息

  • 群聊中有人@你

  • 后台系统状态发生变化

浏览器却无法在"没有请求"的情况下接收这些事件。

(二)短轮询:最直接,也最粗暴的解法

1. 最早的工程解法

非常直接:既然服务端不能主动推,那我就让浏览器不断地问。

浏览器以固定时间间隔(如 1s、2s)不断向服务端发起请求:

Request -> Response
Request -> Response
Request -> Respons

服务端每次请求:

  • 查询一次是否有新消息

  • 无论有没有,都立即返回

这种方式被称为 短轮询(Short Polling)

2. 短轮询的致命问题

  • **无效请求占比极高:**在绝大多数时间里,请求返回的都是"无新消息"。

  • 服务端资源浪费严重

    线程被频繁创建和销毁,数据库 / 缓存被高频访问

  • 实时性与资源消耗不可兼得

    轮询间隔短 → 实时性好,但成本高;轮询间隔长 → 成本低,但体验差

在并发用户规模达到几十万、上百万时,短轮询几乎不可用。

二、长轮询:对 HTTP 的一次"工程级 Hack"

(一)核心思想:把"等消息"的时间放到服务端

长轮询的本质思想非常简单:不是让浏览器反复问"有没有消息",而是让服务端在"真的有消息时"再回答。

具体做法是:

  1. 浏览器发起一个 HTTP 请求

  2. 服务端不立即返回

  3. 若有新消息:立即返回响应

  4. 若长时间无消息:在超时阈值到达时返回空响应

  5. 浏览器收到响应后,立刻发起下一次请求

从宏观上看:

  • 仍然是轮询

  • 但轮询频率由客户端控制服务端事件驱动

(二)长轮询的交互特征

维度 短轮询 长轮询
请求频率 固定 不固定
服务端返回时机 立即 有事件或超时
空响应比例 极高 显著降低
实时性 依赖轮询间隔 接近实时

从工程效果上看,长轮询成功在 实时性与资源消耗之间取得了一个可接受的平衡点

(三)长轮询的典型应用场景

长轮询并不仅限于 IM,其适用场景本质是:服务端事件驱动、Web 端需要被动感知

典型包括:

  1. 即时通信 / 私信系统

  2. 扫码登录

  3. 实时监控看板

  4. 抢购 / 秒杀状态同步

  5. 任务进度通知

只要不满足 WebSocket 的使用条件(浏览器、网络、代理限制等),长轮询仍然是一个可靠的工程选择。

三、Java 服务端的挑战:线程模型与异步 Servlet

(一)同步 Servlet 的瓶颈

在 Servlet 3.0 之前,Java Web 容器(如 Tomcat)采用典型的同步模型:

  • 一个 HTTP 请求

  • 占用一个 Servlet 线程

  • 直到 Response 返回,线程才释放

如果使用同步 Servlet 实现长轮询:

  • 请求被挂起

  • 线程长期阻塞

  • 高并发下线程池迅速耗尽

这在 IM 场景中是灾难性的。

(二)异步 Servlet 的引入(Servlet 3.0)

2011 年发布的 Servlet 3.0 规范 jsr315,引入了异步处理能力(AsyncContext):

工作流程变为:

  1. Servlet 线程接收请求

  2. 将请求交给异步上下文

  3. Servlet 线程立即返回线程池

  4. 业务线程在合适时机写回 Response

这使得:

  • 连接可以被"挂起"

  • 线程不再被"绑死"

异步 Servlet 成为了 Java 长轮询的技术基石。

四、长轮询系统架构设计:从"伪长连接"到服务解耦

(一)接口模型设计:Send / Polling 的职责划分与工程语义

在IM通信服务的 Web 长轮询方案中,并没有试图将"发送消息"和"接收消息"揉进同一个接口,而是采用了职责清晰、语义单一的接口模型设计

  • Send 接口:负责消息上行

  • Polling 接口:负责消息下行

这种设计看似简单,但实际上是对 HTTP 请求模型、异步处理能力以及 IM 通信特性 的综合权衡结果。

1. Send 接口:标准 HTTP 上行通道

Send 接口是一个普通的同步 HTTP 接口,其职责非常明确:

Web 端 → 服务端:发送一条消息

1.1 接口特征
  • 请求-响应生命周期短

  • 不涉及连接挂起

  • 不依赖异步 Servlet

  • 行为与普通 REST API 完全一致

1.2 设计原因
  • **消息上行是客户端主动行为:**不需要等待服务端事件,不存在"挂起"的必要性

  • **简化调用与容错:**一次请求对应一次发送结果,失败可直接重试或提示用户

  • **降低系统复杂度:**将复杂性集中在 Polling(下行)接口,上行路径保持稳定、可控

从系统整体看,Send 接口承担的是"入口流量",而非系统瓶颈,因此保持同步、简单,是一种非常理性的工程选择。

2. Polling 接口:基于异步 Servlet 的下行通道

Polling 接口是整个长轮询方案的核心,其目标是:在 HTTP 协议限制下,最大化模拟"服务端主动推送"能力。

2.1 基本交互语义
  • Web 端发起 Polling 请求

  • 服务端 不立即返回响应

  • 请求被异步挂起并保存在服务端内存中

  • 当满足以下任一条件时返回响应:服务端有新消息 or 达到超时阈值

Web 端收到响应后,立刻发起下一次 Polling 请求,从而形成一条"伪长连接"。

3. 异步 Servlet:Polling 接口的技术基石

如果使用同步 Servlet 实现 Polling,请求在等待期间会持续占用 Servlet 线程,这在高并发场景下是不可接受的。因此,IM长轮询服务通常采用异步 Servlet 模型

3.1 常见的异步 Servlet 实现方案
  • Jetty 7 Continuation

  • Servlet 3.0 / 3.1 AsyncContext

  • Spring 3.2 异步 Servlet 支持

在当前通用阶段,市场上更推荐使用 Servlet 3.0/3.1 的 AsyncContext

3.2 异步 Servlet 带来的核心价值
  1. **Servlet 线程立即释放:**请求挂起不占用线程池资源,显著提升并发承载能力

  2. 请求上下文可长期保存: ServletRequest / ServletResponse 由异步上下文托管,支持事件驱动式返回响应

  3. **天然适配长轮询模型:**等待不阻塞,返回有明确触发条件

4. 超时机制:长轮询系统的"安全阀"

长轮询的本质是延长 HTTP 请求生命周期,这意味着系统必须正视各种异常情况:

  • 网络抖动或丢包

  • 客户端异常退出

  • 服务端机器重启

  • 中间代理超时

如果没有超时机制:

  • 请求可能永久挂起

  • 内存和上下文资源无法释放

  • 系统稳定性迅速下降

4.1 服务端超时策略

每个 Polling 请求都有一个最大挂起时间

若在该时间内无新消息:

  • 返回 空响应

  • 主动释放请求上下文

4.2 客户端超时策略

Web 端在发起 Polling 后:

  • 若超过最大时间未收到响应

  • 主动中断当前请求

  • 立即发起新的 Polling 请求

4.3 双向超时的工程意义
  • 防止"假死连接"

  • 保证请求最终一定会结束

  • 使系统具备自我修复能力

这实际上是一种 "双保险式" 的连接保活设计

5. Send / Polling 拆分的整体收益

从接口模型角度总结,大象长轮询服务的 Send / Polling 拆分带来了多重收益:

  • **职责单一,语义清晰:**上行与下行路径完全解耦

  • **实现复杂度集中:**异步、挂起、超时仅存在于 Polling

  • **更易扩展与演进:**后续可平滑切换 WebSocket,上层业务几乎无感知

  • **贴合 HTTP 与浏览器能力边界:**不强行"反模式"设计,顺应协议限制做工程优化

Send / Polling 接口模型,本质上是一次对 HTTP 能力边界的尊重与利用

它并不追求形式上的"长连接",而是通过异步、超时与事件驱动,构建了一条工程上足够稳定、语义上足够清晰的消息通道

(二)服务解耦:拆分长轮询与长连接的职责边界

尽管长轮询服务通过 SendPolling 两个接口,在 Web 端实现了近似长连接的通信效果,但从技术本质上看,这仍然是一种基于 HTTP 请求上下文的伪长连接机制

在该模型中,长轮询服务所维护的,仅仅是 Web 端每一次 Polling 请求对应的上下文信息(如 ServletRequestServletResponse 及其异步状态),用于在合适的时机返回响应数据;真正承载 IM 消息上下行的,仍然是基于 TCP 的长连接通道。只不过,这条长连接并非由 Web 客户端直接建立,而是由长轮询服务代为与 IM 服务建立和维护。

这意味着,长轮询服务在系统中同时承担了两类职责:

  • 面向 Web 端,维持大量"挂起中的" HTTP 轮询请求;

  • 面向 IM 服务,维持稳定、持久的 TCP 长连接。

当并发用户规模不断扩大时,请求上下文的长期驻留 + IM 长连接的持续维护 会使长轮询服务的内存、线程调度以及网络负载迅速上升,逐渐成为系统中的性能瓶颈。

1. 引入代理服务与缓存层,实现职责解耦

为降低长轮询服务的系统负载、明确各组件的职责边界,IM通信服务通常在整体架构中引入了两项关键组件:

  • Redis:高性能 KV 缓存系统,作用单独进行分析;

  • 长轮询代理服务(Proxy Service):专门负责与 IM 服务建立和维护长连接;

通过这一改造,系统将"长轮询请求管理 "与"IM 长连接维护"进行了解耦:

  • 长轮询服务

    • 仅负责 Web 端的 Send / Polling 请求处理

    • 管理请求上下文与响应返回

  • 代理服务

    • 专注于与 IM 服务建立真实的 TCP 长连接

    • 负责消息的实际上下行传输

  • Redis

    • 作为两类服务之间的状态与消息中转层

    • 提供高效、可扩展的数据共享能力

2. 缓存 Redis 中的核心数据结构设计

为了支撑上述解耦架构,IM 通信服务在 Redis 中维护了三类关键数据结构:

接收队列(Receive Queue)

  • 以 token 为维度存储 IM 服务下发的消息

  • 用于缓存尚未被 Web 端 Polling 获取的下行消息

最近有消息集合(Active Token Set)

  • 记录近期收到消息的 token

  • 用于减少长轮询服务对接收队列的无效轮询

token 映射关系(Token Mapping)

  • 维护 token 与其对应长连接所在代理服务机器 IP 的映射

  • 用于消息上行时的精确路由

通过上述数据结构,长轮询服务无需直接感知 IM 长连接的具体状态,只需围绕 token 与缓存数据进行操作,即可完成消息的收发与响应返回。

3. 解耦带来的工程价值

这种以代理服务和缓存层为核心的解耦设计,使系统具备了以下工程优势:

  • **职责清晰,边界明确:**各服务只关注自身最擅长的能力,避免逻辑膨胀

  • **可扩展性显著提升:**长轮询服务与代理服务可独立扩容,互不影响

  • **系统稳定性增强:**长连接波动不会直接冲击 Web 轮询请求处理

  • **为后续技术演进留足空间:**无论是切换 WebSocket,还是升级 IM 接入层,对 Web 端影响都被最小化

职责拆分:

组件 职责
长轮询服务 Web 请求管理
代理服务 IM 长连接
Redis 消息缓冲、路由

五、消息收发核心设计说明

(一)Token 化的"伪长连接"模型

1. Token 的作用

  • Web 端首次 Polling → 分配 token

  • token 对应一条 IM 长连接

  • token 映射到代理服务 IP

token 成为:

  • 连接身份

  • 路由依据

  • 状态载体

2. 消息收发流程总结

下行:

  • IM → 代理服务

  • 写入 token 接收队列

  • 长轮询服务返回给 Web

上行:

  • Web → Send(token)

  • 查 token 映射

  • 路由至代理服务

  • 经 TCP 发送至 IM

(二)性能优化:避免"无脑轮询"

为避免扫描所有 token:

  • 维护"最近有消息 token 集合"

  • 仅轮询活跃 token

但也需注意:

  • 大规模下集合可能成为 Redis 大 Key

  • 需要动态降级策略

这是典型的 工程权衡问题

(三)稳定性设计:超时与保活

1. 超时机制

  • 服务端超时返回空响应

  • 客户端主动中断重试

  • 防止网络异常导致连接"假死"

2. Token 心跳

  • Web 定期发送 ping

  • IM 回复 pong

  • 任一方异常 → 重新建链

六、长轮询 vs WebSocket:工程视角的理性对比

维度 长轮询 WebSocket
协议 HTTP 独立协议
双工 伪双工 真双工
资源消耗
实时性 较好 极好
兼容性 极强 受限

在IM通信服务中:

  • WebSocket 是首选

  • 长轮询是兜底

七、结语:技术会演进,工程思维不会过时

在浏览完长轮询的原理、设计与实践之后,我们需要从更宏观的视角来反思这一技术模式的价值与局限。

长轮询(Long Polling)本质上是一种对 HTTP 协议模型的工程级 "Hack" 式适配:它没有改变 HTTP 的请求-响应本质,却通过异步挂起、超时返回等机制,将服务端的事件驱动转化为客户端的近实时感知。这种技巧显然并非协议本意,却在特定的历史阶段、特定的技术约束下,成为了 Web 即时通信的重要基础设施。

从工程角度看,长轮询之所以曾经大规模使用,并不仅仅是因为它"实现了推送效果",更因为它具备以下几点工程性价值:

  1. **兼容性极强:**只要浏览器支持 HTTP 请求,无需浏览器特性支持(如 WebSocket),即可实现实时消息感知;支持跨浏览器、跨网络环境,甚至在存在 HTTP 代理的企业网络中也能稳定运行。

  2. **实现门槛低:**不需要额外协议栈,仅依赖熟悉的 HTTP;对现有 Web 服务架构兼容性好,可快速迭代上线。

  3. **渐进式演进路径明确:**在支持 WebSocket 之前作为兜底方案;随着 WebSocket 的普及,逐步退居备用或低兼容场景。

这些设计并非纯粹"应急之策",而是在特定场景下的工程最优解:在约束条件与业务需求之间做出权衡,使得系统在可控成本内达到可接受的实时性能。

随着 WebSocket 协议的标准化与浏览器端的广泛支持,这些缺陷逐渐被可以提供真正双工通信的技术所克服。WebSocket 可以在一个持久 TCP 连接上支持低延迟的双工消息交互,从架构效率、安全性、性能优化三个维度都较长轮询更优。

再往后,HTTP/2 Server Push、HTTP/3、WebTransport 等新协议和传输层创新也正在推动实时通信从传统的轮询模式迈向更高效、更可靠的方向。例如:

  • HTTP/2 的服务器推送机制 允许服务器主动向客户端推送资源;

  • WebTransport 利用 QUIC 提供低延迟、可复用的双向数据流

  • WebRTC DataChannel 进一步为点对点通信提供现实方案。

即便如此,我们仍然要明确一点:

工程技术的生命力不在于某一项技术本身,而在于如何在约束条件下设计出有效、稳定且可演进的系统

长轮询的价值并不会随着其使用频率的下降而消失。它在互联网即时通信早期起到了关键作用,支撑了大量业务级的实时交互,在工程实践层面积累了丰富的经验与模式,这些经验在其他异步、事件驱动系统中仍然具有参考价值。

对于技术工程师而言,理解这些模式的演进路径、取舍缘由和实际权衡,比简单记住某种技术的"接口怎么用"更重要。你要问的问题永远不是:

"某个技术是否过时了?"

而是:

"在当前的约束条件、业务需求和生态环境下,哪种技术能够带来最佳的综合效益?"

这种以问题为中心,约束驱动解决方案的思考方式,才是工程实践中最值得传承的价值。

未来的技术栈会继续发展,协议会继续更新,新的实时通信模式也会不断涌现。但对于我们的工程师而言,有一件事情永远不会过时:以严谨的工程视角审视问题,以可演进的设计思维解决问题。

参考资料

  1. https://en.wikipedia.org/wiki/Push_technology

  2. https://en.wikipedia.org/wiki/Long_polling

  3. https://tools.ietf.org/html/rfc6455

  4. https://www.w3.org/TR/websockets/

  5. https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API

  6. https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest

  7. https://docs.oracle.com/javaee/7/tutorial/servlets012.htm

  8. https://jetty.org/docs/jetty/7.x/continuations.html

  9. https://engineering.fb.com/2010/02/05/web/facebook-chat/

  10. https://www.infoq.com/articles/websocket-and-long-polling/

  11. https://martinfowler.com/articles/201701-event-driven.html

  12. https://en.wikipedia.org/wiki/Push_technology

  13. https://en.wikipedia.org/wiki/Long_polling

  14. https://tools.ietf.org/html/rfc6455 (WebSocket Protocol)

  15. https://www.w3.org/TR/websockets/

  16. https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API

  17. https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest

  18. https://docs.oracle.com/javaee/7/tutorial/servlets012.htm

  19. https://jetty.org/docs/jetty/7.x/continuations.html

  20. https://www.infoq.com/articles/websocket-and-long-polling/

  21. https://engineering.fb.com/2010/02/05/web/facebook-chat/

  22. https://datatracker.ietf.org/doc/html/rfc7540 (HTTP/2)

  23. https://datatracker.ietf.org/doc/html/rfc9116 (HTTP/3)

  24. https://wicg.github.io/webtransport/ (WebTransport Spec)

  25. https://webrtc.org/getting-started/data-channels/

  26. https://www.oreilly.com/library/view/http2-in-action/9781617293781/

  27. https://www.infoq.com/presentations/http2-websocket/

  28. https://spring.io/guides/gs/messaging-stomp-websocket/

  29. https://nodejs.org/dist/latest-v20.x/docs/api/websocket.html

  30. https://medium.com/@mweststrate/websocket-vs-long-polling-7fdce58413e5

  31. https://cloud.google.com/blog/products/gcp/choosing-between-websockets-and-long-polling