专栏名称: 芋道源码
纯 Java 源码分享公众号,目前有「Dubbo」「SpringCloud」「Java 并发」「RocketMQ」「Sharding-JDBC」「MyCAT」「Elastic-Job」「SkyWalking」「Spring」等等
目录
相关文章推荐
芋道源码  ·  阿里这款多级缓存框架一定要掌握,非常不错! ·  20 小时前  
芋道源码  ·  高性能、无侵入的 Java 性能监控神器 ·  昨天  
Java编程精选  ·  手把手教你Java文件断点下载 ·  3 天前  
芋道源码  ·  抱歉,最近我劝各位真的别轻易离职...... ·  2 天前  
51好读  ›  专栏  ›  芋道源码

Web 实时消息推送的 7 种实现方案

芋道源码  · 公众号  · Java  · 2025-05-20 09:35

正文

请到「今天看啥」查看全文


  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro
  • 视频教程:https://doc.iocoder.cn/video/

短轮询

轮询( polling )应该是实现消息推送方案中最简单的一种,这里我们暂且将轮询分为 短轮询 长轮询

短轮询很好理解,指定的时间间隔,由浏览器向服务器发出 HTTP 请求,服务器实时返回未读消息数据给客户端,浏览器再做渲染显示。

一个简单的JS定时器就可以搞定,每秒钟请求一次未读消息数接口,返回的数据展示即可。

setInterval(() => {
  // 方法请求
  messageCount().then((res) => {
      if (res.code === 200) {
          this.messageCount = res.data
      }
  })
}, 1000);

效果还是可以的,短轮询实现固然简单,缺点也是显而易见,由于推送数据并不会频繁变更,无论后端此时是否有新的消息产生,客户端都会进行请求,势必会对服务端造成很大压力,浪费带宽和服务器资源。

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/yudao-cloud
  • 视频教程:https://doc.iocoder.cn/video/

长轮询

长轮询是对上边短轮询的一种改进版本,在尽可能减少对服务器资源浪费的同时,保证消息的相对实时性。长轮询在中间件中应用的很广泛,比如 Nacos apollo 配置中心,消息队列 kafka RocketMQ 中都有用到长轮询。

这次我使用 apollo 配置中心实现长轮询的方式,应用了一个类 DeferredResult ,它是在 servelet3.0 后经过Spring封装提供的一种异步请求机制,直意就是延迟结果。

DeferredResult 可以允许容器线程快速释放占用的资源,不阻塞请求线程,以此接受更多的请求提升系统的吞吐量,然后启动异步工作线程处理真正的业务逻辑,处理完成调用 DeferredResult.setResult(200) 提交响应结果。

下边我们用长轮询来实现消息推送。

因为一个ID可能会被多个长轮询请求监听,所以我采用了 guava 包提供的 Multimap 结构存放长轮询,一个key可以对应多个value。一旦监听到key发生变化,对应的所有长轮询都会响应。前端得到非请求超时的状态码,知晓数据变更,主动查询未读消息数接口,更新页面数据。

@Controller
@RequestMapping("/polling")
publicclass PollingController {

    // 存放监听某个Id的长轮询集合
    // 线程同步结构
    publicstatic Multimap> watchRequests = Multimaps.synchronizedMultimap(HashMultimap.create());

    /**
     * 设置监听
     */

    @GetMapping(path = "watch/{id}")
    @ResponseBody
    public DeferredResult watch(@PathVariable String id) {
        // 延迟对象设置超时时间
        DeferredResult deferredResult = new DeferredResult<>(TIME_OUT);
        // 异步请求完成时移除 key,防止内存溢出
        deferredResult.onCompletion(() -> {
            watchRequests.remove(id, deferredResult);
        });
        // 注册长轮询请求
        watchRequests.put(id, deferredResult);
        return deferredResult;
    }

    /**
     * 变更数据
     */

    @GetMapping(path = "publish/{id}")
    @ResponseBody
    public String publish(@PathVariable String id) {
        // 数据变更 取出监听ID的所有长轮询请求,并一一响应处理
        if (watchRequests.containsKey(id)) {
            Collection> deferredResults = watchRequests.get(id);
            for (DeferredResult deferredResult : deferredResults) {
                deferredResult.setResult("我更新了" + new Date());
            }
        }
        return"success";
    }

当请求超过设置的超时时间,会抛出 AsyncRequestTimeoutException 异常,这里直接用 @ControllerAdvice 全局捕获统一返回即可,前端获取约定好的状态码后再次发起长轮询请求,如此往复调用。

@ControllerAdvice
public class AsyncRequestTimeoutHandler {

    @ResponseStatus(HttpStatus.NOT_MODIFIED)
    @ResponseBody
    @ExceptionHandler(AsyncRequestTimeoutException.class)
    public String asyncRequestTimeoutHandler(AsyncRequestTimeoutException e
{
        System.out.println("异步请求超时");
        return "304";
    }
}

我们来测试一下,首先页面发起长轮询请求 /polling/watch/10086 监听消息更变,请求被挂起,不变更数据直至超时,再次发起了长轮询请求;紧接着手动变更数据 /polling/publish/10086 ,长轮询得到响应,前端处理业务逻辑完成后再次发起请求,如此循环往复。







请到「今天看啥」查看全文