472 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// WebSocket全局配置
const WS_CONFIG = {
heartbeatInterval: 10000, // 30秒心跳间隔
heartbeatTimeout: 10000, // 心跳响应超时时间10秒给服务器更多响应时间
maxReconnectAttempts: 5, // 最大重连次数
reconnectBaseDelay: 500, // 重连基础延迟0.5秒(更快重连)
maxReconnectDelay: 10000, // 最大重连延迟10秒减少等待时间
heartbeatMessage: 'ping', // 心跳消息
heartbeatResponseType: 'pong', // 心跳响应类型
};
// WebSocket关闭码说明
const WS_CLOSE_CODES: Record<number, string> = {
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;
private path: string;
private baseUrl: string;
private heartbeatTimer?: NodeJS.Timeout;
private heartbeatTimeoutTimer?: NodeJS.Timeout;
private reconnectTimer?: NodeJS.Timeout;
private reconnectAttempts: number = 0;
private isManualClose: boolean = false;
private isHeartbeatTimeout: boolean = false;
private userOnMessage: ((event: MessageEvent) => void) | null = null;
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();
}
// 设置事件处理器
private setupHandlers(): void {
this.ws.onopen = (event) => {
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();
if (this.userOnOpen) {
this.userOnOpen(event);
}
};
this.ws.onmessage = (event) => {
const messageData = event.data;
// 🔧 优化:收到任何消息都说明连接正常,清除心跳超时检测
this.clearHeartbeatTimeout();
// 检查是否为心跳响应支持字符串和JSON格式
let isHeartbeatResponse = false;
// 1. 检查是否为简单字符串心跳响应
if (typeof messageData === 'string' && messageData === WS_CONFIG.heartbeatResponseType) {
isHeartbeatResponse = true;
}
// 2. 检查是否为JSON格式心跳响应
if (!isHeartbeatResponse && typeof messageData === 'string') {
try {
const data = JSON.parse(messageData);
if (data.type === WS_CONFIG.heartbeatResponseType) {
isHeartbeatResponse = true;
}
} catch {
// JSON解析失败不是JSON格式的心跳响应
}
}
if (isHeartbeatResponse) {
this.heartbeatReceivedCount++;
const responseTime = Date.now() - this.lastHeartbeatTime;
console.log(
`💗 收到心跳响应: ${this.path}, 响应时间: ${responseTime}ms, 已发送: ${this.heartbeatSentCount}, 已接收: ${this.heartbeatReceivedCount}`,
);
// 心跳响应,不传递给业务代码
return;
}
// 传递给业务代码
if (this.userOnMessage) {
this.userOnMessage(event);
}
};
this.ws.onclose = (event) => {
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();
// 先调用业务代码的关闭处理
if (this.userOnClose) {
this.userOnClose(event);
}
// 如果不是手动关闭,或者是心跳超时导致的关闭,则重连
if (!this.isManualClose || this.isHeartbeatTimeout) {
this.scheduleReconnect();
}
// 重置心跳超时标志
this.isHeartbeatTimeout = false;
};
this.ws.onerror = (event) => {
console.error(`🔥 WebSocket连接错误: ${this.path}`, event);
console.log(` └─ 连接状态: ${this.getReadyStateText()}`);
console.log(` └─ 连接URL: ${this.ws.url}`);
this.stopHeartbeat();
if (this.userOnError) {
this.userOnError(event);
}
};
}
// 分析断连原因
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`);
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);
}
// 停止心跳检测
private stopHeartbeat(): void {
if (this.heartbeatTimer) {
console.log(`🛑 停止心跳检测: ${this.path}`);
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = undefined;
}
this.clearHeartbeatTimeout();
}
// 开始心跳响应超时检测
private startHeartbeatTimeout(): void {
// 不再自动清除,只在收到响应时清除
this.heartbeatTimeoutTimer = setTimeout(() => {
console.log(`💔 心跳响应超时: ${this.path}, ${WS_CONFIG.heartbeatTimeout}ms内未收到响应主动断开连接`);
console.log(` └─ 心跳统计: 发送${this.heartbeatSentCount}次, 接收${this.heartbeatReceivedCount}`);
// 设置心跳超时标志,触发重连
this.isHeartbeatTimeout = true;
this.ws.close(1000, 'Heartbeat timeout'); // 使用正常关闭状态码,通过标志来判断是否重连
}, WS_CONFIG.heartbeatTimeout);
}
// 清除心跳响应超时检测
private clearHeartbeatTimeout(): void {
if (this.heartbeatTimeoutTimer) {
clearTimeout(this.heartbeatTimeoutTimer);
this.heartbeatTimeoutTimer = undefined;
}
}
// 安排重连
private scheduleReconnect(): void {
if (this.isManualClose || this.reconnectAttempts >= WS_CONFIG.maxReconnectAttempts) {
console.log(`🚫 停止重连: ${this.path}, 手动关闭: ${this.isManualClose}, 重连次数: ${this.reconnectAttempts}`);
return;
}
this.reconnectAttempts++;
// 指数退避重连策略
const delay = Math.min(
WS_CONFIG.reconnectBaseDelay * Math.pow(2, this.reconnectAttempts - 1),
WS_CONFIG.maxReconnectDelay,
);
console.log(
`🔄 WebSocket将在${delay}ms后重连: ${this.path} (${this.reconnectAttempts}/${WS_CONFIG.maxReconnectAttempts})`,
);
this.reconnectTimer = setTimeout(() => {
this.reconnect();
}, delay);
}
// 重连逻辑
private reconnect(): void {
if (this.isManualClose) return;
console.log(`🔄 WebSocket重连尝试: ${this.path} (${this.reconnectAttempts}/${WS_CONFIG.maxReconnectAttempts})`);
this.connectionStartTime = Date.now();
// 创建新的WebSocket连接
this.ws = new WebSocket(this.baseUrl + this.path);
this.setupHandlers();
}
// 清理重连定时器
private clearReconnectTimer(): void {
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = undefined;
}
}
// 公开的WebSocket属性和方法
get readyState(): number {
return this.ws.readyState;
}
get url(): string {
return this.ws.url;
}
get protocol(): string {
return this.ws.protocol;
}
get extensions(): string {
return this.ws.extensions;
}
get bufferedAmount(): number {
return this.ws.bufferedAmount;
}
get binaryType(): BinaryType {
return this.ws.binaryType;
}
set binaryType(value: BinaryType) {
this.ws.binaryType = value;
}
// 事件处理器属性
get onopen(): ((event: Event) => void) | null {
return this.userOnOpen;
}
set onopen(handler: ((event: Event) => void) | null) {
this.userOnOpen = handler;
}
get onmessage(): ((event: MessageEvent) => void) | null {
return this.userOnMessage;
}
set onmessage(handler: ((event: MessageEvent) => void) | null) {
this.userOnMessage = handler;
}
get onclose(): ((event: CloseEvent) => void) | null {
return this.userOnClose;
}
set onclose(handler: ((event: CloseEvent) => void) | null) {
this.userOnClose = handler;
}
get onerror(): ((event: Event) => void) | null {
return this.userOnError;
}
set onerror(handler: ((event: Event) => void) | null) {
this.userOnError = handler;
}
// WebSocket方法
send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void {
this.ws.send(data);
}
close(code?: number, reason?: string): void {
console.log(`👋 手动关闭WebSocket: ${this.path}`);
this.isManualClose = true;
this.isHeartbeatTimeout = false; // 手动关闭时重置心跳超时标志
this.stopHeartbeat();
this.clearReconnectTimer();
this.ws.close(code, reason);
}
addEventListener<K extends keyof WebSocketEventMap>(
type: K,
listener: (this: WebSocket, ev: WebSocketEventMap[K]) => void,
options?: boolean | AddEventListenerOptions,
): void {
this.ws.addEventListener(type, listener, options);
}
removeEventListener<K extends keyof WebSocketEventMap>(
type: K,
listener: (this: WebSocket, ev: WebSocketEventMap[K]) => void,
options?: boolean | EventListenerOptions,
): void {
this.ws.removeEventListener(type, listener, options);
}
dispatchEvent(event: Event): boolean {
return this.ws.dispatchEvent(event);
}
// 常量
static readonly CONNECTING = WebSocket.CONNECTING;
static readonly OPEN = WebSocket.OPEN;
static readonly CLOSING = WebSocket.CLOSING;
static readonly CLOSED = WebSocket.CLOSED;
readonly CONNECTING = WebSocket.CONNECTING;
readonly OPEN = WebSocket.OPEN;
readonly CLOSING = WebSocket.CLOSING;
readonly CLOSED = WebSocket.CLOSED;
}
function create(path: string): Promise<WebSocket> {
let baseUrl = '';
if (path.includes(import.meta.env.ENV_STORAGE_WEBSOCKET_BASE)) {
baseUrl = '';
} else {
baseUrl = import.meta.env.ENV_WEBSOCKET_BASE ?? '';
}
const ws = new EnhancedWebSocket(path, baseUrl) as WebSocket;
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
ws.close();
reject(new Error('WebSocket connection timeout'));
}, 10000); // 10秒连接超时
ws.addEventListener('open', () => {
clearTimeout(timeout);
resolve(ws);
});
ws.addEventListener('error', (error: Event) => {
clearTimeout(timeout);
reject(error);
});
});
}
export default { create };