优化socket.io重连机制
This commit is contained in:
@@ -14,6 +14,11 @@ class WatchRoomSocketManager {
|
||||
private socket: WatchRoomSocket | null = null;
|
||||
private config: WatchRoomConfig | null = null;
|
||||
private heartbeatInterval: NodeJS.Timeout | null = null;
|
||||
private heartbeatTimeoutCheck: NodeJS.Timeout | null = null;
|
||||
private lastHeartbeatResponse: number = Date.now();
|
||||
private visibilityChangeHandler: (() => void) | null = null;
|
||||
private reconnectFailedCallback: (() => void) | null = null;
|
||||
private reconnectSuccessCallback: (() => void) | null = null;
|
||||
|
||||
async connect(config: WatchRoomConfig): Promise<WatchRoomSocket> {
|
||||
if (this.socket?.connected) {
|
||||
@@ -55,19 +60,26 @@ class WatchRoomSocketManager {
|
||||
});
|
||||
}
|
||||
|
||||
// 设置事件监听
|
||||
// 设置事件监听(包括 heartbeat:pong)
|
||||
this.setupEventListeners();
|
||||
|
||||
// 开始心跳
|
||||
this.startHeartbeat();
|
||||
|
||||
// 启动心跳超时检查
|
||||
this.startHeartbeatTimeoutCheck();
|
||||
|
||||
// 设置浏览器可见性监听
|
||||
this.setupVisibilityListener();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.socket) {
|
||||
reject(new Error('Socket not initialized'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.socket.on('connect', () => {
|
||||
// 使用 once 而不是 on,避免重复注册
|
||||
this.socket.once('connect', () => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('[WatchRoom] Connected to server');
|
||||
if (this.socket) {
|
||||
@@ -75,7 +87,7 @@ class WatchRoomSocketManager {
|
||||
}
|
||||
});
|
||||
|
||||
this.socket.on('connect_error', (error) => {
|
||||
this.socket.once('connect_error', (error) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('[WatchRoom] Connection error:', error);
|
||||
reject(error);
|
||||
@@ -89,7 +101,24 @@ class WatchRoomSocketManager {
|
||||
this.heartbeatInterval = null;
|
||||
}
|
||||
|
||||
if (this.heartbeatTimeoutCheck) {
|
||||
clearInterval(this.heartbeatTimeoutCheck);
|
||||
this.heartbeatTimeoutCheck = null;
|
||||
}
|
||||
|
||||
// 移除浏览器可见性监听
|
||||
this.removeVisibilityListener();
|
||||
|
||||
if (this.socket) {
|
||||
// 移除所有事件监听器
|
||||
this.socket.off('connect');
|
||||
this.socket.off('disconnect');
|
||||
this.socket.off('error');
|
||||
this.socket.off('heartbeat:pong');
|
||||
this.socket.io.off('reconnect_attempt');
|
||||
this.socket.io.off('reconnect');
|
||||
this.socket.io.off('reconnect_failed');
|
||||
|
||||
this.socket.disconnect();
|
||||
this.socket = null;
|
||||
}
|
||||
@@ -109,6 +138,8 @@ class WatchRoomSocketManager {
|
||||
this.socket.on('connect', () => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('[WatchRoom] Socket connected');
|
||||
// 重置心跳响应时间
|
||||
this.lastHeartbeatResponse = Date.now();
|
||||
});
|
||||
|
||||
this.socket.on('disconnect', (reason) => {
|
||||
@@ -120,6 +151,33 @@ class WatchRoomSocketManager {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('[WatchRoom] Socket error:', error);
|
||||
});
|
||||
|
||||
// 监听心跳响应
|
||||
this.socket.on('heartbeat:pong', (data: { timestamp: number }) => {
|
||||
this.lastHeartbeatResponse = Date.now();
|
||||
});
|
||||
|
||||
// 监听重连尝试
|
||||
this.socket.io.on('reconnect_attempt', (attemptNumber) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('[WatchRoom] Reconnect attempt:', attemptNumber);
|
||||
});
|
||||
|
||||
// 监听重连成功
|
||||
this.socket.io.on('reconnect', (attemptNumber) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('[WatchRoom] Reconnected after', attemptNumber, 'attempts');
|
||||
// 重置心跳响应时间
|
||||
this.lastHeartbeatResponse = Date.now();
|
||||
this.reconnectSuccessCallback?.();
|
||||
});
|
||||
|
||||
// 监听重连失败
|
||||
this.socket.io.on('reconnect_failed', () => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('[WatchRoom] Reconnect failed after all attempts');
|
||||
this.reconnectFailedCallback?.();
|
||||
});
|
||||
}
|
||||
|
||||
private startHeartbeat() {
|
||||
@@ -133,6 +191,118 @@ class WatchRoomSocketManager {
|
||||
}
|
||||
}, 5000); // 每5秒发送一次心跳
|
||||
}
|
||||
|
||||
// 启动心跳超时检查
|
||||
private startHeartbeatTimeoutCheck() {
|
||||
if (this.heartbeatTimeoutCheck) {
|
||||
clearInterval(this.heartbeatTimeoutCheck);
|
||||
}
|
||||
|
||||
// 每3秒检查一次心跳超时
|
||||
this.heartbeatTimeoutCheck = setInterval(() => {
|
||||
if (!this.socket?.connected) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const timeSinceLastResponse = now - this.lastHeartbeatResponse;
|
||||
|
||||
// 如果超过15秒没有收到心跳响应,认为连接可能有问题
|
||||
if (timeSinceLastResponse > 15000) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('[WatchRoom] Heartbeat timeout detected, last response was', timeSinceLastResponse, 'ms ago');
|
||||
|
||||
// 尝试重连
|
||||
if (this.socket) {
|
||||
this.socket.disconnect();
|
||||
this.socket.connect();
|
||||
// 重置心跳响应时间,避免重复触发
|
||||
this.lastHeartbeatResponse = Date.now();
|
||||
}
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// 设置浏览器可见性监听
|
||||
private setupVisibilityListener() {
|
||||
if (typeof document === 'undefined') return;
|
||||
|
||||
this.visibilityChangeHandler = () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('[WatchRoom] Page became visible, checking connection...');
|
||||
|
||||
// 页面可见时检查连接状态
|
||||
if (this.socket && !this.socket.connected) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('[WatchRoom] Socket disconnected, attempting to reconnect...');
|
||||
this.socket.connect();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('visibilitychange', this.visibilityChangeHandler);
|
||||
}
|
||||
|
||||
// 移除浏览器可见性监听
|
||||
private removeVisibilityListener() {
|
||||
if (typeof document === 'undefined' || !this.visibilityChangeHandler) return;
|
||||
|
||||
document.removeEventListener('visibilitychange', this.visibilityChangeHandler);
|
||||
this.visibilityChangeHandler = null;
|
||||
}
|
||||
|
||||
// 设置重连失败回调
|
||||
setReconnectFailedCallback(callback: () => void) {
|
||||
this.reconnectFailedCallback = callback;
|
||||
}
|
||||
|
||||
// 设置重连成功回调
|
||||
setReconnectSuccessCallback(callback: () => void) {
|
||||
this.reconnectSuccessCallback = callback;
|
||||
}
|
||||
|
||||
// 手动重连
|
||||
async reconnect(): Promise<boolean> {
|
||||
if (!this.config) {
|
||||
console.error('[WatchRoom] No config available for reconnection');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('[WatchRoom] Manual reconnection initiated...');
|
||||
|
||||
// 如果socket存在且未连接,尝试重新连接
|
||||
if (this.socket && !this.socket.connected) {
|
||||
this.socket.connect();
|
||||
|
||||
// 等待连接结果
|
||||
return new Promise((resolve) => {
|
||||
const timeout = setTimeout(() => {
|
||||
resolve(false);
|
||||
}, 5000); // 5秒超时
|
||||
|
||||
this.socket!.once('connect', () => {
|
||||
clearTimeout(timeout);
|
||||
resolve(true);
|
||||
});
|
||||
|
||||
this.socket!.once('connect_error', () => {
|
||||
clearTimeout(timeout);
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 如果socket不存在,重新创建连接
|
||||
await this.connect(this.config);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[WatchRoom] Manual reconnection failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 单例实例
|
||||
|
||||
Reference in New Issue
Block a user