技术交流,大众号:程序员小富

我们好,我是小富~

我有一个朋友~

做了一个小破站,现在要完结一个站内信web音讯推送的功用,对,便是下图这个小红点,一个很常用的功用。

我有 7种 完结web实时音讯推送的计划,7种!

不过他还没想好用什么办法做,这里我帮他收拾了一下几种计划,并简略做了完结。

我有 7种 完结web实时音讯推送的计划,7种!

案例下载,记住Star 哦

什么是音讯推送(push)

推送的场景比较多,比方有人关注我的大众号,这时我就会收到一条推送音讯,以此来招引我点击翻开运用。

音讯推送(push)通常是指网站的运营作业等人员,经过某种东西对用户当时网页或移动设备APP进行的主动音讯推送。

音讯推送一般又分为web端音讯推送移动端音讯推送

我有 7种 完结web实时音讯推送的计划,7种!

上边的这种归于移动端音讯推送,web端音讯推送常见的比如站内信、未读邮件数量、监控报警数量等,运用的也十分广泛。

我有 7种 完结web实时音讯推送的计划,7种!

在详细完结之前,咱们再来分析一下前边的需求,其实功用很简略,只需触发某个事情(主动分享了资源或许后台主动推送音讯),web页面的告诉小红点就会实时的+1就能够了。

通常在服务端会有若干张音讯推送表,用来记录用户触发不同事情所推送不同类型的音讯,前端主动查询(拉)或许被动接纳(推)用户一切未读的音讯数。

我有 7种 完结web实时音讯推送的计划,7种!

音讯推送无非是推(push)和拉(pull)两种办法,下边咱们逐个了解下。

短轮询

轮询(polling)应该是完结音讯推送计划中最简略的一种,这里咱们暂时将轮询分为短轮询长轮询

短轮询很好了解,指定的时刻间隔,由浏览器向服务器宣布HTTP恳求,服务器实时返回未读音讯数据给客户端,浏览器再做渲染显示。

一个简略的JS定时器就能够搞定,每秒钟恳求一次未读音讯数接口,返回的数据展现即可。

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

效果仍是能够的,短轮询完结固然简略,缺点也是清楚明了,因为推送数据并不会频频改变,无论后端此刻是否有新的音讯发生,客户端都会进行恳求,必然会对服务端形成很大压力,浪费带宽和服务器资源。

我有 7种 完结web实时音讯推送的计划,7种!

长轮询

长轮询是对上边短轮询的一种改进版别,在尽或许减少对服务器资源浪费的一起,确保音讯的相对实时性。长轮询在中间件中运用的很广泛,比方Nacosapollo配置中心,音讯行列kafkaRocketMQ中都有用到长轮询。

Nacos配置中心交互模型是push仍是pull?一文中我详细介绍过Nacos长轮询的完结原理,感兴趣的小伙伴能够瞅瞅。

这次我运用apollo配置中心完结长轮询的办法,运用了一个类DeferredResult,它是在servelet3.0后经过Spring封装供给的一种异步恳求机制,直意便是延迟结果。

我有 7种 完结web实时音讯推送的计划,7种!

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,长轮询得到呼应,前端处理事务逻辑完结后再次建议恳求,如此循环往复。

长轮询比较于短轮询在性能上提升了许多,但依然会发生较多的恳求,这是它的一点不完美的当地。

iframe流

iframe流便是在页面中刺进一个躲藏的<iframe>标签,经过在src中恳求音讯数量API接口,由此在服务端和客户端之间创立一条长衔接,服务端继续向iframe传输数据。

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

我有 7种 完结web实时音讯推送的计划,7种!

这种办法完结简略,前端只需一个<iframe>标签搞定了

<iframe src="/iframe/message"hljs-attribute">display:none"></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>");
        }
    }
}

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

我有 7种 完结web实时音讯推送的计划,7种!

SSE (我的办法)

