websocket的基本使用,与消息推送的一般设计

本文章需要先阅读前面写的Bus事件,传送门:https://editor.csdn.net/md/?articleId=139666035

// Bus事件,传送门:https://editor.csdn.net/md/?articleId=139666035
import Bus from "@/utils/Bus";
// 全局config的ws异常事件名
import {websocketErrorEventName} from "@/config";
// 获取用户token
import {getToken} from "@/utils/auth";

// 本文章需要先阅读前面写的Bus事件,传送门:https://editor.csdn.net/md/?articleId=139666035

// socket基地址
const socketUrl = import.meta.env.VITE_API_SOCKET_BASE;
class Socket {
    // 是否断线自动重连
    private autoConnect: boolean = true;
    // 自动重连计数器,连接上websocket时自动归置为0,以负数作为连接计数
    public isActive: number = 0;
    // 贮存的socket对象
    private socket: WebSocket | undefined;
    // 心跳时间间隔(单位:秒)
    private heartInterval: number = 35;
    // 心跳重连定时器
    private reconnectTimeOut: NodeJS.Timeout | null = null;
    // 重连时间间隔,即每次websocket error(错误)/close(关闭时)都会自动重连,当重
    private reconnectInterval: number = 10;
    // 最大重连次数(当自动重连N次后,默认服务器异常,终止自动连接)
    private reconnectMax: number = 10;
    // 心跳发送检测回应计时器
    private heartbeatTimer: NodeJS.Timeout | null = null;
    // 自动装箱错误处理事件集
    private autoEmitEvents: string[] = [];
    // 心跳发送计数器时递增,当服务端返回心跳回应时则递减
    private noHeartBackNum: number = 0;
    // 是否通过Bus事件中心向上推送ws心跳检查无返回异常
    private isExport: boolean = false;

    constructor(
        private url: string = socketUrl,
        private protocol: any[] = [],
    ) {
        this.initSocket();
    }
    // 初始化websocket
    private initSocket() {
        if(this.socket) {
            this.socket.close();
            this.socket = undefined;
        }
        if (typeof window.WebSocket === "function") {
            // getToken() 为jwt认证中的token,可加可不加,如果是通过,也可以在请求头中加入token
            // 依据个人喜好,如果在请求头中加入token,则在new Socket时传递第二个参数
            const URL = this.url + (getToken? "/" + getToken() : '');
            this.socket = new WebSocket(this.url, this.protocol);
            console.log('连接websocket 的地址为: ' + this.url);
        } else {
            console.log("WebSocket 不兼容的浏览器!");
            return;
        }
        const _this = this;
        // 为websocket各种事件定制自己的响应函数
        this.socket.onopen = function () {
            _this.open();
        };
        this.socket.onclose = function () {
            _this.close();
        };
        this.socket.onerror = function () {
            _this.error();
        };
        this.socket.onmessage = function (ev) {
            _this.dealMessage(ev);
        };
    }

    // websocket打开函数
    private open() {
        console.log("socket已打开");
        // 重连开始计数
        this.isActive = 1;
        // 如果开启了自动重连,意味着随时要检测心跳
        if (this.autoConnect) {
            this.detectHeartbeat();
        }
    }
    // websocket发生错误时的处理
    private error() {
        console.error('socket发生错误');

        this.socketClose();
    }
    // websocket关闭时的处理
    private close() {
        console.log('socket关闭');
        this.socketClose()
    }
    // 处理websocket心跳
    private detectHeartbeat() {
        // 当存在之前的心跳定时器时则先清理掉之前的心跳定时器
        if (this.heartbeatTimer) {
            // 清除原来的
            clearInterval(this.heartbeatTimer as NodeJS.Timeout);
            this.heartbeatTimer = null;
        }
        // 解决this指向问题,因为setInterval并不是使用的箭头函数
        const _this = this;
        _this.heartbeatTimer = setInterval(function () {
            // 当检查到websocket的状态是open的状态时,则发送心跳包
            if (_this.examOpen()) {
                // 发送心跳包
                _this.send<string>("SocketHeart", "ping");
                // 如果发送心跳包超过5次没有回应默认为服务器异常
                if(_this.noHeartBackNum > 5) {
                    clearInterval(_this.heartbeatTimer as NodeJS.Timeout);
                    _this.heartbeatTimer = null;
                    // 向上传递服务器异常,websocketErrorEventName为全局config的ws异常事件名
                    // 这里可以将websocketErrorEventName更改为自己的全局ws异常事件名,也可注释掉该行代码
                    Bus.on(websocketErrorEventName);
                }
            }
            // 否则默认ws异常关闭websocket
            else _this.socketClose();
        },
            // 心跳间隔时长
            _this.heartInterval * 1000
        );
    }

