相关文章推荐
爱运动的核桃  ·  LSM树详解 - 知乎·  1 年前    · 
讲道义的鸡蛋面  ·  Elasticsearch ...·  1 年前    · 
酷酷的红金鱼  ·  vue移动端缩放-掘金·  1 年前    · 
俊秀的凳子  ·  matlab ...·  1 年前    · 


我有一个朋友~

做了一个小破站,现在要实现一个站内信web消息推送的功能,对,就是下图这个小红点,一个很常用的功能。

image

不过他还没想好用什么方式做,这里我帮他整理了一下几种方案,并简单做了实现。

image

什么是消息推送(push)

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

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

消息推送一般又分为 web端消息推送 移动端消息推送

image

上边的这种属于移动端消息推送,web端消息推送常见的诸如站内信、未读邮件数量、监控报警数量等,应用的也非常广泛。

image

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

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

image

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

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

短轮询

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

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

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

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

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

image

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

长轮询

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

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

image

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 ,长轮询得到响应,前端处理业务逻辑完成后再次发起请求,如此循环往复。

image

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

iframe流

iframe流就是在页面中插入一个隐藏的 <iframe> 标签,通过在 src 中请求消息数量API接口,由此在服务端和客户端之间创建一条长连接,服务端持续向 iframe 传输数据。

传输的数据通常是 HTML 、或是内嵌的 javascript 脚本,来达到实时更新页面的效果。

image

这种方式实现简单,前端只要一个 <iframe> 标签搞定了

<iframe src="/iframe/message" style=""></iframe>

服务端直接组装html、js脚本数据向 response 写入就行了

@Controller
@RequestMapping("/iframe")
public class IframeController {
    @GetMapping(path = "message")
    public void message(HttpServletResponse response) throws IOException, InterruptedException {
        while (true) {
            response.setHeader("Pragma", "no-cache");
            response.setDateHeader("Expires", 0);
            response.setHeader("Cache-Control", "no-cache,no-store");
            response.setStatus(HttpServletResponse.SC_OK);
            response.getWriter().print(" <script type=\"text/javascript\">\n" +
                    "parent.document.getElementById('clock').innerHTML = \"" + count.get() + "\";" +
                    "parent.document.getElementById('count').innerHTML = \"" + count.get() + "\";" +
                    "</script>");
}

但我个人不推荐,因为它在浏览器上会显示请求未加载完,图标会不停旋转,简直是强迫症杀手。

image

web页面实现全背景视频功能方案:使用bideo.js来处理object-fit在ie浏览器下不兼容问题
web页面实现全背景视频功能方案:使用bideo.js来处理object-fit在ie浏览器下不兼容问题
阿里云上云方案:Web与移动App云上部署解决方案及服务指南
对于绝大部分的上云用户来说,部署Web与移动App是最常见的,很多新手用户不知道上云时该如何选择阿里云产品与配置,为此,阿里云专门针对这部分用户的需求推出了Web与移动App云上部署解决方案,下面是方案详情介绍。
Java Web——Servlet的数据共享方案:HttpServletRequest接口(请求作用域对象)
Java Web——Servlet的数据共享方案:HttpServletRequest接口(请求作用域对象)
Java Web——Servlet的数据共享方案:HttpSession接口(会话作用域对象)
Java Web——Servlet的数据共享方案:HttpSession接口(会话作用域对象)
Java Web——Servlet的数据共享方案:ServletContext接口(全局作用域对象)
Java Web——Servlet的数据共享方案:ServletContext接口(全局作用域对象)