Skip to content
欢迎扫码关注公众号

Spring Boot 长轮询示例

这是一个 Spring Boot 项目,演示了如何使用长轮询实现服务器推送功能。其中展示两种实现方式:

  • 阻塞式长轮询:使用阻塞队列来存储服务器推送的消息,当客户端发起请求时,服务器会一直阻塞,直到有新消息或者超时。
  • 非阻塞式长轮询:使用 DeferredResult 来实现非阻塞式的长轮询,当客户端发起请求时,服务器会立即返回一个 DeferredResult 对象,然后在后台线程中处理请求,并在处理完成后设置结果。
java
package me.liujiajia.example.longpolling;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.TimeUnit;

@RestController
public class LongPollingController {

    private static Logger log = LoggerFactory.getLogger(LongPollingController.class);

    @Autowired
    private HttpServletRequest request;
    @Autowired
    private HttpServletResponse response;

    // 用来存储服务器推送的消息
    private static final BlockingQueue<String> messageQueue = new ArrayBlockingQueue<>(10);

    // 模拟一个消息生产者线程
    static {
        new Thread(() -> {
            try {
                int i = 0;
                while (true) {
                    String message = "Message " + i++;
                    messageQueue.put(message);
                    log.info("Produced: {}", message);
                    // 每 5 秒生产一条消息
                    TimeUnit.SECONDS.sleep(5);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }).start();
    }

    @GetMapping("/long-polling-with-block")
    public void longPollingWithBlock() throws IOException {
        log.info("long-polling-with-block start");
        // 设置响应内容类型为文本
        response.setContentType("text/plain");
        response.setCharacterEncoding("UTF-8");

        // 设置响应头,防止浏览器关闭连接
        response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
        response.setHeader("Pragma", "no-cache");
        response.setDateHeader("Expires", 0);

        PrintWriter out = response.getWriter();

        try {
            // 从队列中获取消息,如果队列为空则阻塞,直到有新消息或者超时
            String message = messageQueue.poll(30, TimeUnit.SECONDS);
            log.info("long-polling-with-block poll {}", message);

            if (message != null) {
                out.println(message);
            } else {
                out.println("No new messages. Try again later.");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            out.flush();
            out.close();
        }

        log.info("long-polling-with-block end");
    }

    @GetMapping("/long-polling-no-block")
    public DeferredResult<String> longPollingNoBlock() {
        log.info("long-polling-no-block start");
        DeferredResult<String> result = new DeferredResult<>();

        ForkJoinPool.commonPool().submit(() -> {
            String message;
            try {
                // 从队列中获取消息,如果队列为空则阻塞,直到有新消息或者超时
                message = messageQueue.poll(30, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                log.error("Interrupted", e);
                result.setResult("error");
                return;
            }
            result.setResult(message);
            log.info("long-polling-no-block poll {}", message);
        });

        log.info("long-polling-no-block end");

        return result;
    }
}

long-polling-with-block_ 接口的日志,阻塞到 poll 成功后,才打印的 end 日志。

java
2025-03-13T18:35:07.392+08:00  INFO 36976 --- [longpolling] [nio-8080-exec-6] m.l.e.longpolling.LongPollingController  : long-polling-with-block start
2025-03-13T18:35:11.553+08:00  INFO 36976 --- [longpolling] [       Thread-1] m.l.e.longpolling.LongPollingController  : Produced: Message 16
2025-03-13T18:35:11.553+08:00  INFO 36976 --- [longpolling] [nio-8080-exec-6] m.l.e.longpolling.LongPollingController  : long-polling-with-block poll Message 16
2025-03-13T18:35:11.555+08:00  INFO 36976 --- [longpolling] [nio-8080-exec-6] m.l.e.longpolling.LongPollingController  : long-polling-with-block end

long-polling-no-block 接口的日志,可以看到,先打印的 end 日志,等到队列中添加了消息后才打印的 pool 日志。

java
2025-03-13T18:35:17.442+08:00  INFO 36976 --- [longpolling] [nio-8080-exec-3] m.l.e.longpolling.LongPollingController  : long-polling-no-block start
2025-03-13T18:35:17.443+08:00  INFO 36976 --- [longpolling] [nio-8080-exec-3] m.l.e.longpolling.LongPollingController  : long-polling-no-block end
2025-03-13T18:35:21.569+08:00  INFO 36976 --- [longpolling] [       Thread-1] m.l.e.longpolling.LongPollingController  : Produced: Message 18
2025-03-13T18:35:21.569+08:00  INFO 36976 --- [longpolling] [onPool-worker-2] m.l.e.longpolling.LongPollingController  : long-polling-no-block poll Message 18