用TypeScript实现对WebSocket的封装-Part-1

在 node.js 的第三方 module 中提供了许多优质的 WebSocket 驱动,比如 wsSocket.IO,不过 TypeScript 内置库也已经实现了 WebSocket 驱动.因此可以无需添加第三方 WebSocket 库.

得益于 TypeScript 强大语法的支持,因此代码可以像编译型语言那样先对用到的类型进行声明,然后对接口实现定义.

先前项目前端使用 TypeScript,因此使用了 TypeScript 实现了WebSocket的聊天驱动.这些天来总结了项目关于聊天驱动底层的一些经验与问题,故记录下.

废话不说.

首先定义通知上层的事件回调接口,这些事件通常是收到 ws 数据或 ws 异常后触发的,因此可以按照功能分为:

  • 登录事件回调 : type FnOnLoginCallback,来自登录成功后的事件回调声明
  • 登出事件回调 : type FnOnLogoutCallback,来自主动登出或被强制下线的事件回调声明
  • 消息接收事件回调:type FnOnReciveCallback,来自接收到服务消息的事件回调声明
  • 异常错误事件回调: type FnOnErrorCallback,来自 ws 错误事件回调声明

声明文件chat-ws.d.ts:

//消息类型枚举
declare const enum EMsgType {
    Sys=1, // JavaScript中返回的0可能是undefined,因此不建议用0开始
    SendLogin,
    SendLogout,
    SendAck,
    SendChat,
    //...
}
//一个所有消息类型的基类
interface TMsgBase{
    msgType?:EMsgType;
}
interface TRecvLoginMsg extends TMsgBase{}
interface TRecvLogoutMsg extends TMsgBase{}
interface TRecvChatMsg extends TMsgBase{}
interface TRecvAckMsg extends TMsgBase{}

interface TSendLoginMsg extends TMsgBase{}
interface TSendLogoutMsg extends TMsgBase{}
interface TSendChatMsg extends TMsgBase{}
interface TSendAckMsg extends TMsgBase{}
type TSendPkg = TSendLoginMsg | TSendLogoutMsg | TSendChatMsg | TSendAckMsg ;

// 回调事件类型
type FnOnLoginCallback = () => void; //登录成功后,要通知上层即可,通常没有额外数据处理.
type FnOnLogoutCallback = (data: object | TLogoutMsg) => void;
type FnOnReciveCallback = (data: object | TChatMsg | TLoginMsg) => void;
type FnOnErrorCallback = (data: object | TWsErrorMsg) => void;

// 回调事件集合接口
interface IWebSocketEvent{
    onLogin: FnOnLoginCallback;
    onLogout: FnOnLogoutCallback;
    onRecive: FnOnReciveCallback;
    onError: FnOnErrorCallback;
}

//给上层接口的封装声明
interface IChatWSProvider {
    // event: IWebSocketEvent; //内置事件集合建议使用私有或保护属性

    /** 验证连接地址,如果失败代表远程地址有问题 */
    verifyRemoteAddr(addr:string):boolean;

    /** 初始化该事件回调 */
    initEvHandler(onLogin: FnOnLoginCallback, onLogout: FnOnLogoutCallback, onRecive: FnOnReciveCallback,onError: FnOnErrorCallback):void;

    /** 连接服务器并登录 */
    connect(loginInfo?: TSendPkg[]):boolean; //登录的消息数据类型可自定义,用数组可以方便往后扩展匿名身份登录.

    /** 检查当前状态是否已登录 */
    isConnected(): boolean;

    /** 下线并关闭ws,用于上层调用,不产生重连动作 */
    disconnect():void;

    /** 发送消息 */
    sendMsg(msg?: TMsgPkg):boolean;
}

对于连接而言,有了事件回调,同样少不了定时器.比如需要用定时的重连超时,消息重发超时,维持连接的心跳包等等.
实现定时器的封装 chat-timer.ts:

///<reference path="./chat-ws.d.ts" />

export enum ETimerEventType{
    /** 连接心跳 */
    HeartBeat=1,
    /** 断线重连 */
    Reconnect,
    /** 消息发送超时 */
    MsgSendTimeout,
    /** 消息接收速率限制 */
    MsgRecvRate,
}

/** 定义定时器回调函数类型 */
type FnTimerHandler = (nowTime:number,timeSpan:number) => boolean;

