我有一个朋友~
我做了一个小破网站,现在想在网站内实现一个推送网页消息的功能。是的,就是下图中的小红点,一个非常常用的功能。
但他还没想好该怎么做。这里我帮他梳理了几个解决方案,并简单的实现了。
的推送场景有很多。例如,如果有人关注我的公众号,我就会收到一条推送消息,吸引我点击打开应用程序。
消息推送(push)通常是指网站运营人员等人员通过某种工具向用户当前网页或移动设备APP主动推送消息。
push
消息推送一般分为网页消息推送和手机消息推送。
网页消息推送
手机消息推送
以上为手机消息推送。常见的网页消息推送消息包括站点消息、未读邮件数、监控报警数等,应用也很广泛。
在具体实现之前,我们先来分析一下之前的需求。其实功能很简单。只要触发某个事件(主动共享资源或者后台主动推送消息),网页上的通知红点就会实时+1就可以了。
+1
服务器上通常会有多个消息推送表,用于记录用户推送的不同类型的消息,触发不同的事件。 前端主动查询(拉取)或被动接收(推送)来自用户的所有主动消息。已读消息数。
消息推送无非就是推送(push)和拉动(pull)。下面我们就来一一了解一下。
pull
轮询(轮询)应该是实现消息推送解决方案的最简单方法。这里我们暂时将轮询分为短轮询和长轮询Ask 。
轮询
短轮询
长轮询Ask
简短的轮询很容易理解。按照指定的时间间隔,浏览器向服务器发送 HTTP 请求。服务器实时返回未读消息数据给客户端,浏览器渲染显示。
HTTP
可以使用一个简单的JS定时器,每秒请求一次未读消息计数接口,并显示返回的数据。
setInterval(() => { // 方法请求 messageCount().then((res) => { if (资源.code === 200) { This.messageCount = www.sychzs.cn } })}, 1000);
效果还是不错的。短轮询虽然实现简单,但缺点也很明显。由于推送数据不经常变化,此时无论后端是否有新的消息产生,客户端都会发出请求,这必然会影响服务。对端造成很大的压力,浪费带宽和服务器资源。
长轮询是上述短轮询的改进版本,保证了消息的相对实时性,同时最大限度地减少了服务器资源的浪费。长轮询广泛应用于中间件,如Nacos和apollo配置中心、消息队列kafka、RocketMQZhongdu 用于长轮询。
Nacos
apollo
kafka
RocketMQ
Nacos配置中心交互模式是推式还是拉式?我在一篇文章中详细介绍了Nacos长轮询的实现原理。有兴趣的朋友可以看看。
这次使用了apollo配置中心来实现长轮询,应用了一个类DeferredResult,是在servelet3.0之后传递的 spring封装了一个异步请求机制,旨在延迟结果。
DeferredResult
servelet3.0
DeferredResult可以让容器线程在不阻塞请求线程的情况下快速释放占用的资源,从而接受更多的请求来提高系统的吞吐量,然后启动异步工作线程来处理真正的业务逻辑和处理已完成的调用DeferredResult.setResult(200)提交响应结果。
DeferredResult.setResult(200)
下面我们使用长轮询来实现消息推送。
因为一个ID可能会被多个长轮询请求监控,所以我使用了guava包来存储Multimap值提供的长轮询结构。一旦检测到关键更改,所有相应的长轮询都会做出响应。 前端获取非请求超时状态码,获知数据变化,主动查询未读消息计数接口,更新页面数据。
guava
Multimap
@Controller@RequestMapping("/轮询")公共类轮询控制器{ // 存储监控某个Id的长轮询集合 // 线程同步结构 public 静态多重地图 > watchRequests = Multimaps.synchronizedMultimap(HashMultimap.create()); /** * 设置监控 */ @Get映射(路径= "观看/{id}") @ResponseBody公共延迟结果 观看(@PathVariable String id) { DeferredResult deferredResult = 新 DeferredResult<>( TIME_OUT ); //异步请求完成后移除key,防止内存溢出 deferredResult.onCompletion(() -> .remove(id, deferredResult); ); //注册长轮询请求 deferredResult; }/** @GetMapping(路径 = "发布/ { id}") @ResponseBody 公共 String p ublish(@PathVariable String id) { 集合> deferredResults = watchRequests.get(id); for (DeferredResult deferredResult : deferredResults) { deferredResult.setResult("我更新了" + 新日期()); "success"; }
请求时如果超过设置的超时时间,则会抛出AsyncRequestTimeoutException异常。这里可以直接使用@ControllerAdvice进行全局捕获并统一返回。前端获取约定的状态码,再次发起长轮询请求。 , 等等。
AsyncRequestTimeoutException
@ControllerAdvice
@ControllerAdvice公共类AsyncRequestTimeoutHandler{@ResponseStatus(HttpStatus.NOT_MODIFIED)@ResponseBody@ExceptionHandler(AsyncRequestTimeoutException.class)公共String asyncRequest TimeoutHandler(AsyncRequestTimeoutException e) { System.out.println("异步请求超时"); "304";}}
我们来测试一下。首先,页面发起长轮询请求/polling/watch/10086来监控消息变化。请求被挂起,数据不会改变,直到超时。再次发起长轮询请求。 ;手动更改数据后/polling/publish/10086,长轮询得到响应,前端处理业务逻辑完成并再次发起请求,如此类推。
/polling/watch/10086
/polling/publish/10086
长轮询相比短轮询性能提升不少,但仍然会产生更多的请求,这是它的不完美之处。
iframe流程是在页面中插入隐藏的标签,通过请求src中的消息号API接口,在服务端和客户端之间建立长连接,服务端客户端继续向iframe传输数据。
src
iframe