import { Config } from "../models/config"; import CryptoJS from "crypto-js" import { UUID } from 'uuid-generator-ts'; import { IMessage } from "../models/message"; import { IResponseData } from "../models/response-data"; export class MessageService { private config: Config = { host: "0.0.0.0", port: 12580, timeout: 300000, browserId: "davinWorkbench", mode: "debug", secret: "M2Q2VF5pbXRiLyRxcVRGJHR2cy9aNi52UEl5JSQ/Vj8=" }; private websocket?: WebSocket; private messageMap: Map) => void> = new Map(); private monitorMap: Map void> = new Map(); private timerId: number = 0; private reconnection: number = 0; private maxReconnection: number = 3; // 待发送消息列表 private waitSendMessage: string[] = []; // 超时检测列表 private timeoutCheckItems: Map = new Map(); private heartBeat:number=0; // websocket服务是否正常 private get isRunning(): boolean { if (this.websocket) { return this.websocket.readyState === this.websocket.OPEN; } return false; } /** * 初始化服务 * @param config 配置项 */ init(config: Config | undefined) { if (config) { Object.assign(this.config, config); if (this.config.secret) { this.config.secret = atob(this.config.secret); } this.start(); } else { throw new Error("config is null") } } private start() { const sign = this.createSign(); let wsUrl = `ws://${this.config.host}:${this.config.port}?sign=${sign}&browserId=${this.config.browserId}`; if (this.config.urlParam) { wsUrl += `&${this.config.urlParam}`; } this.websocket = new WebSocket(wsUrl); this.websocket.onmessage = (e) => { const response = this.decryptAES(e.data, this.config.secret); if (this.config.mode === "debug") { console.log("receive data", response); } const data = JSON.parse(response) as IResponseData; if (data) { const resolve = this.messageMap.get(data.requestId); if (resolve) { resolve(data); this.messageMap.delete(data.requestId); } if (this.monitorMap.has(data.requestId)) { const fn = this.monitorMap.get(data.requestId); if (fn) { fn(data); } } } } this.websocket.onclose = (e) => { console.log(`websocket 断开,code:${e.code},reason:${e.reason}`); this.reconnection++; // 断开即进入重连 // if (this.reconnection <= this.maxReconnection) { this.stop(); console.log("正在重新连接websocket..."); this.start(); // } } this.websocket.onopen=(e)=>{ this.healthMonitor(); }; } private healthMonitor() { this.timerId = setInterval(() => { if (this.waitSendMessage.length > 0 && this.isRunning) { let message = this.waitSendMessage.pop(); while (message) { this.websocket?.send(message); message = this.waitSendMessage.pop(); } } const currentTime = Date.now().valueOf(); this.timeoutCheckItems.forEach((value, key) => { const offset = currentTime - key; if (offset > this.config.timeout) { const resolve = this.messageMap.get(value.requestId); if (resolve) { resolve({ requestId: value.requestId, module: value.module, action: value.action, ret: 400, returnObj: "TimeOut" } as IResponseData); } } }); this.heartBeat++; if(this.heartBeat===60){ this.heartBeatCheck(); this.heartBeat=0; } }, 1000); } private async heartBeatCheck():Promise{ const msg:IMessage={ "requestId":UUID.createUUID(), "action":"HeartBeat", "tag":"access", "module":"WebSocketModule" } const data=await this.send(msg); return data.ret===200; } private stop() { // if (this.websocket) { // this.websocket.close(); // delete this.websocket; // } clearInterval(this.timerId); } /** * 发送消息 * @param msg * @returns */ send(msg: IMessage, fn?: (...args: any[]) => void): Promise { if (!(fn === undefined || fn === null)) { if (this.monitorMap.has(msg.requestId)) { throw new Error(`Attempted to register a second handler for '${msg.requestId}'`) } if (typeof fn !== 'function') { throw new Error(`Expected handler to be a function, but found type '${typeof fn}'`) } this.monitorMap.set(msg.requestId, async (...args) => { try { await Promise.resolve(fn(...args)) } catch (err) { console.log(err) } }) } this.timeoutCheckItems.set(Date.now().valueOf(), msg); return new Promise(resolve => { if (this.config.mode === "debug") { console.log("send data", msg); } const messageData = this.encryptAES(JSON.stringify(msg), this.config.secret); this.messageMap.set(msg.requestId, resolve); if (this.isRunning) { this.websocket?.send(messageData); } else { this.waitSendMessage.push(messageData); } }); } /** * 生成签名 */ private createSign(): string { const time = new Date().getTime(); const salt = UUID.createUUID(); const token = this.getSignToken(time, salt); const param = { "time": time, "token": token, "salt": salt, "browserId": this.config.browserId } const paramStr = JSON.stringify(param) const sign = this.encryptAES(paramStr, this.config.secret) return sign } private getSignToken(time: number, salt: string): string { const token = salt + time + this.config.secret + this.config.browserId + salt + '' const encryptAESToken = this.encryptMD5(token) return encryptAESToken } private encryptAES(text: string, key: string) { const iv = CryptoJS.lib.WordArray.random(16); const encryptText = CryptoJS.AES.encrypt(text, CryptoJS.enc.Utf8.parse(key), { "iv": iv, "mode": CryptoJS.mode.CBC, "padding": CryptoJS.pad.Pkcs7 }).toString(); const data = { "iv": CryptoJS.enc.Base64.stringify(iv), "value": encryptText }; const dataString = CryptoJS.enc.Utf8.parse(JSON.stringify(data)); return CryptoJS.enc.Base64.stringify(dataString); } private decryptAES(base64cipherText: string, key: string) { const cipherText = CryptoJS.enc.Base64.parse(base64cipherText).toString(CryptoJS.enc.Utf8); const data = JSON.parse(cipherText) as { iv: string, value: string, mac: string } const decrypted = CryptoJS.AES.decrypt(data.value, CryptoJS.enc.Utf8.parse(key), { iv: CryptoJS.enc.Base64.parse(data.iv), mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }) return CryptoJS.enc.Utf8.stringify(decrypted); } private encryptMD5(text: string) { const val = CryptoJS.MD5(text).toString() return val } }