1. 背景
公司内目前有几个项目都有消息推送的功能,例如:某个业务操作之后需要推送消息给前端页面,让用户实时感知。
但是目前公司内的消息推送实现分散在在各个项目中,与业务系统强耦合,如果有其他项目需要集成消息推送功能,需要重复开发。故对消息推送功能进行公共抽取实现,提供一个通用的消息推送服务供各项目使用,减少重复开发,并且统一管控,做到降本增效。
消息推送是消息中心里重要的一环,会作为消息中心的一个模块进行设计开发实现。
主要定位是:致力于为公司各项目提供接入简单、可靠、安全稳定、快速的实时推送服务。
2.特性
2.1 分布式
消息推送是消息中心里重要的一环,会作为消息中心的一个模块进行设计开发实现。
消息中心是多节点集群部署,支持水平扩展,保证推送能力可用性。
2.2 接入简便
消息中心提供统一推送api供应用方调用,引入依赖,配置消息中心地址即可。
2.3 统一实现
无需重复开发,统一消息推送技术栈,实现长连接管理和推送能力,便于功能迭代和维护,并作为基础技术能力沉淀。
2.4 解耦
业务系统和推送能力解耦,让业务方专注业务,不用关心推送实现细节。
2.5 监控
采用Prometheus Grafana对推送服务的各项指标进行监控展示,并对异常进行报警。保证推送能力稳定性。
并且在线人数、连接数可观测。
2.6 可靠
提供心跳检测,及时重连和释放连接。保证消息不丢失,不重复推送,离线消息推送,消息补发。
2.7 并发
内部采用mq进行异步处理,支撑较高并发。
2.8 性能
内部采用forkjoin并发处理模型、缓存等手段避免耗时长的代码,提升推送效率。
3. 技术调研
下面主要介绍 web 端主要的四种消息推送方式。
3.1 短轮询
短轮询指的是前端页面每隔一定时间定时调用服务端的 HTTP 请求(如每1秒),之后由服务端返回最新的数据给前端页面。因为HTTP协议是一种无状态的、基于TCP的请求/响应模式的协议,请求只能由客户端发起然后服务端进行响应。
这种方式是实现最简单的。缺点是大部分请求是无效的,浪费了带宽和服务器资源。如果间隔很小,会对服务端造成比较大的压力。
3.2 长轮询
长轮询是前端页面向服务端发送一次 ajax 请求,服务端收到请求后保持连接,直到有新消息才返回响应并关闭连接,并且处理完响应信息后再向服务端发送新的请求
长轮询的优点很明显,在服务端没有消息的情况下不会频繁的请求,实现了服务端主动向前端推送的功能。但是服务端保持连接会消耗资源、返回数据顺序无保证、难于管理维护。(webQQ 就是使用了基于comet的长轮询技术)
3.3 Server-Sent Events
服务器发送事件是 HTML5 规范中提供的服务端事件 EventSource,浏览器在实现了该规范的前提下创建一个EventSource 连接后,便可收到服务端的发送的消息,实现一个单向通信。它类似于长轮询的机制,但是它在每一次的连接中,不只等待一次数据的更动。客户端发送一个请求到服务端 ,服务端保持这个请求直到一个新的消息准备好,将消息返回至客户端,此时不关闭连接,仍然保持它,供其它消息使用。
该方式的优点就是重复利用一个连接来处理每一个消息,缺点是只能服务端向客户端推送,并不是所有浏览器都支持。
3.4 WebSocket方案
webSocket 是 HTML5 下的一种新协议,是基于TCP的应用层协议,只需要一次连接,便可以实现全双工通信,即客户端和服务端可以相互主动发送消息。
该方式是目前服务端推送技术的主流方案,优点是双向通信,服务器与客户端之间交换的数据包头信息很小,缺点就是编码相对来说会多点,服务端处理更复杂。
最终决定采用 webSocket 方案来实现。然而websocket方式也有众多解决方案。
3.4.1 Java Websocket 规范
JavaEE 提供的规范,代码在包javax.websocket下,包含客户端 API 和服务端 API,服务端 API 完全依赖于客户端 API,只是在其基础上添加了一些功能,所以只需要导入服务端依赖即可。
优点:集成起来简单,原生的Java支持。
缺点:和 Web 服务器等共享容器耦合度高,广播、组播需要自行控制。并发量较低,调优麻烦,存在兼容性问题。
3.4.2 Spring Websocket
websocket 已经被springboot很好地集成封装了,所以在springboot上开发 websocket 服务非常方便。
该方案用到了还要用到SockJs STOMP。
SockJS 是 WebSocket 技术的一种模拟。为了应对许多浏览器不支持WebSocket协议的问题,设计了备选SockJs。开启并使用SockJS后,它会优先选用Websocket协议作为传输协议,如果浏览器不支持Websocket协议,则会在其他方案中,选择一个较好的协议进行通讯。
STOMP是面向消息的简单文本协议。使用STOMP的好处在于,它完全就是一种消息队列模式,你可以使用生产者与消费者的思想来认识它,发送消息的是生产者,接收消息的是消费者。而消费者可以通过订阅不同的destination,来获得不同的推送消息,不需要开发人员去管理这些订阅与推送目的地之前的关系。
优点:性能良好,社区活跃,技术成熟,协议栈丰富,有全套 Spring 解决方案,兼容性强。
缺点:需要对 SockJS 和 STOMP 进行学习,断线重连、心跳检测、二进制支持不好,需要自行实现。
3.4.3 netty Socket.IO
[http://Socket.IO][http_Socket.IO] 基于 Node.js 的实时应用程序框架。虽然主流浏览器都已经支持WebSocket,但仍然可能有不兼容的情况,为了兼容所有浏览器,给程序员提供一致的编程体验。它将WebSocket、AJAX和其它的通信方式全部封装成了统一的通信接口,也就是说,使用SocketIO时不用担心兼容问题,底层会自动选用最佳的通信方式。而netty-socketio是一个开源的[http://Socket.io][http_Socket.io]服务器端的一个java的实现,它基于Netty框架,同时支持Websocket和长轮询。除了Websocket的常用场景外,可以通过该组件实现安卓和IOS的消息推送。
优点:性能良好,支持广播、组播,断线重连、心跳检测、二进制。支持安卓和 IOS 平台。
缺点:有一定的学习成本,需要自行封装同 Spring 的集成,资源消耗大。
3.4.4 ReactiveStream
一些反应流规范和框架也对Websocket进行了实现。Spring Webflux和RSocket就是其中的代表,目前官方已经放出了一些相关的 DEMO。
优点:高吞吐量、高性能。
缺点:技术比较新、学习资料少,学习成本高。
总结:之前的项目采用spring websocket实现,线上已经平稳运行一段时间。坑也踩的七七八八。决定采用spring websocket技术方案。
4.整体设计
- 客户端向消息中心任一节点握手建立起WebSocket长连接,连接session保存在该节点的内存中。此时客户端定时向服务端发送心跳消息,如果超过设定的时间仍没有收到心跳,则认为客户端与服务端的长连接已经断开,然后服务端会关闭连接并清理内存中的会话信息。
- 当业务服务需要向客户端推送消息时,调用消息中心提供的api发送到消息中心。
- 消息中心收到需要推送的请求后,将消息发送到mq。
- 消息中心作为消费者,以广播模式消费消息,此时所有节点都会消费到消息。
- 节点消费消息后判断推送目标对应的session是否保存在自己维护的内存中,如果不存在直接忽略,否则通过长连接推送数据。
消息中心目前以双节点方式构成集群,每个节点负责一部分长连接,可以实现负载均衡,当连接数达到瓶颈时,也可以增加节点实现水平扩展。如果某一个节点出现宕机时,客户端通过心跳检测发现后会尝试重新与其他节点建立长连接,保证消息中心服务的可用性。