基于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 = reconnectthis.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 = nullreturn 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.dataif (type === 'send') {// 发送心跳pingsendPing()}} }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++// 开启全屏loadingwebsocket.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 = nullwebsocket.reconnectMsg?.close()websocket.reconnectMsg = nullwebsocket.retryTime = 0websocket.heartBeatRsp = 0stopRetryTimer() }/*** 监听网络连接状态,* 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 = nullconsole.log('心跳包任务停止成功')} }
- 在组件中使用
import websocket from '@/websocket'export default {mounted () {websocket.connect()} }