|
|
@@ -15,6 +15,7 @@ export class LiveSocketManager {
|
|
|
this.reconnectCount = 0
|
|
|
this.maxReconnectAttempts = 5
|
|
|
this.reconnectTimer = null
|
|
|
+ this.connectTimeoutTimer = null
|
|
|
this.networkRetryTimer = null
|
|
|
this.heartBeatTimer = null
|
|
|
this.pingTimeoutTimer = null
|
|
|
@@ -29,6 +30,7 @@ export class LiveSocketManager {
|
|
|
this.errorCount = 0
|
|
|
this.lastHeartBeatTime = 0
|
|
|
this.resolvedWsBase = hooks.wsBaseUrl || ''
|
|
|
+ this._suppressReconnect = false
|
|
|
// fs-live-ws:7115=Tomcat HTTP,7116=Netty WebSocket
|
|
|
this.fallbackWsHost = hooks.fallbackWsHost || 'ws://127.0.0.1:7116'
|
|
|
}
|
|
|
@@ -61,10 +63,8 @@ export class LiveSocketManager {
|
|
|
}
|
|
|
|
|
|
resetState() {
|
|
|
- this.isSocketOpen = false
|
|
|
- this.isConnecting = false
|
|
|
+ this._forceCloseAll(true)
|
|
|
this.isManualClose = true
|
|
|
- this.pendingSocketTask = null
|
|
|
this.reconnectCount = 0
|
|
|
this.heartBeatRetryCount = 0
|
|
|
this.lastHeartBeatTime = 0
|
|
|
@@ -87,7 +87,9 @@ export class LiveSocketManager {
|
|
|
}
|
|
|
|
|
|
connect() {
|
|
|
- if (this.isConnecting || this.pendingSocketTask) return
|
|
|
+ if (this.isConnecting || this.pendingSocketTask) {
|
|
|
+ return
|
|
|
+ }
|
|
|
if (!this._networkAvailable()) {
|
|
|
if (!this.networkRetryTimer) {
|
|
|
this.networkRetryTimer = setTimeout(() => {
|
|
|
@@ -122,20 +124,175 @@ export class LiveSocketManager {
|
|
|
close(isManual = true) {
|
|
|
if (!this.socket && !this.pendingSocketTask && !this.isSocketOpen && !this.isConnecting) return
|
|
|
this.isManualClose = isManual
|
|
|
+ this._forceCloseAll(false)
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 关闭本地 SocketTask,并尝试关闭微信全局 WS(避免 CONNECTING 残留触发 clickCheckTask) */
|
|
|
+ _forceCloseAll(useUniCloseSocket = true) {
|
|
|
this.cleanupTimers()
|
|
|
- try {
|
|
|
- const socketToClose = this.socket || this.pendingSocketTask
|
|
|
- this.socket = null
|
|
|
+ this._clearConnectTimeout()
|
|
|
+ const tasks = []
|
|
|
+ if (this.socket) tasks.push(this.socket)
|
|
|
+ if (this.pendingSocketTask && this.pendingSocketTask !== this.socket) {
|
|
|
+ tasks.push(this.pendingSocketTask)
|
|
|
+ }
|
|
|
+ this.socket = null
|
|
|
+ this.pendingSocketTask = null
|
|
|
+ this.isSocketOpen = false
|
|
|
+ this.isConnecting = false
|
|
|
+ tasks.forEach((task) => {
|
|
|
+ try {
|
|
|
+ if (task && typeof task.close === 'function') {
|
|
|
+ task.close({ code: 1000, reason: 'close' })
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ console.warn('[LiveSocket] close task failed', err)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ if (useUniCloseSocket) {
|
|
|
+ try {
|
|
|
+ uni.closeSocket({})
|
|
|
+ } catch (err) {
|
|
|
+ // 无活跃连接时会失败,可忽略
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** uni.connectSocket 必须带 success/fail/complete 之一,否则可能返回 Promise 而非 SocketTask */
|
|
|
+ _createSocketTask(url) {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ let settled = false
|
|
|
+ const finish = (task, err) => {
|
|
|
+ if (settled) return
|
|
|
+ settled = true
|
|
|
+ if (err) reject(err)
|
|
|
+ else resolve(task)
|
|
|
+ }
|
|
|
+ const options = {
|
|
|
+ url,
|
|
|
+ success: () => {},
|
|
|
+ fail: (err) => finish(null, err || new Error('connectSocket fail')),
|
|
|
+ complete: () => {}
|
|
|
+ }
|
|
|
+ let result
|
|
|
+ try {
|
|
|
+ result = uni.connectSocket(options)
|
|
|
+ } catch (err) {
|
|
|
+ finish(null, err)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (result && typeof result.then === 'function') {
|
|
|
+ result.then((task) => {
|
|
|
+ if (task && typeof task.onOpen === 'function') {
|
|
|
+ finish(task)
|
|
|
+ } else {
|
|
|
+ finish(null, new Error('connectSocket Promise 未解析为 SocketTask'))
|
|
|
+ }
|
|
|
+ }).catch((err) => finish(null, err))
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (result && typeof result.onOpen === 'function') {
|
|
|
+ finish(result)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ finish(null, new Error('connectSocket 未返回 SocketTask'))
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ _bindSocketTask(socketTask) {
|
|
|
+ this.pendingSocketTask = socketTask
|
|
|
+ this._startConnectTimeout(socketTask)
|
|
|
+
|
|
|
+ socketTask.onOpen(() => {
|
|
|
+ if (this.pendingSocketTask !== socketTask && this.socket !== socketTask) return
|
|
|
+ this._clearConnectTimeout()
|
|
|
this.pendingSocketTask = null
|
|
|
+ this.socket = socketTask
|
|
|
+ this.isConnecting = false
|
|
|
+ this.isSocketOpen = true
|
|
|
+ if (this.connectionStartTime > 0) {
|
|
|
+ this.connectionLatency = Date.now() - this.connectionStartTime
|
|
|
+ }
|
|
|
+ this.reconnectCount = 0
|
|
|
+ this._resetReconnectState()
|
|
|
+ this.heartBeatRetryCount = 0
|
|
|
+ console.log('[LiveSocket] onOpen', { latency: this.connectionLatency })
|
|
|
+ this.hooks.onOpen?.()
|
|
|
+ this._startHeartBeat()
|
|
|
+ })
|
|
|
+
|
|
|
+ socketTask.onMessage((res) => {
|
|
|
+ this.messageCount++
|
|
|
+ try {
|
|
|
+ const parsed = parseWsEnvelope(res.data)
|
|
|
+ if (parsed?.socketMessage?.cmd === 'heartbeat') {
|
|
|
+ if (this.pingTimeoutTimer) {
|
|
|
+ clearTimeout(this.pingTimeoutTimer)
|
|
|
+ this.pingTimeoutTimer = null
|
|
|
+ }
|
|
|
+ this.heartBeatRetryCount = 0
|
|
|
+ this._adjustHeartBeatInterval(true)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ this.hooks.onMessage?.(res)
|
|
|
+ } catch (err) {
|
|
|
+ console.error('消息解析异常:', err)
|
|
|
+ this.errorCount++
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ socketTask.onError((err) => {
|
|
|
+ console.error('[LiveSocket] onError', err)
|
|
|
+ this.errorCount++
|
|
|
this.isSocketOpen = false
|
|
|
this.isConnecting = false
|
|
|
- socketToClose?.close({ code: 1000, reason: isManual ? '主动关闭' : '异常关闭' })
|
|
|
- } catch (err) {
|
|
|
- console.error('关闭WebSocket失败:', err)
|
|
|
- this.socket = null
|
|
|
- this.pendingSocketTask = null
|
|
|
+ this._clearConnectTimeout()
|
|
|
+ if (this.pendingSocketTask === socketTask) {
|
|
|
+ this.pendingSocketTask = null
|
|
|
+ }
|
|
|
+ if (this.socket === socketTask) {
|
|
|
+ this.socket = null
|
|
|
+ }
|
|
|
+ this._stopHeartBeat()
|
|
|
+ this._handleConnectionError('连接错误', err)
|
|
|
+ })
|
|
|
+
|
|
|
+ socketTask.onClose((res) => {
|
|
|
+ console.log('[LiveSocket] onClose', res?.code, res?.reason)
|
|
|
this.isSocketOpen = false
|
|
|
this.isConnecting = false
|
|
|
+ this._clearConnectTimeout()
|
|
|
+ if (this.pendingSocketTask === socketTask) {
|
|
|
+ this.pendingSocketTask = null
|
|
|
+ }
|
|
|
+ if (this.socket === socketTask) {
|
|
|
+ this.socket = null
|
|
|
+ }
|
|
|
+ this._stopHeartBeat()
|
|
|
+ this.hooks.onClose?.(res)
|
|
|
+ if (!this.isManualClose && !this._suppressReconnect && res.code !== 1000) {
|
|
|
+ this._handleReconnect()
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ _startConnectTimeout(socketTask) {
|
|
|
+ this._clearConnectTimeout()
|
|
|
+ this.connectTimeoutTimer = setTimeout(() => {
|
|
|
+ this.connectTimeoutTimer = null
|
|
|
+ if (!this.isSocketOpen && (this.pendingSocketTask === socketTask || this.socket === socketTask)) {
|
|
|
+ console.warn('[LiveSocket] connect timeout, force close')
|
|
|
+ this.close(false)
|
|
|
+ this.isManualClose = false
|
|
|
+ this._handleReconnect()
|
|
|
+ }
|
|
|
+ }, 15000)
|
|
|
+ }
|
|
|
+
|
|
|
+ _clearConnectTimeout() {
|
|
|
+ if (this.connectTimeoutTimer) {
|
|
|
+ clearTimeout(this.connectTimeoutTimer)
|
|
|
+ this.connectTimeoutTimer = null
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -226,16 +383,22 @@ export class LiveSocketManager {
|
|
|
return
|
|
|
}
|
|
|
|
|
|
- this.isConnecting = true
|
|
|
- this.isSocketOpen = false
|
|
|
this.connectionStartTime = Date.now()
|
|
|
this._resetReconnectState()
|
|
|
+ this._suppressReconnect = true
|
|
|
+ this._forceCloseAll(true)
|
|
|
+ this._suppressReconnect = false
|
|
|
+ this.isConnecting = true
|
|
|
+ this.isSocketOpen = false
|
|
|
|
|
|
let wsUrl
|
|
|
try {
|
|
|
const baseUrl = await this._resolveWsBaseUrl(liveId)
|
|
|
wsUrl = this._buildConnectUrl(baseUrl, config)
|
|
|
console.log('[LiveSocket] connect', wsUrl.replace(/APPToken=[^&]+/, 'APPToken=***'))
|
|
|
+ if (wsUrl.startsWith('ws://')) {
|
|
|
+ console.warn('[LiveSocket] 使用 ws:// 地址,微信开发者工具需勾选「不校验合法域名」')
|
|
|
+ }
|
|
|
} catch (err) {
|
|
|
console.error('构建 WebSocket 地址失败', err)
|
|
|
this.isConnecting = false
|
|
|
@@ -244,91 +407,13 @@ export class LiveSocketManager {
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
- const socketTask = uni.connectSocket({
|
|
|
- url: wsUrl,
|
|
|
- fail: (err) => {
|
|
|
- this.isConnecting = false
|
|
|
- this.pendingSocketTask = null
|
|
|
- this._handleConnectionError('连接请求失败', err)
|
|
|
- }
|
|
|
- })
|
|
|
- if (!socketTask || typeof socketTask.onOpen !== 'function') {
|
|
|
- this.isConnecting = false
|
|
|
- this._handleConnectionError('连接请求失败', new Error('connectSocket 未返回 SocketTask'))
|
|
|
- return
|
|
|
- }
|
|
|
- // 与 living.vue 一致:仅在 onOpen 后持有 socket,避免连接中 clickCheckTask 访问空队列
|
|
|
- this.pendingSocketTask = socketTask
|
|
|
-
|
|
|
- socketTask.onOpen(() => {
|
|
|
- if (this.pendingSocketTask !== socketTask && this.socket !== socketTask) return
|
|
|
- this.pendingSocketTask = null
|
|
|
- this.socket = socketTask
|
|
|
- this.isConnecting = false
|
|
|
- this.isSocketOpen = true
|
|
|
- if (this.connectionStartTime > 0) {
|
|
|
- this.connectionLatency = Date.now() - this.connectionStartTime
|
|
|
- }
|
|
|
- this.reconnectCount = 0
|
|
|
- this._resetReconnectState()
|
|
|
- this.heartBeatRetryCount = 0
|
|
|
- this.hooks.onOpen?.()
|
|
|
- this._startHeartBeat()
|
|
|
- })
|
|
|
-
|
|
|
- socketTask.onMessage((res) => {
|
|
|
- this.messageCount++
|
|
|
- try {
|
|
|
- const parsed = parseWsEnvelope(res.data)
|
|
|
- if (parsed?.socketMessage?.cmd === 'heartbeat') {
|
|
|
- if (this.pingTimeoutTimer) {
|
|
|
- clearTimeout(this.pingTimeoutTimer)
|
|
|
- this.pingTimeoutTimer = null
|
|
|
- }
|
|
|
- this.heartBeatRetryCount = 0
|
|
|
- this._adjustHeartBeatInterval(true)
|
|
|
- return
|
|
|
- }
|
|
|
- this.hooks.onMessage?.(res)
|
|
|
- } catch (err) {
|
|
|
- console.error('消息解析异常:', err)
|
|
|
- this.errorCount++
|
|
|
- }
|
|
|
- })
|
|
|
-
|
|
|
- socketTask.onError((err) => {
|
|
|
- console.error('WebSocket连接错误:', err)
|
|
|
- this.errorCount++
|
|
|
- this.isSocketOpen = false
|
|
|
- this.isConnecting = false
|
|
|
- if (this.pendingSocketTask === socketTask) {
|
|
|
- this.pendingSocketTask = null
|
|
|
- }
|
|
|
- if (this.socket === socketTask) {
|
|
|
- this.socket = null
|
|
|
- }
|
|
|
- this._stopHeartBeat()
|
|
|
- this._handleConnectionError('连接错误', err)
|
|
|
- })
|
|
|
-
|
|
|
- socketTask.onClose((res) => {
|
|
|
- this.isSocketOpen = false
|
|
|
- this.isConnecting = false
|
|
|
- if (this.pendingSocketTask === socketTask) {
|
|
|
- this.pendingSocketTask = null
|
|
|
- }
|
|
|
- if (this.socket === socketTask) {
|
|
|
- this.socket = null
|
|
|
- }
|
|
|
- this._stopHeartBeat()
|
|
|
- this.hooks.onClose?.(res)
|
|
|
- if (!this.isManualClose && res.code !== 1000) {
|
|
|
- this._handleReconnect()
|
|
|
- }
|
|
|
- })
|
|
|
+ const socketTask = await this._createSocketTask(wsUrl)
|
|
|
+ this._bindSocketTask(socketTask)
|
|
|
} catch (e) {
|
|
|
- console.error('创建WebSocket异常:', e)
|
|
|
- this._handleReconnect()
|
|
|
+ console.error('[LiveSocket] 创建连接失败:', e)
|
|
|
+ this.isConnecting = false
|
|
|
+ this.pendingSocketTask = null
|
|
|
+ this._handleConnectionError('连接请求失败', e)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -437,7 +522,8 @@ export class LiveSocketManager {
|
|
|
}
|
|
|
if (this.reconnectCount < this.maxReconnectAttempts) {
|
|
|
this.reconnectCount++
|
|
|
- const totalDelay = Math.min(1000 * Math.pow(2, this.reconnectCount - 1) + 10000 + Math.random() * 1000, 30000)
|
|
|
+ const baseWait = this.reconnectCount <= 1 ? 500 : 10000
|
|
|
+ const totalDelay = Math.min(1000 * Math.pow(2, this.reconnectCount - 1) + baseWait + Math.random() * 1000, 30000)
|
|
|
this.reconnectTimer = setTimeout(() => {
|
|
|
this.reconnectTimer = null
|
|
|
if (this._networkAvailable() && !this.isManualClose && !this.isAvailable()) {
|