import { IMessageBase, IConnectOptions, IWebSocketCallbacks, IMessageQueueItem, IChatMessage, IHeartbeatMessage, IWebSocketPayload } from './socket-types.uts' import userState from '@/stores/user.uts' class WebSocketManager { private static instance : WebSocketManager | null = null private socketTask : SocketTask | null = null private url = '' private protocols : Array = [] private header : UTSJSONObject | null = null private connected = false private connecting = false private manualClose = false private reconnectTimer : number | null = null private heartbeatTimer : number | null = null private lastHeartbeatAt = 0 private waitingHeartbeat = false private reconnectCount = 0 private reconnectInterval = 3000 private heartbeatInterval = 20000 private heartbeatTimeout = 10000 private connectTimeout = 10000 private maxReconnectCount = 5 private debug = false private callbacks : IWebSocketCallbacks = {} private messageQueue : Array = [] static getInstance() : WebSocketManager { if (WebSocketManager.instance == null) { WebSocketManager.instance = new WebSocketManager() } return WebSocketManager.instance } connect(url : string, options : IConnectOptions | null = null) : void { this.log('WebSocket connect') if (url.length == 0) { this.log('WebSocket url 不能为空') return } const urlChanged = this.url.length > 0 && this.url != url this.url = url this.applyOptions(options) if (urlChanged == true) { this.releaseSocket() } if (this.connected == true || this.connecting == true) { return } this.manualClose = false this.connecting = true this.clearReconnectTimer() this.createSocketTask() } /** * 应用配置 */ private applyOptions(options : IConnectOptions | null) : void { if (options == null) { return } if (options.reconnectInterval != null) this.reconnectInterval = options.reconnectInterval if (options.heartbeatInterval != null) this.heartbeatInterval = options.heartbeatInterval if (options.maxReconnectCount != null) this.maxReconnectCount = options.maxReconnectCount if (options.protocols != null) this.protocols = options.protocols if (options.header != null) this.header = options.header if (options.heartbeatTimeout != null) this.heartbeatTimeout = options.heartbeatTimeout if (options.connectTimeout != null) this.connectTimeout = options.connectTimeout if (options.debug != null) this.debug = options.debug } private createSocketTask() : void { this.releaseSocket() let finished = false const finishConnect = () : void => { if (finished == true) { return } finished = true this.connecting = false } const connectTimeoutTimer = setTimeout(() => { finishConnect() this.handleError({ errMsg: 'WebSocket 连接超时' }) this.scheduleReconnect('connect timeout') }, this.connectTimeout) as number try { this.socketTask = uni.connectSocket({ url: this.url, header: this.header, protocols: this.protocols }) const currentTask = this.socketTask if (currentTask == null) { clearTimeout(connectTimeoutTimer) finishConnect() return } currentTask.onOpen((res : any) => { clearTimeout(connectTimeoutTimer) finishConnect() this.connected = true this.reconnectCount = 0 this.waitingHeartbeat = false this.startHeartbeat() this.flushMessageQueue() if (this.callbacks.onOpen != null) { this.callbacks.onOpen(res) } uni.$emit('onWsOpen', res) }) currentTask.onMessage((res : OnSocketMessageCallbackResult) => { console.log(res) this.handleMessage(res) }) currentTask.onError((err : any) => { clearTimeout(connectTimeoutTimer) finishConnect() uni.$emit('onWsError', err) this.handleError(err) }) currentTask.onClose((res : any) => { clearTimeout(connectTimeoutTimer) finishConnect() uni.$emit('onWsClose', res) this.handleClose(res) }) } catch (error) { clearTimeout(connectTimeoutTimer) finishConnect() this.handleError(error) } } private handleMessage(res : OnSocketMessageCallbackResult) : void { const dataStr = res.data as string // #ifndef APP const rawData = JSON.parse(dataStr) as IMessageBase // #endif // #ifdef APP const rawJson = JSON.parseObject(dataStr) console.log("OnMessage:", rawJson) const rawData = rawJson?.parse() // #endif //if (rawData.type == "ping") console.log("OnMessage:", rawData) if (rawData != null && rawData.header.type == "pong") { this.waitingHeartbeat = false this.lastHeartbeatAt = Date.now() return } if (rawData != null && this.callbacks.onMessage != null) { uni.$emit('onWsMessage', rawData) this.callbacks.onMessage(rawData) } } send(data : IWebSocketPayload, success : (() => void) | null = null, fail : ((err : any) => void) | null = null) : void { let payload : string | IMessageBase | UTSJSONObject | null = null try { payload = JSON.stringify(data) } catch (error) { payload = null } if (payload == null) { if (fail != null) { fail({ errMsg: 'WebSocket 消息序列化失败' }) } return } if (this.socketTask == null || this.connected == false) { this.enqueueMessage(data, success, fail) return } this.socketTask.send({ data: payload, success: () => { if (success != null) { success() } }, fail: (err : any) => { this.enqueueMessage(data, success, fail) if (fail != null) { fail(err) } } }) } close(code : number = 1000, reason : string = 'manual close') : void { this.manualClose = true this.stopHeartbeat() this.clearReconnectTimer() this.connected = false this.connecting = false if (this.socketTask != null) { try { this.socketTask.close({ code, reason }) } catch (error) { this.log('关闭 WebSocket 失败', error) } } this.releaseSocket() } on(callbacks : IWebSocketCallbacks) : void { if (callbacks.onOpen != null) this.callbacks.onOpen = callbacks.onOpen if (callbacks.onMessage != null) this.callbacks.onMessage = callbacks.onMessage if (callbacks.onError != null) this.callbacks.onError = callbacks.onError if (callbacks.onClose != null) this.callbacks.onClose = callbacks.onClose if (callbacks.onReconnect != null) this.callbacks.onReconnect = callbacks.onReconnect if (callbacks.onReconnectFailed != null) this.callbacks.onReconnectFailed = callbacks.onReconnectFailed if (callbacks.onHeartbeatTimeout != null) this.callbacks.onHeartbeatTimeout = callbacks.onHeartbeatTimeout } isConnected() : boolean { return this.connected } getReconnectCount() : number { return this.reconnectCount } getStats() : UTSJSONObject { return { url: this.url, connected: this.connected, connecting: this.connecting, reconnectCount: this.reconnectCount, queueSize: this.messageQueue.length, lastHeartbeatAt: this.lastHeartbeatAt } as UTSJSONObject } removeAllListeners() : void { this.callbacks = {} } /** * 开始心跳 */ private startHeartbeat() : void { this.stopHeartbeat() if (this.heartbeatInterval <= 0) return this.heartbeatTimer = setInterval(() => { if (this.connected == false) { this.stopHeartbeat() return } if (this.waitingHeartbeat == true) { const expired = Date.now() - this.lastHeartbeatAt > this.heartbeatTimeout if (expired == true) { if (this.callbacks.onHeartbeatTimeout != null) { this.callbacks.onHeartbeatTimeout({ errMsg: 'WebSocket 心跳超时' }) } this.scheduleReconnect('heartbeat timeout') return } } this.waitingHeartbeat = true this.lastHeartbeatAt = Date.now() const heartbeatUserId = userState.uid const heartData : IMessageBase = { header: { id: "heartbeat_1700000000000", type: "ping", seqno: 1, timestamp: Date.now(), source: "client", target: "system", version: "1.0.0", platform: 'android', device_id: "device_id", network_type: 'wifi', }, payload: { uid: heartbeatUserId, status: 'sent', type: 'text', content: 'ping' } } this.send(heartData) }, this.heartbeatInterval) as number } /** * 停止心跳 */ private stopHeartbeat() : void { const timer = this.heartbeatTimer if (timer != null) { clearInterval(timer) this.heartbeatTimer = null } this.waitingHeartbeat = false } /** * 将消息入队 * @param data IWebSocketPayload * @param success (() => void) | null * @param fail fail : ((err : any) => void) | null * @return { void } */ private enqueueMessage(data : IWebSocketPayload, success : (() => void) | null, fail : ((err : any) => void) | null) : void { this.messageQueue.push({ data, success, fail }) if (this.messageQueue.length > 100) { this.messageQueue.splice(0, this.messageQueue.length - 100) } } /** * 刷新消息队列 * @return {void} */ private flushMessageQueue() : void { if (this.messageQueue.length == 0) { return } const queue = this.messageQueue.slice() this.messageQueue = [] for (let i = 0; i < queue.length; i++) { const item = queue[i] this.send(item.data, item.success, item.fail) } } private handleError(err : any) : void { if (this.callbacks.onError != null) { this.callbacks.onError(err) } this.scheduleReconnect('socket error') } private handleClose(res : any) : void { this.connected = false this.connecting = false this.stopHeartbeat() this.releaseSocket() if (this.callbacks.onClose != null) { this.callbacks.onClose(res) } if (this.manualClose == false) { this.scheduleReconnect('socket close') } } private scheduleReconnect(reason : string) : void { if (this.manualClose == true || this.url.length == 0) { return } if (this.reconnectTimer != null) { return } if (this.reconnectCount >= this.maxReconnectCount) { if (this.callbacks.onReconnectFailed != null) { this.callbacks.onReconnectFailed({ errMsg: reason, reconnectCount: this.reconnectCount }) } return } this.connected = false this.connecting = false this.stopHeartbeat() this.releaseSocket() this.reconnectCount = this.reconnectCount + 1 if (this.callbacks.onReconnect != null) { this.callbacks.onReconnect({ reason, reconnectCount: this.reconnectCount }) } const delay = this.reconnectInterval * this.reconnectCount this.reconnectTimer = setTimeout(() => { this.reconnectTimer = null this.connect(this.url) }, delay) as number } private clearReconnectTimer() : void { const timer = this.reconnectTimer if (timer != null) { clearTimeout(timer) this.reconnectTimer = null } } private releaseSocket() : void { this.socketTask = null } private log(message : string, detail : any | null = null) : void { if (this.debug == false) { return } if (detail != null) { console.log('[WebSocketManager]', message, detail) return } console.log('[WebSocketManager]', message) } } export type { IConnectOptions, IWebSocketCallbacks } export const wsManager = WebSocketManager.getInstance() export default WebSocketManager