长轮询
定期轮询有什么缺点?
- 做法: 客户端每固定间隔 (如 10 秒) 发一次请求, 问服务器"有我的消息吗";
- 延迟: 消息最多要等一个间隔才送达;
- 空转: 即使没有消息也照发, 用户切走或休眠时仍在压服务器, 性能开销大;
- 结论: 很小的服务尚可, 一般场景需要改进;
长轮询为什么能消除延迟?
- 流程:
- 客户端发请求;
- 服务器不关闭连接, 直到有消息才响应;
- 有消息时用它响应, 连接关闭;
- 浏览器立刻发起下一个请求;
- 特征: 无消息时连接一直挂着 (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;