在 node.js 的第三方 module 中提供了许多优质的 WebSocket 驱动,比如 ws 和 Socket.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();
}
}