    // 格式化ws数据包
    /**
     * 本人常以
     * {
     *     action: 'ws处理的事件名称', //是添加好友,群消息,单聊消息,系统消息的区分
     *     data: 相应事件的数据包,
     * }
     * 为ws的数据格式,同样服务端的数据回执也是以本格式的上添加一些校验而已
      */
    /**
     * 格式化ws数据包
     * @param action ws处理的事件名称
     * @param data 相应事件的数据包
     * @private
     */
    private stringifyData(action: string, data: any) {
        const backData = {
            action,
            data: {
                // callback为了区分多个相同的action,但是返回的data数据包不同,自动增加的字段
                callback: action + "$--$" + parseInt((Math.random() * 1000).toString()),
                ...data,
            },
        };
        return JSON.stringify(backData);
    }

    /**
     * 发送ws消息
     * @param action ws处理的事件名称
     * @param data 相应事件的数据包
     * @param errorBack 发送数据后服务器返回错误的状态码时的处理函数,可不传递,此处需要前置的Bus事件中心的知识
     */
    public send<T = any>(action: string, data?: T, errorBack: any = undefined) {
        return new Promise<{action: string, callback: number}>((resolve, reject) => {
            // 先检查ws的状态
            if (this.examOpen()) {
                // 格式化数据包
                const sendData = this.stringifyData(action, data);
                // 解析数据包为下面的errorBack作准备
                const parseData = JSON.parse(sendData);
                // 发送数据包
                this.socket?.send(sendData);
                // 非心跳检查数据包时,打印发送的消息
                if(action !== 'SocketHeart') {
                    console.log('客户端发送的socket消息为: ', sendData);
                }
                // 是心跳检测试,暂时认为服务无回应,无回应次数递增,当服务有回应时则递减
                else this.noHeartBackNum ++;

                // 当传递了errorBack,且errorBack为函数时,说明希望自动处理状态码错误的返回
                if(errorBack && typeof errorBack === 'function') {
                    // 检查Bus事件中心中是否存在,防止之前已有的事件处理的干扰
                    const initiative = Bus.hasEvent(parseData.data.callback);
                    // 不存在是直接监听事件
                    if(!initiative) Bus.emit(parseData.data.callback, errorBack);
                    // 当本对象中不存在该事件的记录时,则加入自动装箱记录
                    if(!initiative && !this.isAutoEmitEvent(parseData.data.callback))
                        this.autoEmitEvents.push(parseData.data.callback);
                }
                // 当第三个参数传递错误时示警
                else if(errorBack) console.error("send方法的第三个参数必须是个函数");

                resolve({
                    action,
                    callback: parseData.data.callback
                });
            } else {
                console.error("socket 不在连接状态");
                // 关闭ws
                this.socketClose();
                reject();
            }
        });
    }
    // 检测ws状态是否正常
    public examOpen() {
        const active = this.socket && this.socket.readyState === WebSocket.OPEN;
        // 连接状态为打开状态时,则计为1
        if (active) this.isActive = 1;

        return active;
    }
    // ws发送管理时的处理
    private socketClose() {
        // 关闭心跳检查
        if (this.heartbeatTimer) {
            clearInterval(this.heartbeatTimer as NodeJS.Timeout);
            this.heartbeatTimer = null;
        }

        // 如果为自动重连且重连次数为超过最大重连次数时,继续执行重连
        if(this.autoConnect && this.isActive >= -1 * this.reconnectMax) {
            this.reconnect();
        }
        // 重连次数超过最大重连次数时,向上传递ws异常
        if(this.isActive < -1 * this.reconnectMax) {
            console.error("socket重连"+ this.reconnectMax +"次失败,自动关闭重连");
            // 向上提醒全局服务异常
            this.isExport && Bus.on(websocketErrorEventName);
            this.isExport = true;
        }
    }
    // 主动关闭ws
    public disconnect() {
        // 主动关闭ws意味着不需要自动重连,也不需要向上传递消息
        this.autoConnect = false;
        this.isActive = 0;
        this.examOpen() && this.socket?.close();
        this.socketClose();
        console.log('socket主动已断开');
    }
    // 重连处理函数
    private reconnect() {
        // 先关闭心跳检测
        if(this.heartbeatTimer) {
            clearInterval(this.heartbeatTimer);
            this.heartbeatTimer = null;
        }
        // 当当前不处于ws重连时
        if (!this.reconnectTimeOut) {
            const _this = this;
            // 自动重连定时器打开
            _this.reconnectTimeOut = setTimeout(function () {
                // 到了重连时间间隔ws还未open时,再次自动初始化ws
                if (!_this.examOpen()) {
                    if(_this.isActive > 0) _this.isActive = 0;
                    else _this.isActive--;

                    console.log("sokcet正在尝试重新连接");
                    // 自动重连几初始化ws
                    _this.initSocket();
                }
                // 关闭当前重连定时器
                clearTimeout(_this.reconnectTimeOut as NodeJS.Timeout);
                _this.reconnectTimeOut = null;
            }, this.reconnectInterval * 1000);
        }
    }

