From 0558c9aa1b551e750e617692da2221be77900fc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E6=97=A6?= Date: Sun, 29 Jun 2025 19:18:59 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0ws=E8=B0=83=E8=AF=95?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=20=E6=89=93=E5=8D=B0=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/services/ws.ts | 152 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 137 insertions(+), 15 deletions(-) diff --git a/src/services/ws.ts b/src/services/ws.ts index aa5ad94..9b6c06d 100644 --- a/src/services/ws.ts +++ b/src/services/ws.ts @@ -9,6 +9,33 @@ const WS_CONFIG = { heartbeatResponseType: 'pong', // 心跳响应类型 }; +// WebSocket关闭码说明 +const WS_CLOSE_CODES: Record = { + 1000: '正常关闭', + 1001: '端点离开(如页面关闭)', + 1002: '协议错误', + 1003: '接收到不支持的数据类型', + 1004: '保留码', + 1005: '未收到预期的状态码', + 1006: '连接异常关闭', + 1007: '接收到无效的frame payload数据', + 1008: '策略违规(服务器主动关闭)', + 1009: '消息过大', + 1010: '扩展协商失败', + 1011: '服务器遇到意外情况', + 1012: '服务重启', + 1013: '稍后重试', + 1014: '网关超时', + 1015: 'TLS握手失败', +}; + +// 获取关闭原因描述 +function getCloseReasonDescription(code: number, reason: string): string { + const codeDescription = WS_CLOSE_CODES[code] || '未知关闭码'; + const reasonText = reason ? ` (原因: ${reason})` : ''; + return `${code} - ${codeDescription}${reasonText}`; +} + // 增强的WebSocket包装器 class EnhancedWebSocket { private ws: WebSocket; @@ -24,10 +51,16 @@ class EnhancedWebSocket { private userOnClose: ((event: CloseEvent) => void) | null = null; private userOnError: ((event: Event) => void) | null = null; private userOnOpen: ((event: Event) => void) | null = null; + private connectionStartTime: number = 0; + private lastHeartbeatTime: number = 0; + private heartbeatSentCount: number = 0; + private heartbeatReceivedCount: number = 0; constructor(path: string, baseUrl: string) { this.path = path; this.baseUrl = baseUrl; + this.connectionStartTime = Date.now(); + console.log(`🔗 开始创建WebSocket连接: ${this.path}, 基础URL: ${baseUrl}`); this.ws = new WebSocket(baseUrl + path); this.setupHandlers(); } @@ -35,13 +68,19 @@ class EnhancedWebSocket { // 设置事件处理器 private setupHandlers(): void { this.ws.onopen = (event) => { - console.log(`WebSocket连接已建立: ${this.path}`); + const connectionDuration = Date.now() - this.connectionStartTime; + console.log(`✅ WebSocket连接已建立: ${this.path}, 耗时: ${connectionDuration}ms`); this.reconnectAttempts = 0; + this.heartbeatSentCount = 0; + this.heartbeatReceivedCount = 0; this.clearReconnectTimer(); // 🔧 优化:连接建立后立即发送一次心跳,然后开始定期心跳 if (this.ws.readyState === WebSocket.OPEN) { + console.log(`💓 连接建立后发送初始心跳: ${this.path}`); this.ws.send(WS_CONFIG.heartbeatMessage); + this.heartbeatSentCount++; + this.lastHeartbeatTime = Date.now(); this.startHeartbeatTimeout(); } this.startHeartbeat(); @@ -72,12 +111,15 @@ class EnhancedWebSocket { if (data.type === WS_CONFIG.heartbeatResponseType) { isHeartbeatResponse = true; } - } catch (e) { + } catch { // JSON解析失败,不是JSON格式的心跳响应 } } if (isHeartbeatResponse) { + this.heartbeatReceivedCount++; + const responseTime = Date.now() - this.lastHeartbeatTime; + console.log(`💗 收到心跳响应: ${this.path}, 响应时间: ${responseTime}ms, 已发送: ${this.heartbeatSentCount}, 已接收: ${this.heartbeatReceivedCount}`); // 心跳响应,不传递给业务代码 return; } @@ -89,7 +131,19 @@ class EnhancedWebSocket { }; this.ws.onclose = (event) => { - console.log(`WebSocket连接关闭: ${this.path}`, event.code, event.reason); + const connectionDuration = Date.now() - this.connectionStartTime; + const closeReason = getCloseReasonDescription(event.code, event.reason); + + console.log(`❌ WebSocket连接关闭: ${this.path}`); + console.log(` └─ 关闭原因: ${closeReason}`); + console.log(` └─ 连接持续时间: ${connectionDuration}ms`); + console.log(` └─ 心跳统计: 发送${this.heartbeatSentCount}次, 接收${this.heartbeatReceivedCount}次`); + console.log(` └─ 是否手动关闭: ${this.isManualClose}`); + console.log(` └─ 是否心跳超时: ${this.isHeartbeatTimeout}`); + + // 分析断连原因 + this.analyzeDisconnectionReason(event.code, event.reason); + this.stopHeartbeat(); // 先调用业务代码的关闭处理 @@ -107,7 +161,9 @@ class EnhancedWebSocket { }; this.ws.onerror = (event) => { - console.error(`WebSocket连接错误: ${this.path}`, event); + console.error(`🔥 WebSocket连接错误: ${this.path}`, event); + console.log(` └─ 连接状态: ${this.getReadyStateText()}`); + console.log(` └─ 连接URL: ${this.ws.url}`); this.stopHeartbeat(); if (this.userOnError) { @@ -116,18 +172,81 @@ class EnhancedWebSocket { }; } + // 分析断连原因 + private analyzeDisconnectionReason(code: number, reason: string): void { + console.log(`🔍 断连原因分析: ${this.path}`); + if (reason) { + console.log(` └─ 服务器提供的关闭原因: ${reason}`); + } + + switch (code) { + case 1000: + console.log(' └─ 正常关闭,可能是服务器主动关闭或客户端主动关闭'); + break; + case 1001: + console.log(' └─ 端点离开,可能是页面关闭或刷新'); + break; + case 1006: + console.log(' └─ 连接异常关闭,可能是网络问题或服务器崩溃'); + break; + case 1008: + console.log(' └─ 策略违规,服务器主动关闭连接'); + console.log(' └─ 可能原因: 1) 服务器负载过高 2) 连接频率过快 3) 服务器重启 4) 业务逻辑限制'); + break; + case 1011: + console.log(' └─ 服务器遇到意外情况,可能是服务器内部错误'); + break; + case 1012: + console.log(' └─ 服务重启,服务器正在重启'); + break; + case 1013: + console.log(' └─ 稍后重试,服务器暂时无法处理请求'); + break; + default: + if (code >= 3000 && code <= 3999) { + console.log(' └─ 应用程序定义的关闭码,可能是业务逻辑关闭'); + } else if (code >= 4000 && code <= 4999) { + console.log(' └─ 私有关闭码,可能是框架或库定义的关闭原因'); + } else { + console.log(' └─ 未知关闭码,需要进一步调查'); + } + } + + // 心跳统计分析 + if (this.heartbeatSentCount > this.heartbeatReceivedCount) { + const missedHeartbeats = this.heartbeatSentCount - this.heartbeatReceivedCount; + console.log(` └─ 心跳分析: 有${missedHeartbeats}个心跳未收到响应,可能存在网络延迟或服务器响应问题`); + } + } + + // 获取连接状态文本 + private getReadyStateText(): string { + switch (this.ws.readyState) { + case WebSocket.CONNECTING: return 'CONNECTING(0)'; + case WebSocket.OPEN: return 'OPEN(1)'; + case WebSocket.CLOSING: return 'CLOSING(2)'; + case WebSocket.CLOSED: return 'CLOSED(3)'; + default: return `UNKNOWN(${this.ws.readyState})`; + } + } + // 开始心跳检测 private startHeartbeat(): void { this.stopHeartbeat(); - console.log(`开始心跳检测: ${this.path}, 间隔: ${WS_CONFIG.heartbeatInterval}ms`); + console.log(`💓 开始心跳检测: ${this.path}, 间隔: ${WS_CONFIG.heartbeatInterval}ms`); this.heartbeatTimer = setInterval(() => { if (this.ws.readyState === WebSocket.OPEN) { + this.heartbeatSentCount++; + this.lastHeartbeatTime = Date.now(); + console.log(`💓 发送心跳: ${this.path} (#${this.heartbeatSentCount})`); this.ws.send(WS_CONFIG.heartbeatMessage); // 只有在没有进行超时检测时才设置新的超时检测 if (!this.heartbeatTimeoutTimer) { this.startHeartbeatTimeout(); } + } else { + console.log(`⚠️ 心跳检测时发现连接状态异常: ${this.path}, 状态: ${this.getReadyStateText()}`); } }, WS_CONFIG.heartbeatInterval); } @@ -135,6 +254,7 @@ class EnhancedWebSocket { // 停止心跳检测 private stopHeartbeat(): void { if (this.heartbeatTimer) { + console.log(`🛑 停止心跳检测: ${this.path}`); clearInterval(this.heartbeatTimer); this.heartbeatTimer = undefined; } @@ -145,7 +265,8 @@ class EnhancedWebSocket { private startHeartbeatTimeout(): void { // 不再自动清除,只在收到响应时清除 this.heartbeatTimeoutTimer = setTimeout(() => { - console.log(`心跳响应超时: ${this.path}, ${WS_CONFIG.heartbeatTimeout}ms内未收到响应,主动断开连接`); + console.log(`💔 心跳响应超时: ${this.path}, ${WS_CONFIG.heartbeatTimeout}ms内未收到响应,主动断开连接`); + console.log(` └─ 心跳统计: 发送${this.heartbeatSentCount}次, 接收${this.heartbeatReceivedCount}次`); // 设置心跳超时标志,触发重连 this.isHeartbeatTimeout = true; this.ws.close(1000, 'Heartbeat timeout'); // 使用正常关闭状态码,通过标志来判断是否重连 @@ -163,7 +284,7 @@ class EnhancedWebSocket { // 安排重连 private scheduleReconnect(): void { if (this.isManualClose || this.reconnectAttempts >= WS_CONFIG.maxReconnectAttempts) { - console.log(`停止重连: ${this.path}, 手动关闭: ${this.isManualClose}, 重连次数: ${this.reconnectAttempts}`); + console.log(`🚫 停止重连: ${this.path}, 手动关闭: ${this.isManualClose}, 重连次数: ${this.reconnectAttempts}`); return; } @@ -176,7 +297,7 @@ class EnhancedWebSocket { ); console.log( - `WebSocket将在${delay}ms后重连: ${this.path} (${this.reconnectAttempts}/${WS_CONFIG.maxReconnectAttempts})`, + `🔄 WebSocket将在${delay}ms后重连: ${this.path} (${this.reconnectAttempts}/${WS_CONFIG.maxReconnectAttempts})`, ); this.reconnectTimer = setTimeout(() => { @@ -188,7 +309,8 @@ class EnhancedWebSocket { private reconnect(): void { if (this.isManualClose) return; - console.log(`WebSocket重连尝试: ${this.path} (${this.reconnectAttempts}/${WS_CONFIG.maxReconnectAttempts})`); + console.log(`🔄 WebSocket重连尝试: ${this.path} (${this.reconnectAttempts}/${WS_CONFIG.maxReconnectAttempts})`); + this.connectionStartTime = Date.now(); // 创建新的WebSocket连接 this.ws = new WebSocket(this.baseUrl + this.path); @@ -271,7 +393,7 @@ class EnhancedWebSocket { } close(code?: number, reason?: string): void { - console.log(`手动关闭WebSocket: ${this.path}`); + console.log(`👋 手动关闭WebSocket: ${this.path}`); this.isManualClose = true; this.isHeartbeatTimeout = false; // 手动关闭时重置心跳超时标志 this.stopHeartbeat(); @@ -281,7 +403,7 @@ class EnhancedWebSocket { addEventListener( type: K, - listener: (this: WebSocket, ev: WebSocketEventMap[K]) => any, + listener: (this: WebSocket, ev: WebSocketEventMap[K]) => void, options?: boolean | AddEventListenerOptions, ): void { this.ws.addEventListener(type, listener, options); @@ -289,7 +411,7 @@ class EnhancedWebSocket { removeEventListener( type: K, - listener: (this: WebSocket, ev: WebSocketEventMap[K]) => any, + listener: (this: WebSocket, ev: WebSocketEventMap[K]) => void, options?: boolean | EventListenerOptions, ): void { this.ws.removeEventListener(type, listener, options); @@ -313,7 +435,7 @@ class EnhancedWebSocket { function create(path: string): Promise { const baseUrl = import.meta.env.ENV_WEBSOCKET_BASE ?? ''; - const ws = new EnhancedWebSocket(path, baseUrl) as any; + const ws = new EnhancedWebSocket(path, baseUrl) as WebSocket; return new Promise((resolve, reject) => { const timeout = setTimeout(() => { @@ -326,9 +448,9 @@ function create(path: string): Promise { resolve(ws); }); - ws.addEventListener('error', (e: any) => { + ws.addEventListener('error', (error: Event) => { clearTimeout(timeout); - reject(e); + reject(error); }); }); }