基于webSocket实现双向通信,使用webworker保持心跳。
由于浏览器的资源管理策略会暂停或限制某些资源的消耗,导致前端心跳包任务时效,后端接收不到webSocket心跳主动断开,因此需要使用webworker保持心跳
-
引入webworker
npm install worker-loader -D
- vue.config配置webworker
module.exports = { chainWebpack: config => { // web worker配置 config.module .rule('worker') .test(/\.worker\.js$/) .use('worker-loader') .loader('worker-loader') .options({ inline: 'fallback', filename: 'workerName.[hash].worker.js' }) .end(); // 解决worker 热更新 config.module.rule('js').exclude.add(/\.worker\.js$/); } }
- 新增websocket.js创建websocket单例
const token = null; const WsUrl = null; import store from '@/store' import { WS_CODE_ENUM, TASK_TYPE_ENUM } from 'config/enum'; import { startNetworkListener, stopNetworkListener, startHeartBeat, stopHeartBeat, openReconnect, clearRetry } from './wsUtils' /** * WebSocket对象实例 */ class WebSocketUtil { constructor () { /** * ws对象,全局共用同一个对象 */ this.socket = null /** * WS是否重连标识 * 0:非重连,1:重连, * ws建立连接参数 */ this.reconnect = 0 /** * 前后端心跳服务端响应次数 * ws连接建立时重置 * 第一次收到服务端响应,车辆未连接且非ws重连时,打开车辆连接弹窗 * 接收到后端心跳响应累加 */ this.pongNum = 0 /** * 前后端心跳后端未响应次数 * 前端发送心跳时加1 * 后端有响应时清空 * 车辆连接成功后, 未响应次数≥2,说明后端两次未响应,自动开启前后端重连 */ this.heartBeatRsp = 0 /** * 前后端ws重连尝试次数,最多三次, * 重连累加,重连结束重置为0 * 三次均重连失败,断开ws连接,清空单车诊断业务缓存,跳转到车辆连接页面 */ this.retryTime = 0 /** * 前后端重连任务,重试最多持续10秒,若超过10秒,则按照重试失败处理, * 第一次开启重连时,开启任务, * 重连成功、10秒未连接成功关闭任务, * 重连失败时,若用户处于车辆连接页面,toast提示; 若用户处于单车诊断页面则跳转回车辆连接页面;若用于处于非单车诊断toast提示 */ this.retryTimer = 0 /** * 重连任务toast * 重连开始开启 * 重连结束关闭、重置 */ this.reconnectMsg = null /** * 重连任务全局遮罩 * 重连开始开启 * 重连结束关闭、重置 */ this.reconnectLoading = null /** * 页面超时任务 * 最后一次下发或最后一次上报开始时间点 * 11分钟内无任务下发、任务上报判定页面超时 * 浏览器刷新、车辆连接成功后开启 * ws断开连接、重连过程中关闭 * 页面超时关闭ws,跳转单车连接页面 */ this.pageTimer = null /** * token续期任务,每5分钟一次, * 浏览器刷新、车辆连接成功后开启 * ws断开连接、重连过程中关闭 */ this.tokenPolling = null /** * 开启一个独立线程,处理心跳包任务 * setInterval是基于当前页面的定时任务,如果浏览器切换窗口/隐藏时会停止任务,这时后端接收不到前端发送的心跳包,会触发断开ws * 使用webWorker线程,可以突破浏览器默认机制 * 启动心跳后,开启独立线程,发送心跳包 * 心跳关闭时关闭独立线程 */ this.worker = null } /** * 建立WS连接 * @param {*} reconnect 是否重连,0:非重连,1:重连 * @param {*} vin 车辆VIN码 * @param {*} userName 用户信息 * @param {*} needAuth 车机授权 * @description 每次建立ws连接,重置服务端响应次数 */ connect (reconnect = 0, vin, userName, needAuth) { this.socket = new WebSocket(`${WsUrl}/${vin}/${userName}/${reconnect}/${needAuth}`, getToken()); this.reconnect = reconnect this.pongNum = 0 // 服务端响应次数 this.socket.onopen = this.onOpen.bind(this); this.socket.onmessage = this.onMessage.bind(this); this.socket.onerror = this.onError.bind(this); this.socket.onclose = this.onClose.bind(this); } /** * 开启WebSocket * 启动心跳任务 * 启动网络监听 */ onOpen () { // 启动心跳 startHeartBeat() // 启动网络监听 startNetworkListener() } /** * WebSocket响应业务处理 * 1:服务端响应次数累加 * 2:第一次收到服务端响应,根据是否重连,响应不通的车辆连接业务 * 3:服务端未响应次数重置 * 4:车辆已连接且处于重连过程时,关闭重连业务,提示重连成功 */ onMessage ({ data }) { // 服务端响应次数累加 this.pongNum++ // 收到服务端第一次信息 if (this.pongNum === 1) { // 非重连,第一次收到服务端信息,开启车辆连接弹窗 if (this.reconnect === 0) // TODO 业务操作 // 重连继续心跳计时 else TODO 业务操作 } // 心跳响应无需处理 if (data === 'pong') { // 服务端未响应次数重置 this.heartBeatRsp = 0 // 车辆已连接,存在重连 if (store.getters.connected && this.retryTime > 0) { clearRetry() Message.success('重连成功') } return } // 业务操作 } /** * WebSocket关闭处理 * 1:关闭网络监听 * 2:前后端重连业务 * 3:连接过程中ws断开异常处理 * 4: 退出业务 */ onClose (event) { console.log('WebSocket关闭:', event.code, event.reason); // 关闭网络监听 stopNetworkListener() // 服务端未响应次数 ≥ 2,需要重连 if (this.heartBeatRsp >= 2) return openReconnect() this.disconnect() } /** * 断开ws连接 * 关闭心跳包、清空tokne续期任务、关闭页面超时 */ clearWs () { this.socket?.close(); this.socket = null; stopHeartBeat() } /** * 退出单车诊断业务 * @param {boolean} clearAll 是否清空所有state数据 * 1: 车辆已连接,记录断开连接时间,用于车辆连接页面-车辆连接按钮退出后5秒不能连接判断 * 2:断开ws连接, * 3: 关闭心跳包、清空tokne续期任务、关闭页面超时判定 * 4:清空重连loading、toast提示、关闭重连超时任务 * 5: 调用退出实时模式接口,通知后端退出实施模式 * 6:清空单车诊断相关浏览器缓存 */ disconnect (clearAll = false) { console.log('WebSocket断开连接') // 记录断开连接日期 if (store.getters.connected) storage.set('WS_LAST_CLOSE_TIME', dayjs().unix()) this.clearWs() clearRetry() Message.closeAll(); // 重置信息 store.commit('RESET_STATE') } } // 懒汉模式 const LazySingleton = (function () { let _instance = null return function () { return _instance || (_instance = new WebSocketUtil()) } })() const websocket = new LazySingleton() export default websocket
- websocket工具类wsUtils.js
import websocket from '@/diagnostic/websocket' import store from '@/store' import { Message, Loading } from 'element-ui'; import WsWorker from './ws.worker.js' /** * ws通信建立成功开启心跳包任务; * 每5秒发送一次心跳包; * 每次发送心跳包累加服务端未响应次数【heartBeatRsp】; * 服务端未响应次数【heartBeatRsp】 ≥ 2,判定服务端响应超,开启前后端重连; */ export const startHeartBeat = () => { websocket.socket && websocket.socket.readyState === WebSocket.OPEN && websocket.socket.send('ping'); websocket.worker = new WsWorker() websocket.worker.postMessage({ type: 'start' }) websocket.worker.onmessage = (e) => { const { type } = e.data if (type === 'send') { // 发送心跳ping sendPing() } } } const sendPing = () => { // 服务端未响应次数累加 websocket.heartBeatRsp++ // 车辆已连接,服务端响应次数≥2,鉴定为服务端响应超时,断开ws连接,开启重连 if (store.getters.connected && websocket.heartBeatRsp >= 2) websocket.clearWs() websocket.socket && websocket.socket.readyState === WebSocket.OPEN && websocket.socket.send('ping'); } /** * ws断开连接,关闭心跳包任务, * 清空tokne续期任务,关闭页面超时判定 */ export const stopHeartBeat = () => { // 关闭心跳 websocket.worker?.postMessage({ type: 'stop' }) // 清空轮询 clearInterval(websocket.tokenPolling) websocket.tokenPolling = null // 关闭页面超时判定 clearTimeout(websocket.pageTimeout) websocket.pageTimeout = null // 关闭心跳包独立线程 websocket.worker?.terminate() } /** * 开启重连 * 每次重连需要间隔三秒 * 三次重连失败,toast提示,退出单车诊断业务 */ export const openReconnect = async () => { switch (websocket.retryTime) { case 0: retryConnect() break; case 1: case 2: await sleep(3000) retryConnect() break; default: Message.error('当前您的网络不稳定,车辆连接已断开,请重新进行连接') websocket.disconnect() break; } } export const sleep = (ms) => { return new Promise((resolve) => setTimeout(resolve, ms)) } /** * 前后端重连,重连次数累加 * 1:开启重连全屏loading、toast提示 * 2:关闭旧连接 * 3:第一次重连开启重连超时任务 * 4:开始重连 */ export const retryConnect = () => { // 重连次数累加 websocket.retryTime++ // 开启全屏loading websocket.reconnectLoading = Loading.service({ fullscreen: true }); websocket.reconnectMsg?.close() websocket.reconnectMsg = Message.warning({ message: `当前您的网络环境不稳定,正在进行第${websocket.retryTime}次重连,请等待`, duration: 0 }) // 关闭旧连接 websocket.clearWs() // 第一次重连开启重连超时任务 if (websocket.retryTime === 1) startRetryTimer() // 开启重连 websocket.connect(1) } /** * 重连超时任务 * 重连三次时间不能超过10秒,超过10秒按照重连失败处理 * 1:开启重连超时任务前,如果存在重连超时任务,先关闭 * 2:开启超时重连任务,时间10秒 * 3:10秒后,服务端未响应次数不等于0 判定重连超时,退出单车诊断业务 */ const startRetryTimer = () => { // 1:开启重连超时任务前,如果存在重连超时任务,先关闭 stopRetryTimer() // 2:暂停车云心跳计时 store.commit('connect/STOP_CONNECT_TIMER') // 3:开启超时重连任务,时间10秒 websocket.retryTimer = setTimeout(() => { // 4:10秒后,服务端未响应次数不等于0 判定重连超时,退出单车诊断业务 if (websocket.retryTime != 0) { Message.error('当前您的网络不稳定,车辆连接已断开,请重新进行连接') websocket.disconnect() } }, 1000 * 10) } /** * 关闭重连超时任务 */ const stopRetryTimer = () => { if (websocket.retryTimer) { clearTimeout(websocket.retryTimer) websocket.retryTimer = null } } /** * 关闭重连流程 * 清空重连全屏loading、toast提示 * 重置重连次数、关闭重连超时任务、重置服务端未响应次数 */ export const clearRetry = () => { websocket.reconnectLoading?.close() websocket.reconnectLoading = null websocket.reconnectMsg?.close() websocket.reconnectMsg = null websocket.retryTime = 0 websocket.heartBeatRsp = 0 stopRetryTimer() } /** * 监听网络连接状态, * ws建立通信后开启 */ export const startNetworkListener = () => { window.addEventListener('offline', offline) } /** * ws连接断开后,停止网络监听 */ export const stopNetworkListener = () => { window.removeEventListener('offline', offline) } /** * 监听网络连接状态 * 监听到网络中断:主动断开当前ws连接;网络中断后onclose事件会失效,需要主动提前断开ws * 判断车辆是否已连接,如果车辆已连接需要开启前后端三次重连 */ const offline = (e) => { // 网络中断 if (e.type === 'offline') { // 车辆已连接 if (store.getters.connected) websocket.heartBeatRsp = 2 // 后端未响应次数≥2开启重连 // 主动断开当前ws连接 websocket.clearWs() } }
- 创建webWorker进程用来处理ws心跳ws.worker.js
/** * 前后端心跳包任务,ws连接建立后每5秒发送一次,由前端主动发起,后端响应 * ws断开心跳任务清空 */ let heartBeatTimer = null onmessage = (e) => { const { type } = e.data; if (type === 'start') { heartBeatTimer = setInterval(() => { console.log('WebSocket is sending heartbeat'); postMessage({ type: 'send' }); }, 1000 * 5); } if (type === 'stop') { // 清除定时器 clearInterval(heartBeatTimer) heartBeatTimer = null console.log('心跳包任务停止成功') } }
- 在组件中使用
import websocket from '@/websocket' export default { mounted () { websocket.connect() } }