Files

425 lines
11 KiB
Plaintext

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<string> = []
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<IMessageQueueItem> = []
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<IMessageBase>()
// #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