vue2使用webSocket双向通讯

基于webSocket实现双向通信,使用webworker保持心跳。

由于浏览器的资源管理策略会暂停或限制某些资源的消耗,导致前端心跳包任务时效,后端接收不到webSocket心跳主动断开,因此需要使用webworker保持心跳

  1. 引入webworker

    npm install worker-loader -D
  2. 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$/);
        }
    }
  3. 新增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

  4. 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()
        }
    }
    

  5. 创建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('心跳包任务停止成功')
        }
    }
    

  6. 在组件中使用
    import websocket from '@/websocket'
    
    export default {
        mounted () {
            websocket.connect()
        }
    }

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值