长轮询 实现

什么是长轮询(Long Polling)?

在 常规的 HTTP 请求 中:

  1. 客户端 发送请求到服务器。
  2. 服务器 处理请求并立即返回响应。
  3. 客户端 收到响应后,HTTP 连接关闭。

但如果服务器端数据 更新不频繁,客户端需要 不断发送请求(短轮询) 来获取最新数据,这种方式浪费资源,因为大多数请求都是无效的(服务器没新数据)。

长轮询(Long Polling)如何突破 HTTP 一次请求一次响应的限制?

长轮询仍然遵循 HTTP 协议的 “一次请求,一次响应”,但关键在于服务器不立即返回响应,而是等待数据可用后才返回。

流程如下:

  1. 客户端发送 HTTP 请求,请求服务器的最新数据。
  2. 服务器保持该请求(不立即返回),直到:
    • 有新数据时,服务器返回数据并关闭连接。
    • 超时(如 30 秒)时,服务器返回空数据或超时响应,客户端需重新请求。
  3. 客户端收到响应后,立即发起新的请求,等待下一次数据更新。

这样,服务器 只在有新数据时才返回响应,减少了无效请求,节约资源。

Spring Boot DeferredResult 实现长轮询

Spring Boot 提供了 DeferredResult 来支持长轮询,它允许服务器 延迟返回 HTTP 响应,直到有数据可用。

📌 代码示例:模拟消息推送
@RestController
public class LongPollingController {
    private final Map<String, DeferredResult<String>> requestMap = new ConcurrentHashMap<>();

    @GetMapping("/long-polling")
    public DeferredResult<String> longPolling(@RequestParam String clientId) {
        // 创建一个 DeferredResult 对象,超时 30 秒
        DeferredResult<String> deferredResult = new DeferredResult<>(30000L, "No new messages");

        // 存储请求(等待数据到来)
        requestMap.put(clientId, deferredResult);

        // 监听请求超时,超时后移除
        deferredResult.onCompletion(() -> requestMap.remove(clientId));

        return deferredResult;
    }

    @PostMapping("/send-message")
    public ResponseEntity<String> sendMessage(@RequestParam String clientId, @RequestParam String message) {
        DeferredResult<String> deferredResult = requestMap.get(clientId);
        if (deferredResult != null) {
            deferredResult.setResult(message); // 立即返回消息
            requestMap.remove(clientId);
        }
        return ResponseEntity.ok("Message sent");
    }
}

📌 代码解析

  1. 客户端 调用 /long-polling?clientId=123 发送请求。
  2. 服务器 在 requestMap 里 存储该请求,并 不立即返回(除非超时)。
  3. 当 /send-message 发送新消息时:
    • 服务器找到该 clientId 的请求,并立即返回消息。
    • 客户端收到消息后,会再次请求 /long-polling,等待下一条数据。
  4. 如果 30 秒内没有消息,返回 "No new messages",客户端需要重新请求。
📌 客户端示例(JavaScript AJAX 轮询)
function longPolling() {
    fetch('/long-polling?clientId=123')
        .then(response => response.text())
        .then(data => {
            console.log("Received:", data);
            setTimeout(longPolling, 100); // 立即发送新请求,等待下一次数据
        })
        .catch(error => {
            console.error("Polling error:", error);
            setTimeout(longPolling, 5000); // 失败后延迟 5 秒重试
        });
}

// 启动长轮询
longPolling();

📌 客户端不断请求服务器:

  • 当有 新消息,服务器立即返回数据,客户端重新请求。
  • 当 超时(30s 内无消息),服务器返回 "No new messages",客户端也会重新请求。

🚀 长轮询 vs. 短轮询 vs. WebSocket

方式 机制 适用场景 服务器资源消耗 网络流量
短轮询(Short Polling) 客户端每隔 X 秒请求一次 低并发,数据更新频繁 高(大量无效请求)
长轮询(Long Polling) 客户端请求,服务器等数据可用后再返回 中等并发,数据不频繁更新 低(只有新数据时才返回)
WebSocket 双向长连接,实时数据推送 高并发,实时性高(如聊天、游戏) 低(连接保持但不频繁创建)

多实例条件下的实现

  • clientId = user1访问app1的/long-polling接口,等待接口返回数据
  • clientId = user2访问app2的/send-message接口, 向clientId = user1发送message,

由于不在同一个实例,无法获取requestMap并进行setResult(),那么我们需要如下处理:

  • 在步骤1的时候, 往redis中存放key = user1, value=app1的键值,
  • 在步骤2的时候,查询获取value,然后将message发送到queue = app1的队列中;
    • 实例app1的监听queue = app1的,进行setResult()操作;长轮询接口返回数据给用户
    • 如果长轮询不存在,那么需要增加额外机制保存历史信息,比如存放在db中,每次访问时,先查询和加载历史信息

📌 什么时候用长轮询?

✅ 适用于

  • 即时消息通知(如 Web 版微信消息提醒)。
  • 状态更新(如订单状态变更、设备在线状态)。
  • 服务器端数据更新不频繁,但需要及时通知。

❌ 不适用于

  • 高并发、低延迟的实时通信(WebSocket 更合适)。
  • 服务器压力大时(可以用 Server-Sent Events(SSE))。

总结

  1. 长轮询是基于 HTTP 协议,不改变“一次请求,一次响应”的机制。
  2. 服务器不立即响应,而是等待数据可用后才返回,减少无效请求,提高效率。
  3. Spring Boot 的 DeferredResult 可以轻松实现长轮询。
  4. 适用于低流量的状态变更通知,不适用于高并发的实时通信(建议 WebSocket)。
posted @ 2025-03-04 10:57  又是火星人  阅读(372)  评论(0)    收藏  举报