    /**
     * 消息处理,此本作者认为是最经典的东西
     * 即如何将ws推送过来的消息进行自动推送,延长消息的作用域范围,即为回调的作用域放大方式
     * 此处极其依赖于Bus事件中心的推送处理,如果ws推送的action比较少,则可以以switch,if...else if...else丰方式实现
     * @param event MessageEvent
     */
    public dealMessage(
        event: MessageEvent<{
            code: number;
            data: string;
            callback: string;
            action: string;
            msg: string
        }>
    ) {
        // 解析返回的消息
        /**
         * 此处依据http的设计方式一样,只不过多了callback,
         * callback是对应上次action的动作,如果多次调用相同的action,
         * 则有多个相同的action消息,不同的data返回,为了区分是那次的action,特地增加的callback
         * 一般前端传递啥callback,服务端即返回啥callback
         */
        const { code, data, callback, action, msg } = JSON.parse(event.data.toString());
        if(action === 'SocketHeart') {
            this.isActive = 1;
            // 心跳有返回,则直接归零计数
            this.noHeartBackNum = 0;
        }

        // 成功返回则向上推送相应的处理函数
        if (code == 200) {
            // 当存在事件时直接卸载事件
            this.autoUninstallEvent(callback, false);
            Bus.emit(action, data);
            return data;
        }
        // 失败返回则推送相应的错误处理函数
        else {
            Bus.emit(callback, msg);
            this.autoUninstallEvent(callback);
            return null;
        }
    }

    // 判断是否存在装入返回处理函数
    private isAutoEmitEvent(eventName: string) {
        const isAuto = this.autoEmitEvents.indexOf(eventName);
        return isAuto !== -1;
    }

    // 自动卸载返回处理函数
    private autoUninstallEvent(eventName: string, delay: boolean = true) {
        let timer: NodeJS.Timeout | null = setTimeout(() => {
            // Bus事件中心存在相应的处理事件,且暂存的自动装箱错误处理事件集存在
            if(Bus.hasEvent(eventName) && this.isAutoEmitEvent(eventName)) {
                // 销毁相应错误处理事件
                Bus.off(eventName);
                // 将本事件剔除出自动装箱错误处理事件集
                let newEventArr: string[] = [];
                this.autoEmitEvents.forEach(action => {
                    if(action !== eventName) newEventArr.push(action);
                })
                this.autoEmitEvents = newEventArr;
            }
            // 延时消除相应的错误处理事件
            if(timer) {
                clearTimeout(timer);
                timer = null;
            }
        }, delay? 300 : 0);

    }
}

export default Socket;

在vue3中使用

1、加入到vue原型链中

import { createApp } from 'vue';
import App from './App.vue';

socket = new Socket();
const app = createApp(App);
app.config.globalProperties.$Socket = socket;

2、在组件中使用

import {
  getCurrentInstance,
  reactive
} from 'vue';
const { proxy } = getCurrentInstance() as any;

const message = reactive<{
  uid: string,
  username: string,
  avatar: '',
  list: Array<MessageType>
}>({
  uid: '1',
  username: '1231',
  avatar: '/xxx/xxx.png',
  list: []
})

const SendUserMessageName = "sendUserMessage";
/* 普通不处理错误返回的函数调用 */
proxy.$Socket.send(SendUserMessageName , {
	uid: '1',
	message: '你好',
	type: 'text'
});
// 结合Bus事件中心一起使用,这里是正确返回的处理
proxy.$Bus.on(SendUserMessageName, (data: MessageType) => {
	// 相应的数据处理
	// 例如
	message.list.push(data);
})

/* 处理错误返回的函数调用 */
const SendSystemMessageName = "sendSystemMessage";
// 普通不处理错误返回的函数调用
proxy.$Socket.send(SendSystemMessageName , {
	money: 1.68,
	type: 'money',
	inOut: 1
}, (msg: string) => {
	// 错误的处理函数
});
// 结合Bus事件中心一起使用,这里是正确返回的处理
proxy.$Bus.on(SendSystemMessageName, (data: MessageType) => {
	// 相应的数据处理
	// 例如
	message.list.push(data);
})

3、主动关闭websocket

import {
  getCurrentInstance,
  reactive
} from 'vue';
const { proxy } = getCurrentInstance() as any;

proxy.$Socket.disconnect();
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值