许多人或许不知道,服务端向客户端推送音讯,其实除了能够用WebSocket这种耳熟能详的机制外,还有一种服务器发送事情(Server-sent events),简称SSE

SSE它是根据HTTP协议的,咱们知道一般意义上的HTTP协议是无法做到服务端主动向客户端推送音讯的,但SSE是个例外,它变换了一种思路。

我有 7种 完结web实时音讯推送的计划,7种!

SSE在服务器和客户端之间翻开一个单向通道,服务端呼应的不再是一次性的数据包而是text/event-stream类型的数据流信息,在有数据改变时从服务器流式传输到客户端。

整体的完结思路有点类似于在线视频播映,视频流会接二连三的推送到浏览器,你也能够了解成,客户端在完结一次用时很长(网络不畅)的下载。

我有 7种 完结web实时音讯推送的计划,7种!

SSEWebSocket作用相似,都能够树立服务端与浏览器之间的通讯,完结服务端向客户端推送音讯,但仍是有少许不同:

  • SSE 是根据HTTP协议的,它们不需求特殊的协议或服务器完结即可作业;WebSocket需独自服务器来处理协议。
  • SSE 单向通讯,只能由服务端向客户端单向通讯;webSocket全双工通讯,即通讯的两边能够一起发送和承受信息。
  • SSE 完结简略开发本钱低,无需引入其他组件;WebSocket传输数据需做二次解析,开发门槛高一些。
  • SSE 默许支撑断线重连;WebSocket则需求自己完结。
  • SSE 只能传送文本音讯,二进制数据需求经过编码后传送;WebSocket默许支撑传送二进制数据。

SSE 与 WebSocket 该怎么挑选?

技术并没有好坏之分,只有哪个更合适

SSE好像一向不被我们所熟知,一部分原因是呈现了WebSockets,这个供给了更丰厚的协议来履行双向、全双工通讯。关于游戏、即时通讯以及需求双向近乎实时更新的场景,拥有双向通道更具招引力。

可是,在某些情况下,不需求从客户端发送数据。而你只需求一些服务器操作的更新。比方:站内信、未读音讯数、状况更新、股票行情、监控数量等场景,SEE不管是从完结的难易和本钱上都愈加有优势。此外,SSE 具有WebSockets在规划上缺少的多种功用,例如:主动重新衔接事情ID发送恣意事情的能力。

前端只需进行一次HTTP恳求,带上唯一ID,翻开事情流,监听服务端推送的事情就能够了

<script>
    let source = null;
    let userId = 7777
    if (window.EventSource) {
        // 树立衔接
        source = new EventSource('http://localhost:7777/sse/sub/'+userId);
        setMessageInnerHTML("衔接用户=" + userId);
        /**
         * 衔接一旦树立,就会触发open事情
         * 另一种写法:source.onopen = function (event) {}
         */
        source.addEventListener('open', function (e) {
            setMessageInnerHTML("树立衔接。。。");
        }, false);
        /**
         * 客户端收到服务器发来的数据
         * 另一种写法:source.onmessage = function (event) {}
         */
        source.addEventListener('message', function (e) {
            setMessageInnerHTML(e.data);
        });
    } else {
        setMessageInnerHTML("你的浏览器不支撑SSE");
    }
</script>

服务端的完结更简略,创立一个SseEmitter对象放入sseEmitterMap进行管理

private static Map<String, SseEmitter> sseEmitterMap = new ConcurrentHashMap<>();
/**
 * 创立衔接
 *
 * @date: 2022/7/12 14:51
 * @auther: 大众号:程序员小富
 */
