功能
一般来说,WebSocket应该具有以下功能:
- 断线重连
- 单例模式
- 发布订阅
- 超时提示
- 心跳保活
- 错误处理
源码
源码使用ts编写:
/**
* @file websocket链接相关
*/
interface SubscribeList {
[index: string]: Function[]
}
export class Socket {
private static instance: Socket|null;
private static _isConnected: boolean = false;
private readonly cachedUrl: string = '';
private readonly cachedOnConnected: Function | undefined;
private socket!: WebSocket;
// all,通用回调函数
private cbs: Function[] = [];
private subscribeList: SubscribeList = {};
// error,socket连接出错时的回调函数
private errCbs: Function[] = [];
// 重试次数
private reconnectTimes: number = 0;
private maxReconnectTimes: number = 10;
private timeout: number = 60 * 1000; // 60s超时
private timeoutTimer: any = null;
// 心跳检测由server端触发:适用于服务端释放资源
// websocket支持心跳检测帧消息 ping/pong opcode 9/10
// 对于Javascript并没有暴露心跳相关API:https://stackoverflow.com/questions/10585355/sending-websocket-ping-pong-frame-from-browser
// 实际测试:server端发送ping帧的时候,浏览器会自动回应pong消息
constructor(url: string, onConnected?: Function) {
if (Socket.instance) {
return Socket.instance;
}
this.cachedUrl = url;
this.cachedOnConnected = onConnected;
this.socket = this.connect(url, onConnected);
Socket.instance = this;
}
static get isConnected(): boolean {
return this._isConnected;
}
connect(url: string, onConnected?: Function) {
const ws = new WebSocket(url);
ws.onopen = (...args) => {
Socket._isConnected = true;
onConnected && onConnected(...args);
};
this.init(ws);
return ws;
}
init(socket: WebSocket) {
socket.onmessage = (evt: any) => {
this.timeoutTimer && clearTimeout(this.timeoutTimer);
this.reconnectTimes = 0;
let jsonData: any = {};
try {
jsonData = JSON.parse(evt.data);
}
catch (e) {
throw new Error('[socket]: data' + evt.data + 'cannot convert to json.');
}
finally {
this.callCbs(this.cbs, jsonData);
this.callCbs(this.subscribeList[jsonData.Name] || [], jsonData);
}
};
socket.onerror = (...err) => {
Socket._isConnected = false;
Socket.instance = null;
this.reconnected();
this.errCbs.forEach((cb: Function) => cb(...err));
};
socket.onclose = (...reason) => {
Socket._isConnected = false;
Socket.instance = null;
};
}
private callCbs(cbs: Function[], jsonData: object) {
let delIdxList: number[] = [];
cbs.forEach((cb, idx) => {
cb(jsonData);
// @ts-ignore
if (cb.isOnce) {
delIdxList.push(idx);
}
});
delIdxList.forEach(it => {
cbs.splice(it, 1);
});
}
private startTimeOutTimer() {
this.timeoutTimer = setTimeout(() => {
this.callCbs(this.errCbs, new Error('[socket]: webSocket接受消息超时!'));
}, this.timeout);
}
sendJSON(data: Object) {
if (Socket._isConnected) {
this.socket.send(JSON.stringify(data, null, 4));
// 设置统一超时处理,不区分某个send请求的60s延迟,发送多个命令之后只要有响应就清除超时定时器
// 因为只要有响应就说明服务器在正常工作,应继续等待请求处理
this.startTimeOutTimer();
}
else {
this.callCbs(this.errCbs, new Error('[socket]: 当前WebSocket未连接!'));
}
}
sendPrimitive(data: string) {
if (Socket._isConnected) {
this.socket.send(data);
}
else {
this.callCbs(this.errCbs, new Error('[socket]: 当前WebSocket未连接!'));
}
}
private reconnected() {
if (!Socket._isConnected && this.reconnectTimes < this.maxReconnectTimes) {
setTimeout(() => {
this.reconnectTimes++;
this.socket = this.connect(this.cachedUrl, this.cachedOnConnected);
}, 2000);
}
}
destroy() {
this.socket.close(1000);
Socket.instance = null;
}
// 只能remove具名函数
private removeListener(cbList: Function[], cb?: Function) {
if (!cb) {
cbList.length = 0;
return;
}
for (let i = 0; i < cbList.length; i++) {
const currentCb = cbList[i];
if (cb === currentCb) {
cbList.splice(i, 1);
break;
}
}
}
unsubscribe(eventName: string, cb?: Function) {
if (eventName === 'error') {
this.removeListener(this.errCbs, cb);
}
else if (eventName === 'all') {
this.removeListener(this.cbs, cb);
}
else if (this.subscribeList[eventName]) {
this.removeListener(this.subscribeList[eventName], cb);
}
}
subscribe(eventName: string, cb: Function, once?: boolean) {
// @ts-ignore
cb.isOnce = once;
if (eventName === 'error') {
this.errCbs.push(cb);
}
else if (eventName === 'all') {
this.cbs.push(cb);
}
else {
if (!this.subscribeList[eventName]) {
this.subscribeList[eventName] = [cb];
}
else {
this.subscribeList[eventName].push(cb);
}
}
}
}