/** 定时器事件处理 */
interface TimerEventHandler{
    /** 回调的函数,即FnTimerHandler */
    FnOnTimer(nowTime:number,timeSpan:number):boolean;
    /** 定时事件类型 */
    readonly Type:ETimerEventType;
    /** 回调的时间跨度 */
    readonly TimeSpan:number;
    /** 最后调用时间 */
    LastInvokeTime: number;
    /** 是否启用该事件 */
    Enable: boolean;
}

export class IntervalTimer{
    constructor(private logger: ILogger){ //logger结构使用注入
        this.timerId = 0;
        this.handlers = new Map<ETimerEventType,TimerEventHandler>();
    }

    /** 定时器ID,创建后才能获得 */
    private timerId: any;
    /** 定时器调用间隔,默认1s */
    private timeSpan = 1000;
    /** 所有注册事件的集合 */
    private handlers: Map<ETimerEventType,TimerEventHandler>;
    /** 最小调用的时间跨度 */
    private readonly minTimeSpan = 1000;

    /** 获取当前时间 精度ms */
    private get Now():number{ return new Date().getTime();}

    /**根据注册事件的回调间隔求出最大公约数,用于设定定时器的执行间隔 */
    private CalcMaxCommonDivisor():number{
        if(this.handlers.size == 0) return 0;
        let dividend = 0, divisor = 0, remainder = 0, tmp=0, flag = 0;
        for (let handler of this.handlers) {
            if(0 == flag++){ //除数不可为0
                divisor = handler[1].TimeSpan;
                continue;
            }
            if(0==divisor) return divisor;
            dividend = handler[1].TimeSpan;
            if (dividend < divisor) { //交换 被除数和除数,确保 被除数>除数  
                tmp = dividend;
                dividend = divisor;
                divisor = tmp;
            }
            remainder = dividend % divisor;
            while (remainder != 0) { //辗转相除
                dividend = divisor;
                divisor = remainder;
                remainder = dividend % divisor;
            }
        }
        return divisor;
    }
    /**设置内部定时器的调用间隔 */
    private SetTimerSpan():void{
        this.timeSpan = this.CalcMaxCommonDivisor();
        if(this.timeSpan < this.minTimeSpan){
            this.timeSpan = this.minTimeSpan;
        }
    }
    /**当前的调用时间间隔 */
    public get CurrentTimeSpan():number{
        return this.timeSpan;
    }

    /**注册一个定时事件
     * @param type  定时器事件类型
     * @param timeSpan 回调间隔
     * @param fnOnTimer 事件函数
     * @param enable 是否启用
     */
    public Registry(type:ETimerEventType,timeSpan:number,fnOnTimer:FnTimerHandler,enable:boolean=false):boolean{
        if(!fnOnTimer) return false;
        //防止重复注册
        if(!this.handlers.get(type)){
            this.handlers.set(type,{
                FnOnTimer:fnOnTimer,
                TimeSpan:timeSpan,
                Type:type,
                LastInvokeTime:this.Now,
                Enable:enable
            });
            this.SetTimerSpan();
            return true;
        }
        return false;
    }

    /**设置一个定时事件是否启动
     * @param type  事件类型
     * @param enable 是否启动
     */
    public SetStatus(type:ETimerEventType,enable:boolean = true):void{
        if(this.handlers == null) return;
        let handler = this.handlers.get(type);
        if(handler !=null){
            if(handler.Enable != enable) this.logger.debug("Timer Evnet: "+type.toString() + (enable? "Enable":"Disable") + ".");
            handler.Enable = enable;
        }
    }

    /** 启动定时事件 */
    public Start():void{
        if(0 != this.timerId) return;
        this.timerId = setInterval(()=>{
            let now = this.Now;
            for(let handler of this.handlers){
                let ev = handler[1] as TimerEventHandler;
                if(handler[1] == null){
                    continue;
                }
                let timeSpan = now - ev.LastInvokeTime;
                if(ev.Enable && timeSpan > ev.TimeSpan){
                    let bHandled:boolean = ev.FnOnTimer(now,timeSpan);
                    if(bHandled) ev.LastInvokeTime=now;
                }
            }
        },this.timeSpan);
        this.logger.debug("Timer start, id: " + this.timerId +",callback span: " + this.timeSpan + "ms.");
    }

    /** 停用定时器 */
    public Stop():void{
        if(this.timerId <=0){
            this.timerId = 0;
            return;
        }
        clearInterval(this.timerId);
        this.logger.debug("Timer stop, id:" +this.timerId);
        this.timerId = 0;
    }

    /** 清理定时器注册的所有事件 */
    public Dispose():void{
        this.Stop();
        this.handlers.clear();
    }
}