public static SseEmitter connect(String userId) {
    try {
        // 设置超时时刻,0表示不过期。默许30秒
        SseEmitter sseEmitter = new SseEmitter(0L);
        // 注册回调
        sseEmitter.onCompletion(completionCallBack(userId));
        sseEmitter.onError(errorCallBack(userId));
        sseEmitter.onTimeout(timeoutCallBack(userId));
        sseEmitterMap.put(userId, sseEmitter);
        count.getAndIncrement();
        return sseEmitter;
    } catch (Exception e) {
        log.info("创立新的sse衔接反常,当时用户:{}", userId);
    }
    return null;
}
/**
 * 给指定用户发送音讯
 *
 * @date: 2022/7/12 14:51
 * @auther: 大众号:程序员小富
 */
public static void sendMessage(String userId, String message) {
    if (sseEmitterMap.containsKey(userId)) {
        try {
            sseEmitterMap.get(userId).send(message);
        } catch (IOException e) {
            log.error("用户[{}]推送反常:{}", userId, e.getMessage());
            removeUser(userId);
        }
    }
}

咱们模拟服务端推送音讯,看下客户端收到了音讯,和咱们预期的效果一致。

我有 7种 完结web实时音讯推送的计划,7种!

注意: SSE不支撑IE浏览器,对其他主流浏览器兼容性做的还不错。

我有 7种 完结web实时音讯推送的计划,7种!

MQTT

什么是 MQTT协议?

MQTT 全称(Message Queue Telemetry Transport):一种根据发布/订阅(publish/subscribe)模式的轻量级通讯协议,经过订阅相应的主题来获取音讯,是物联网(Internet of Thing)中的一个规范传输协议。

该协议将音讯的发布者(publisher)与订阅者(subscriber)进行别离,因此能够在不牢靠的网络环境中,为远程衔接的设备供给牢靠的音讯服务,运用办法与传统的MQ有点类似。

我有 7种 完结web实时音讯推送的计划,7种!

TCP协议坐落传输层,MQTT 协议坐落运用层,MQTT 协议构建于TCP/IP协议上,也便是说只需支撑TCP/IP协议栈的当地,都能够运用MQTT协议。

为什么要用 MQTT协议?

MQTT协议为什么在物联网(IOT)中如此受偏爱?而不是其它协议,比方咱们更为了解的 HTTP协议呢?

  • 首要HTTP协议它是一种同步协议,客户端恳求后需求等候服务器的呼应。而在物联网(IOT)环境中,设备会很受制于环境的影响,比方带宽低、网络延迟高、网络通讯不稳定等,明显异步音讯协议更为合适IOT运用程序。

  • HTTP是单向的,假如要获取音讯客户端必须建议衔接,而在物联网(IOT)运用程序中,设备或传感器往往都是客户端,这意味着它们无法被动地接纳来自网络的指令。

  • 通常需求将一条指令或许音讯,发送到网络上的一切设备上。HTTP要完结这样的功用不但很困难,而且本钱极高。

详细的MQTT协议介绍和实践,这里我就不再赘述了,我们能够参考我之前的两篇文章,里边写的也都很详细了。

MQTT协议的介绍

我也没想到 springboot + rabbitmq 做智能家居,会这么简略

MQTT完结音讯推送

未读音讯(小红点),前端 与 RabbitMQ 实时音讯推送实践,贼简略~

Websocket

websocket应该是我们都比较了解的一种完结音讯推送的办法,上边咱们在讲SSE的时分也和websocket进行过比较。

WebSocket是一种在TCP衔接上进行全双工通讯的协议,树立客户端和服务器之间的通讯途径。浏览器和服务器仅需一次握手,两者之间就直接能够创立持久性的衔接,并进行双向数据传输。

我有 7种 完结web实时音讯推送的计划,7种!

springboot整合websocket,先引入websocket相关的东西包,和SSE比较额外的开发本钱。

<!-- 引入websocket -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

服务端运用@ServerEndpoint注解标注当时类为一个websocket服务器,客户端能够经过ws://localhost:7777/webSocket/10086来衔接到WebSocket服务器端。

