Skip to content

http 长短轮训

要点速览

shell
短轮询、长轮询 都需要前后端配合,不能只靠前端。
短轮询:前端定时器不断发请求,后端即时响应,实现最简单,性能最差。
长轮询:前端收到响应立刻复用发下一次请求;后端核心改造:请求挂起阻塞、等待数据或超时再返回。
演进路线:短轮询 长轮询 WebSocket;
长轮询是无 WebSocket 环境下最优的实时降级方案。

# 理解
短轮询
    控制权:前端
    逻辑:请求回来 N 再发请求
    后端,简单版本:普通接口,啥都不用改,来了就直接查、直接返回(完整替换)
    后端,优化版本:查看请求是否携带时间戳,没携带则普通接口同上,有时间戳则通过时间戳查询是否有新数据,有则返回新数据,没有则返回空数组(增量累加) 
长轮询
    控制权:后端
    逻辑:
    前端发请求,拿到响应立刻马上发下一次(不等待)
    后端无新数据 挂起、阻塞等待
    后端有新数据 / 超时 立即返回结果
    核心差异:后端阻塞等待

短论训(普通轮询)

shell
# 原理
前端:
    定时(比如 2s / 3s)主动发 HTTP 请求
    后端立刻返回结果:有新数据就返回数据,没数据也直接返回空 / 无更新
    前端拿到响应,间隔固定时间,再发下一次请求
    循环往复
后端:
    后端逻辑极其简单,正常接口即可

# 缺点:
无论有无数据,都频繁发请求
服务器压力大、流量浪费、实时性差

# 问题:
短论训需要判断是否有新数据,有新返回,无新返回空数据
长轮询需要判断是否有新数据,有新返回,无新挂起

# 前端代码
# 短轮询:每隔3秒请求一次
function shortPoll() {
  fetch("/api/getMsg")
    .then(res => res.json())
    .then(data => {
      if (data.list.length) {
        console.log("收到新消息", data.list);
      }
    })
    .finally(() => {
      # 固定间隔再次请求
      setTimeout(shortPoll, 3000);
    });
}
# 启动
shortPoll();

长轮询(Long Polling)

shell
# 原理
前端:
    前端发起 HTTP 请求
后端:
    后端不立即返回
    如果暂无新数据,挂起请求、阻塞等待
    等到有新数据 / 超时时间到了,才返回响应
前端:
    一收到后端响应(无论有数据还是超时),立刻马上发起下一次新请求
    始终保持一个请求在 “等待中”,实现准实时

# 核心逻辑在后端
短轮询后端不用改;长轮询必须后端特殊处理:
    收到请求后,不直接响应
    阻塞挂起,监听消息队列 / 事件
    有新数据 立即返回数据
    超时(如 30s)→ 返回超时标识,断开本次请求
    控制连接挂起、超时释放、防止连接打爆
后端
    短轮询:来一个请求,立刻 return;
    长轮询:来一个请求,我先 hold 住,等消息 / 等超时再 return。


# 缺点:
无论有无数据,都频繁发请求
服务器压力大、流量浪费、实时性差

# 问题:
短论训后端返回的结果,有无新数据是怎么判断的,返回空还是返回查询的结果呢?

# 前端代码
function longPoll() {
  # 每次响应完 立刻再请求
  fetch("/api/longPoll")
    .then(res => res.json())
    .then(data => {
      if (data.msg) {
        console.log("长轮询收到消息", data.msg);
      }
    })
    .catch(err => {
      console.log("连接异常");
    })
    .finally(() => {
      # 关键:马上建立下一次长连接,不延时
      longPoll();
    });
}
# 启动长轮询
longPoll();

对比

shell
类型   前端         后端                          实时性               请求频率
短轮询 定时器循环请求 普通接口,无特殊逻辑             差,固定间隔          极高,浪费资源
长轮询 响应后立即重发 需要阻塞挂起、超时控制、消息等待   接近实时             低,同一时间基本只 1 个请求

拓展

shell
# 长轮询是长连接吗?
不是,依然是短 HTTP 单次请求,只是响应被后端人为延迟了,每次请求都是独立 http。

# 为什么长轮询更省性能?
不会无效频繁请求,没消息时连接挂起,不会频繁握手。

# 长轮询超时作用?
避免连接无限挂死、占用服务端连接数、适配代理 / 防火墙超时限制。

# 长轮询挂起那么多请求,会不会把服务器搞崩?
会,所以要加最大挂起数 + 自动超时释放
单个用户同时只保留一条长轮询请求
超时自动断开,防止连接泄露

# 核心问题:后端怎么知道哪些是「新数据」?
通用核心方案:时间戳判断,所谓的新数据就是查询一下是否在这个时间点之后有新数据插入到表中,之后插入的数据都是新数据
无论短轮询、长轮询、WebSocket,实时消息系统全靠这一套。
短轮询 后端判断新数据
    前端定时请求,携带 lastTime
    后端 SQL / 缓存对比时间
    有新数据→返回列表;无→返回空数组
    结束本次 HTTP 请求
长轮询 后端判断新数据
    前端请求带 lastTime
    后端先查:有没有 > 该时间的新数据
 直接立刻返回新数据
        没有 挂起当前请求,进入等待队列
    后台新增消息时,存入 DB + 推送至消息队列
    遍历挂起的请求,匹配时间条件,唤醒并返回新数据
    超时时间到,无新数据,主动返回空,结束本次请求,前端立即发送跟上


# NestJS 如何实现长轮询挂起?
后端挂起本质:通过 Promise 阻塞响应,不立即返回数据。
NestJS 实现:利用 Promise 延迟 resolve,结合定时器做超时兜底、事件回调做新消息唤醒。

# 生产要注意的坑
必须加超时时间,防止请求无限挂起,占满服务器连接数
用户维度隔离:不能全局一个等待队列,要按 userId 区分
客户端异常断开(关闭页面):要捕获异常、清理等待回调,防内存泄漏
Nginx 层也要配置超时,否则 nginx 主动断连接: proxy_read_timeout 30s;

NestJS 实现长轮询挂起

第一步:定义消息服务(存放等待队列、消息推送)

js
// message.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class MessageService {
  // 存放等待中的长轮询回调
  private waitList: Array<(data: any) => void> = [];

  // 新增消息,唤醒所有挂起的请求
  sendNewMsg(msg: any) {
    this.waitList.forEach(cb => cb(msg));
    this.waitList = [];
  }

  // 挂起等待方法
  waitMessage(timeout = 30000): Promise<any> {
    return new Promise((resolve) => {
      // 加入等待队列
      this.waitList.push((data) => {
        resolve({ code: 200, data });
      });

      // 超时自动释放,结束本次请求
      setTimeout(() => {
        resolve({ code: 200, data: [], msg: 'timeout' });
      }, timeout);
    });
  }
}

第二步:控制器 长轮询接口

js
// app.controller.ts
import { Controller, Get } from '@nestjs/common';
import { MessageService } from './message.service';

@Controller()
export class AppController {
  constructor(private readonly msgService: MessageService) {}

  // 长轮询接口:请求进来直接挂起
  @Get('long-poll')
  async longPoll() {
    // 关键:这里会卡住,直到有消息 / 超时
    return this.msgService.waitMessage();
  }

  // 测试:手动推送新消息,触发前端立刻返回
  @Get('push')
  push() {
    this.msgService.sendNewMsg({ content: '新消息来了' });
    return 'ok';
  }
}