chore: 重写初始提交(清空历史,整理后全量提交)
This commit is contained in:
@@ -0,0 +1,424 @@
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user