跳到主要内容

长轮询

定期轮询有什么缺点?​

  • 做法: 客户端每固定间隔 (如 10 秒) 发一次请求, 问服务器"有我的消息吗";
  • 延迟: 消息最多要等一个间隔才送达;
  • 空转: 即使没有消息也照发, 用户切走或休眠时仍在压服务器, 性能开销大;
  • 结论: 很小的服务尚可, 一般场景需要改进;

长轮询为什么能消除延迟?​

  • 流程:
    1. 客户端发请求;
    2. 服务器不关闭连接, 直到有消息才响应;
    3. 有消息时用它响应, 连接关闭;
    4. 浏览器立刻发起下一个请求;
  • 特征: 无消息时连接一直挂着 (pending), 一有消息立即返回, 因此几乎没有延迟;
  • 断线: 网络错误导致连接丢失时, 浏览器立即重新发起请求;
定期轮询: 请求 ─► 响应(空) ─► 等 10s ─► 请求 ─► 响应(消息)
长轮询: 请求 ───────► 挂起等待 ────► 响应(消息) ─► 立即再请求
  • 定位: 实现最简单, 不需要 WebSocket 或 SSE 这样的专用协议;

客户端如何实现长轮询?​

  • 思路: subscribe() 里 fetch 等响应, 处理完再调用自己, 形成无限循环;
  • 502: 连接挂太久被服务器或代理掐断的典型信号, 直接重连;
  • 其他非 200: 显示错误, 退避 1 秒再重连;
  • 200: 取出并显示消息, 立刻进入下一轮;
async function subscribe() {
const response = await fetch("/subscribe");

if (response.status == 502) {
await subscribe(); // 连接超时被关闭, 立即重连
} else if (response.status != 200) {
showMessage(response.statusText);
await new Promise((resolve) => setTimeout(resolve, 1000)); // 退避 1 秒
await subscribe();
} else {
showMessage(await response.text());
await subscribe(); // 收到消息后马上继续
}
}

subscribe();
  • 等待不阻塞: await 只挂起 subscribe 自己, 事件循环继续处理 UI 与其他任务, 见 async 与 await;
  • 递归即循环: 下一轮调用发生在 await 之后, 上一层已经返回, 不会累积调用栈;

长轮询对服务端有什么要求?​

  • 并发: 每个在线客户端都挂一条 pending 连接, 服务端必须能同时持有大量连接;
  • 反面: 每连接开一个进程的架构 (部分 PHP、Ruby 部署) 会让进程数等于连接数, 很快吃光内存;
  • 正面: 事件驱动的架构 (如 Node.js) 天然适合;
  • 结论: 这是服务端架构问题而非语言问题, 现代 PHP/Ruby 也能做到, 但必须确认架构支持大量挂起连接;

长轮询适合哪些场景?​

  • 适合: 消息稀疏的场景 (如聊天中偶尔来一条), 实现简单且无延迟;
  • 不适合: 消息频繁时每条消息都变成一次独立请求, 每次都要带请求头与认证开销, 请求-响应曲线变成锯齿状;
  • 替代: 高频或双向通信改用 WebSocket, 服务端单向推送用 SSE, 见 WebSocket 与 SSE;