什么是消息推送?

简介: 使用Web 实时消息推送详解

推送的场景比较多,比如有人关注我的公众号,这时我就会收到一条推送消息,以此来吸引我点击打开应用。

消息推送通常是指网站的运营工作等人员,通过某种工具对用户当前网页或移动设备 APP 进行的主动消息推送。

消息推送一般又分为 Web 端消息推送和移动端消息推送。

移动端消息推送示例:

移动端消息推送示例

Web 端消息推送示例:

Web 端消息推送示例

在具体实现之前,咱们再来分析一下前边的需求,其实功能很简单,只要触发某个事件(主动分享了资源或者后台主动推送消息),Web 页面的通知小红点就会实时的 +1 就可以了。

通常在服务端会有若干张消息推送表,用来记录用户触发不同事件所推送不同类型的消息,前端主动查询(拉)或者被动接收(推)用户所有未读的消息数。

消息推送表

消息推送无非是推(push)和拉(pull)两种形式,下边我们逐个了解下。

# 消息推送常见方案

# 短轮询

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

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

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

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

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

# 长轮询

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

Nacos 配置中心交互模型是 push 还是 pull?open in new window一文中我详细介绍过 Nacos 长轮询的实现原理,感兴趣的小伙伴可以瞅瞅。

长轮询其实原理跟轮询差不多,都是采用轮询的方式。不过,如果服务端的数据没有发生变更,会 一直 hold 住请求,直到服务端的数据发生变化,或者等待一定时间超时才会返回。返回后,客户端又会立即再次发起下一次长轮询。

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

长轮询示意图

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

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

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

@Controller
@RequestMapping("/polling")
public class PollingController {
    // 存放监听某个Id的长轮询集合
    // 线程同步结构
    public static Multimap<String, DeferredResult<String>> watchRequests = Multimaps.synchronizedMultimap(HashMultimap.create());
    /**
     * 设置监听
     */
    @GetMapping(path = "watch/{id}")
    @ResponseBody
    public DeferredResult<String> watch(@PathVariable String id) {
        // 延迟对象设置超时时间
        DeferredResult<String> 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<DeferredResult<String>> deferredResults = watchRequests.get(id);
            for (DeferredResult<String> 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,长轮询得到响应,前端处理业务逻辑完成后再次发起请求,如此循环往复。

长轮询相比于短轮询在性能上提升了很多,但依然会产生较多的请求,这是它的一点不完美的地方。

目录
相关文章
|
安全 算法 API
产品经理必备知识——API接口
前言 在古代,我们的传输信息的方式有很多,比如写信、飞鸽传书,以及在战争中使用的烽烟,才有了著名的烽火戏诸侯,但这些方式传输信息的效率终究还是无法满足高速发展的社会需要。如今万物互联的时代,我通过一部手机就可以实现衣食住行的方方面面,比如:在家购物、远程控制家电、自动驾驶等等,背后都离不开我们今天要聊的API接口。
|
资源调度 前端开发 算法
前端依赖版本重写指南
感谢神奇的 Semver 动态规则,npm 社区经常会发生依赖包更新后引入破坏变更的情况(应用没有使用依赖锁的话),而应用开发者就要在自己的依赖声明里先临时绕过,避免安装到有问题的版本,如果是一级依赖,只需要改 package.json 的声明就可以了,但如果是子依赖,就需要进行版本重写(overrides/resolution)了。本文是一篇针对版本重写功能的指南性文章,当你遇到如下的问题时,就可以按照对应的依赖重写语法,解决这些依赖问题了。
7323 1
前端依赖版本重写指南
|
11月前
|
机器学习/深度学习 TensorFlow Swift
Swift语言适合多个领域的开发
Swift语言适合多个领域的开发
285 9
|
消息中间件 存储 Java
自顶向下学习 RocketMQ(九):回溯消费
回溯消费是指 Consumer 已经消费成功的消息,由于业务上需求需要重新消费,要支持此功能,Broker 在向 Consumer 投递成功消息后,消息仍然需要保留。并且重新消费一般是按照时间维度,例如由于 Consumer 系统故障,恢复后需要重新消费 1 小时前的数据,那么 Broker 要提供一种机制,可以按照时间维度来回退消费进度。RocketMQ 支持按照时间回溯消费,时间维度精确到毫秒。
自顶向下学习 RocketMQ(九):回溯消费
Python错误 TypeError: ‘NoneType‘ object is not subscriptable解决方案汇总
Python错误 TypeError: ‘NoneType‘ object is not subscriptable解决方案汇总
|
11月前
|
Java API 调度
如何避免 Java 中的 TimeoutException 异常
在Java中,`TimeoutException`通常发生在执行操作超过预设时间时。要避免此异常,可以优化代码逻辑,减少不必要的等待;合理设置超时时间,确保其足够完成正常操作;使用异步处理或线程池管理任务,提高程序响应性。
498 13
|
安全 API 数据安全/隐私保护
微服务的安全性考虑
【8月更文第29天】随着微服务架构的流行,确保这些服务的安全性成为了系统设计中的一个关键因素。本文将探讨微服务环境下如何确保数据安全、API 安全以及网络通信的安全,并提供相应的代码示例。
254 0
|
消息中间件 监控 网络协议
消息推送分类:推动(Push)模式和拉动(Pull)模式
消息推送分类:推动(Push)模式和拉动(Pull)模式
663 0
|
弹性计算 分布式计算 网络协议
聊聊复杂网络环境下hdfs的BlockMissingException异常|参数dfs.client.use.datanode.hostname
企业真实的网络环境是复杂多变的,在复杂的网络环境中部署并使用 hadoop 时,如果服务端的配置或客户端的使用不当,就可能会遇见各种问题。
聊聊复杂网络环境下hdfs的BlockMissingException异常|参数dfs.client.use.datanode.hostname
|
JSON API 数据安全/隐私保护
CURL 发送POST请求
CURL 发送POST请求
2362 0