如何实现具备自动重连与心跳检测的WebSocket客户端

发布于:2025-03-16 ⋅ 阅读:(23) ⋅ 点赞:(0)

本文介绍如何通过原生WebSocket API封装一个具备自动重连、心跳检测、错误恢复等能力的稳健客户端。适用于需要长连接的实时通讯场景(如聊天室、实时数据监控等)。

核心功能亮点

  1. 自动重连机制 - 指数退避策略重连
  2. 心跳保活 - 双向检测连接活性
  3. 消息可靠性 - 失败消息自动重发
  4. 异常处理 - 错误分类处理机制
  5. 状态管理 - 精准控制连接生命周期   
  6. 关键优化点说明

  7. 事件监听优化

    • 改用addEventListener替代onopen等属性赋值,允许多监听器共存
    • 新增错误边界处理,防止初始化失败导致程序崩溃
  8. 智能重连策略

    • 避免网络恢复后仍要等待过长的延迟时间
  9. 状态管理改进

    • 通过属性访问器清晰管理连接状态
  10. 消息可靠性增强

    • 独立队列存储未发送消息
    • 连接恢复时自动重发
  11. 配置与状态分离

    • 独立config对象管理配置参数
    • state对象清晰反映运行时状态
  12.  
    /**
     * WebSocket客户端实现
     * @param {string} url - 连接地址
     * @param {string|string[]} [protocols] - 可选协议
     */
    class WebSocketClient {
    	// 默认配置(可通过构造函数覆盖)
    	static config = {
    		HEARTBEAT_INTERVAL: 30000, // 心跳间隔30s
    		HEARTBEAT_TIMEOUT: 10000, // 心跳超时10s
    		MAX_RECONNECT_ATTEMPTS: 5, // 最大重连次数
    		BASE_RECONNECT_DELAY: 1000, // 基础重连延迟
    		MAX_RECONNECT_DELAY: 30000, // 最大重连延迟30s
    		MAX_PENDING_MESSAGES: 50 // 最大消息队列长度
    	};
    
    	// 运行时状态
    	state = {
    		ws: null,
    		reconnectCount: 0, //重新连接计数
    		isUserClosed: false, //用户自己关闭
    		pendingMessages: [], //待发送的消息队列
    		heartbeatTimer: null, //心跳计时器
    		heartbeatTimeoutTimer: null //消息超时计时器
    	};
    
    	constructor(url, protocols, options = {}) {
    		// 合并配置
    		this.config = {
    			...WebSocketClient.config,
    			...options
    		};
    		this.url = url;
    		this.protocols = protocols;
    		this.connect();
    	}
    
    	/** 初始化连接 */
    	connect() {
    		this.dispose(); // 清理旧连接
    
    		try {
    			// 创建WebSocket实例
    			this.state.ws = this.protocols ?
    				new WebSocket(this.url, this.protocols) :
    				new WebSocket(this.url);
    
    			this.bindEventListeners();
    		} catch (error) {
    			console.error('WebSocket初始化失败:', error);
    			this.handleReconnect();
    		}
    	}
    
    	/** 清理旧连接和定时器 */
    	dispose() {
    		if (this.state.ws) {
    			this.state.ws.close();
    			this.state.ws = null;
    		}
    		this.clearHeartbeat();
    	}
    
    	/** 绑定事件监听器 */
    	bindEventListeners() {
    		const {
    			ws
    		} = this.state;
    
    		ws.addEventListener('open', (event) => {
    			console.log('连接已建立', event);
    			/** 重置重连状态 */
    			this.resetReconnect();
    			//开始心跳
    			this.startHeartbeat();
    			/** 发送队列中的消息 */
    			this.flushPendingMessages();
    		});
    
    		ws.addEventListener('error', (error) => {
    			console.error('连接异常:', error);
    			this.handleReconnect();
    		});
    
    		ws.addEventListener('close', (event) => {
    			console.log('连接关闭:', event);
    			this.handleClose(event);
    		});
    
    		ws.addEventListener('message', (event) => {
    			this.handleIncomingMessage(event);
    		});
    	}
    
    	/** 心跳管理 */
    	startHeartbeat() {
    		this.clearHeartbeat();
    		this.state.heartbeatTimer = setInterval(() => {
    			if (this.isConnected) {
    				this.sendHeartbeat();
    				this.monitorHeartbeatResponse();
    			}
    		}, this.config.HEARTBEAT_INTERVAL);
    	}
    
    	/** 发送心跳包 */
    	sendHeartbeat() {
    		this.send(JSON.stringify({
    			type: 'ping',
    			timestamp: Date.now()
    		}));
    	}
    
    	/** 监测心跳响应 */
    	monitorHeartbeatResponse() {
    		this.state.heartbeatTimeoutTimer = setTimeout(() => {
    			console.warn('心跳响应超时,执行断开');
    			this.state.ws.close();
    		}, this.config.HEARTBEAT_TIMEOUT);
    	}
    
    	/** 处理接收消息 */
    	handleIncomingMessage(event) {
    		try {
    			const data = JSON.parse(event.data);
    			if (data.type === 'ping') {
    				//关闭超时计时器
    				clearTimeout(this.state.heartbeatTimeoutTimer);
    				return;
    			}
    			this.onMessage?.(data);
    		} catch (error) {
    			console.error('消息解析失败:', error);
    			this.onError?.(error);
    		}
    	}
    
    	/** 重连控制 */
    	handleReconnect() {
    		if (this.shouldReconnect) {
    			const delay = this.calculateReconnectDelay();
    			console.log(`将在 ${delay}ms 后重试...`);
    
    			setTimeout(() => {
    				this.state.reconnectCount++;
    				this.connect();
    			}, delay);
    		}
    	}
    
    	/** 智能重连延迟计算 */
    	calculateReconnectDelay() {
    		return Math.min(
    			this.config.BASE_RECONNECT_DELAY * (2 ** this.state.reconnectCount),
    			this.config.MAX_RECONNECT_DELAY
    		);
    	}
    
    	/** 发送消息(带队列控制) */
    	send(data) {
    		if (this.isConnected) {
    			this.state.ws.send(data);
    		} else {
    			if (this.state.pendingMessages.length >= this.config.MAX_PENDING_MESSAGES) {
    				console.warn('消息队列已满,丢弃最旧消息');
    				this.state.pendingMessages.shift();
    			}
    			this.state.pendingMessages.push(data);
    		}
    	}
    
    	/** 清空心跳定时器 */
    	clearHeartbeat() {
    		clearInterval(this.state.heartbeatTimer);
    		clearTimeout(this.state.heartbeatTimeoutTimer);
    		this.state.heartbeatTimer = null;
    		this.state.heartbeatTimeoutTimer = null;
    	}
    
    	/** 重置重连状态 */
    	resetReconnect() {
    		this.state.reconnectCount = 0;
    		this.state.isUserClosed = false;
    	}
    
    	/** 处理连接关闭事件 */
    	handleClose(event) {
    		this.clearHeartbeat();
    		if (!this.state.isUserClosed) {
    			this.handleReconnect();
    		}
    	}
    
    	/** 发送队列中的消息 */
    	flushPendingMessages() {
    		while (this.state.pendingMessages.length > 0 && this.isConnected) {
    			this.state.ws.send(this.state.pendingMessages.shift());
    		}
    	}
    
    	// 公开方法
    	//关闭
    	close() {
    		this.state.isUserClosed = true;
    		this.dispose();
    	}
    	//重链接
    	reconnect() {
    		this.close();
    		this.state.isUserClosed = false;
    		this.connect();
    	}
    
    	// 状态判断
    	//判断socket是开启状态吗
    	get isConnected() {
    		return this.state.ws?.readyState === WebSocket.OPEN;
    	}
    	//判断可以重连吗
    	get shouldReconnect() {
    		return !this.state.isUserClosed &&
    			this.state.reconnectCount < this.config.MAX_RECONNECT_ATTEMPTS;
    	}
    }
    
    // 使用示例
    // const ws = new WebSocketClient('wss://echo.websocket.org');
    // ws.onMessage = (data) => console.log('收到消息:', data);
    // ws.send('Hello World');

    使用示例

    // 初始化客户端
    const client = new WebSocketClient('wss://api.example.com/ws');
    
    // 监听消息
    client.onMessage = (data) => {
      console.log('收到消息:', data);
    };
    
    // 发送消息
    document.getElementById('sendBtn').addEventListener('click', () => {
      client.send(JSON.stringify({ 
        type: 'chat', 
        content: 'Hello World' 
      }));
    });
    
    // 异常处理
    client.onError = (error) => {
      console.error('连接异常:', error);
    };