SSE (我的方式)
很多人可能不知道,服务端向客户端推送消息,其实除了可以用WebSocket这种耳熟能详的机制外,还有一种服务器发送事件(Server-sent events),简称SSE。
SSE它是基于HTTP协议的,我们知道一般意义上的HTTP协议是无法做到服务端主动向客户端推送消息的,但SSE是个例外,它变换了一种思路。
SSE在服务器和客户端之间打开一个单向通道,服务端响应的不再是一次性的数据包而是text/event-stream类型的数据流信息,在有数据变更时从服务器流式传输到客户端。
整体的实现思路有点类似于在线视频播放,视频流会连续不断的推送到浏览器,你也可以理解成,客户端在完成一次用时很长(网络不畅)的下载。
SSE与WebSocket作用相似,都可以建立服务端与浏览器之间的通信,实现服务端向客户端推送消息,但还是有些许不同:
- 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 */ 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 */ 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); } } }
我们模拟服务端推送消息,看下客户端收到了消息,和我们预期的效果一致。
注意: SSE不支持IE浏览器,对其他主流浏览器兼容性做的还不错。
MQTT
什么是 MQTT协议?
MQTT 全称(Message Queue Telemetry Transport):一种基于发布/订阅(publish/subscribe)模式的轻量级通讯协议,通过订阅相应的主题来获取消息,是物联网(Internet of Thing)中的一个标准传输协议。
该协议将消息的发布者(publisher)与订阅者(subscriber)进行分离,因此可以在不可靠的网络环境中,为远程连接的设备提供可靠的消息服务,使用方式与传统的MQ有点类似。
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连接上进行全双工通信的协议,建立客户端和服务器之间的通信渠道。浏览器和服务器仅需一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
图片源于网络
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连接,之后就可以进行双向通信了,效果还不错
自定义推送
上边我们给我出了6种方案的原理和代码实现,但在实际业务开发过程中,不能盲目的直接拿过来用,还是要结合自身系统业务的特点和实际场景来选择合适的方案。
推送最直接的方式就是使用第三推送平台,毕竟钱能解决的需求都不是问题 ,无需复杂的开发运维,直接可以使用,省时、省力、省心,像goEasy、极光推送都是很不错的三方服务商。
一般大型公司都有自研的消息推送平台,像我们本次实现的web站内信只是平台上的一个触点而已,短信、邮件、微信公 众号、小程序凡是可以触达到用户的渠道都可以接入进来。
图片来源于网络
消息推送系统内部是相当复杂的,诸如消息内容的维护审核、圈定推送人群、触达过滤拦截(推送的规则频次、时段、数量、黑白名单、关键词等等)、推送失败补偿非常多的模块,技术上涉及到大数据量、高并发的场景也很多。所以我们今天的实现方式在这个庞大的系统面前只是小打小闹。
Github地址
文中所提到的案例我都一一的做了实现,整理放在了Github上,觉得有用就 Star 一下吧!
“
传送门:https://github.com/chengxy-nds/Springboot-Notebook/tree/master/springboot-realtime-data







