后端消息推送-SSE协议
介绍
HTTP 服务器推送也称 HTTP 流,是一种客户端-服务器通信模式,它将信息从 HTTP 服务器异步推送到客户端,而无需客户端请求。现在的 web 和 app 中,越来越多的场景使用这种通信模式,比如实时的消息提醒,IM在线聊天,多人文档协作等。以前实现这种类似的功能一般都是用ajax长轮询,而现在我们有了新的、更优雅的选择 —— WebSocket 和 SSE。
- WebSocket是HTML5开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。
- SSE 是 Server-Sent Events 的简称, 是一种服务器端到客户端(浏览器)的单项消息推送。对应的浏览器端实现 Event Source 接口被制定为HTML5 的一部分。不过现在IE不支持该技术。相比于 WebSocket,SSE 简单很多,服务器端和客户端工作量都要小很多、简单很多,同时实现的功能也要有局限。
SSE与WebSocket有相似功能,都是用来建立浏览器与服务器之间的通信渠道。两者的区别在于:
- WebSocket是全双工通道,可以双向通信,功能更强;SSE是单向通道,只能服务器向浏览器端发送。
- WebSocket是一个新的协议,需要服务器端支持;SSE则是部署在 HTTP协议之上的,现有的服务器软件都支持。
- SSE是一个轻量级协议,相对简单;WebSocket是一种较重的协议,相对复杂。
- SSE默认支持断线重连,WebSocket则需要额外部署。
- SSE支持自定义发送的数据类型。
- SSE不支持CORS,参数url就是服务器网址,必须与当前网页的网址在同一个网域(domain),而且协议和端口都必须相同。WebSocket支持
本文介绍SSE的使用方式(如果系统中对这种消息的准确性和可靠性有严格的要求,则使用websocket,websocket的使用相对复杂的多)
如果想了解SSE的详细基础知识,可以参考阮一峰老师的这篇文章:Server-Sent Events 教程
SSE后端代码实现
SpringMVC中,已经集成了该功能,所以无需额外引入jar包,直接上代码:
@RestController @RequestMapping("/notice") public class NoticeController { @Autowired private NoticeService noticeService; @GetMapping(path = "createSseEmitter") public SseEmitter createSseEmitter(String id) { return noticeService.createSseEmitter(id); } @PostMapping(path = "sendMsg") public boolean sendMsg(String id, String content) { noticeService.sendMsg(id, content); return true; } } @Slf4j @Service public class NoticeServiceImpl implements NoticeService { @Autowired @Qualifier("sseEmitterCacheService") private CacheService<SseEmitter> sseEmitterCacheService; @Override public SseEmitter createSseEmitter(String clientId) { if (StringUtil.isBlank(clientId)) { clientId = UUID.randomUUID().toString().replace("-", ""); } SseEmitter sseEmitter = sseEmitterCacheService.getCache(clientId); log.info("获取SSE,id={}", clientId); final String id = clientId; sseEmitter.onCompletion(() -> { log.info("SSE已完成,关闭连接 id={}", id); sseEmitterCacheService.deleteCache(id); }); return sseEmitter; } @Override public void sendMsg(String clientId, String content) { if (sseEmitterCacheService.hasCache(clientId)) { SseEmitter sseEmitter = sseEmitterCacheService.getCache(clientId); try { sseEmitter.send(content); } catch (IOException e) { log.error("发送消息失败:{}", e.getMessage(), e); throw new BusinessRuntimeExcepption(CustomExcetionConstant.IO_ERR, "发送消息失败", e); } } else { log.error("SSE对象不存在"); throw new BusinessRuntimeExcepption("SSE对象不存在"); } } }
这里,只列出了核心的代码,简而言之,需要做到两点即可:
- 前端首先是发起一个请求,创建SseEmitter,即createSseEmitter方法,该方法必须返回一个SseEmitter对象;
- 返回的SseEmitter,后端必须要缓存起来(我用的是ehcache,也可以直接定义一个map来缓存);
前端代码
使用浏览器原生提供的方法即可:
const url = '/xx/xxx' // 1. 创建实例 var source = new EventSource(url) // 2. 事件监听 // 建立连接后,触发`open` 事件 source.addEventListener('open', (e) => { console.log('open', e) }) // 收到消息,触发`message` 事件 source.addEventListener('message', (e) => { console.log('message', e) }) // 发生错误,触发`error` 事件 source.addEventListener('error', (e) => { console.log('error', e) }) // 自定义事件 source.addEventListener('eventName', (e) => { // ... }, false) // 3. 关闭链接 source.close()
由于,我请求该接口,需要带上token,所以直接使用EventSource不行,另外这个IE也不支持。所以选择了一个工具:event-source-polyfill。
1. 先安装 event-source-polyfill
npm install event-source-polyfill --save
2. 使用
import { EventSourcePolyfill } from "event-source-polyfill"; createSource() { const url = '/xx/xx/xx' const source = new EventSourcePolyfill(url, { headers: { token: 'xxxxx' } }) source.addEventListener('open', (e) => { console.log('open', e) }) source.addEventListener('message', (e) => { console.log('message', e) }) source.addEventListener('error', (e) => { console.log('error', e) }) }
注意
前端配置了代理,所以一直收不到后端发送的消息,尝试加入以下参数:
// vue.config.js module.exports = { // ... devServer: { compress: false, .... } }