@Component
@Slf4j
@ServerEndpoint("/websocket/{userId}")
public class WebSocketServer {
    //与某个客户端的衔接会话,需求经过它来给客户端发送数据
    private Session session;
    private static final CopyOnWriteArraySet<WebSocketServer> webSockets = new CopyOnWriteArraySet<>();
    // 用来存在线衔接数
    private static final Map<String, Session> sessionPool = new HashMap<String, Session>();
    /**
     * 大众号:程序员小富
     * 链接成功调用的办法
     */
    @OnOpen
    public void onOpen(Session session, @PathParam(value = "userId") String userId) {
        try {
            this.session = session;
            webSockets.add(this);
            sessionPool.put(userId, session);
            log.info("websocket音讯: 有新的衔接,总数为:" + webSockets.size());
        } catch (Exception e) {
        }
    }
    /**
     * 大众号:程序员小富
     * 收到客户端音讯后调用的办法
     */
    @OnMessage
    public void onMessage(String message) {
        log.info("websocket音讯: 收到客户端音讯:" + message);
    }
    /**
     * 大众号:程序员小富
     * 此为单点音讯
     */
    public void sendOneMessage(String userId, String message) {
        Session session = sessionPool.get(userId);
        if (session != null && session.isOpen()) {
            try {
                log.info("websocket消: 单点音讯:" + message);
                session.getAsyncRemote().sendText(message);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

前端初始化翻开WebSocket衔接,并监听衔接状况,接纳服务端数据或向服务端发送数据。

<script>
    var ws = new WebSocket('ws://localhost:7777/webSocket/10086');
    // 获取衔接状况
    console.log('ws衔接状况:' + ws.readyState);
    //监听是否衔接成功
    ws.onopen = function () {
        console.log('ws衔接状况:' + ws.readyState);
        //衔接成功则发送一个数据
        ws.send('test1');
    }
    // 接听服务器发回的信息并处理展现
    ws.onmessage = function (data) {
        console.log('接纳到来自服务器的音讯:');
        console.log(data);
        //完结通讯后封闭WebSocket衔接
        ws.close();
    }
    // 监听衔接封闭事情
    ws.onclose = function () {
        // 监听整个过程中websocket的状况
        console.log('ws衔接状况:' + ws.readyState);
    }
    // 监听并处理error事情
    ws.onerror = function (error) {
        console.log(error);
    }
    function sendMessage() {
        var content = $("#message").val();
        $.ajax({
            url: '/socket/publish?userId=10086&message=' + content,
            type: 'GET',
            data: { "id": "7777", "content": content },
            success: function (data) {
                console.log(data)
            }
        })
    }
</script>

页面初始化树立websocket衔接,之后就能够进行双向通讯了,效果还不错

我有 7种 完结web实时音讯推送的计划,7种!

我有 7种 完结web实时音讯推送的计划,7种!

自定义推送

上边咱们给我出了6种计划的原理和代码完结,但在实践事务开发过程中,不能盲目的直接拿过来用,仍是要结合自身体系事务的特色和实践场景来挑选合适的计划。

推送最直接的办法便是运用第三推送渠道,毕竟钱能解决的需求都不是问题,无需杂乱的开发运维,直接能够运用,省时、省力、省心,像goEasy、极光推送都是很不错的三方服务商。

一般大型公司都有自研的音讯推送渠道,像咱们本次完结的web站内信只是渠道上的一个触点而已,短信、邮件、微信大众号、小程序但凡能够触到达用户的途径都能够接入进来。

我有 7种 完结web实时音讯推送的计划,7种!

音讯推送体系内部是适当杂乱的,比如音讯内容的维护审阅、圈定推送人群、触达过滤阻拦(推送的规则频次、时段、数量、是非名单、关键词等等)、推送失败补偿十分多的模块,技术上涉及到大数据量、高并发的场景也许多。所以咱们今日的完结办法在这个巨大的体系面前只是小打小闹。

Github地址

文中所说到的案例我都逐个的做了完结,收拾放在了Github上,觉得有用就 Star 一下吧!

传送门:github.com/chengxy-nds…