公司动态

前端 WebSocket 实时通信的架构设计:重连、心跳与消息可靠投递

📅 2026/7/16 19:36:44
前端 WebSocket 实时通信的架构设计:重连、心跳与消息可靠投递
前端 WebSocket 实时通信的架构设计重连、心跳与消息可靠投递一、实时通信的场景划分与技术选型前端实时通信的场景涵盖即时消息、协作编辑、数据推送、监控大屏等。不同场景对实时性、可靠性和带宽的要求差异显著。技术选型上WebSocket 是双向实时通信的标准协议适用于消息频率高 1次/秒、延迟敏感 100ms的场景。SSEServer-Sent Events适合服务端单向推送实现更简单但仅支持文本流。长轮询Long Polling是 WebSocket 不可用时的降级方案。本文聚焦 WebSocket 在前端侧的架构设计围绕三个核心问题展开连接断开后的重连策略、保持连接存活的心跳机制、消息丢失后的可靠投递。二、连接管理器生命周期与状态机WebSocket 连接的管理应从具体的业务逻辑中抽离封装为一个独立的状态机。这既避免了业务代码中散落if (ws.readyState WebSocket.OPEN)的写法也为重连、心跳等策略提供了统一的调度入口。/** WebSocket 连接状态枚举 */ enum ConnectionState { /** 初始未连接 */ IDLE idle, /** 正在连接 */ CONNECTING connecting, /** 已连接 */ CONNECTED connected, /** 正在重连 */ RECONNECTING reconnecting, /** 已关闭不可恢复 */ CLOSED closed, } /** 连接配置 */ interface ConnectionConfig { url: string; /** 心跳间隔毫秒 */ heartbeatInterval: number; /** 心跳超时毫秒超时视为断线 */ heartbeatTimeout: number; /** 最大重连次数 */ maxReconnectAttempts: number; /** 初始重连延迟毫秒 */ initialReconnectDelay: number; /** 重连延迟倍增因子 */ reconnectBackoffMultiplier: number; /** 最大重连延迟毫秒 */ maxReconnectDelay: number; /** 发送队列最大长度超出丢弃旧消息 */ maxSendQueueSize: number; } /** 默认配置 */ const defaultConfig: ConnectionConfig { url: , heartbeatInterval: 30_000, // 30秒 heartbeatTimeout: 10_000, // 10秒超时 maxReconnectAttempts: 10, initialReconnectDelay: 1_000, reconnectBackoffMultiplier: 1.5, maxReconnectDelay: 30_000, maxSendQueueSize: 500, }; class WebSocketManager { private ws: WebSocket | null null; private state: ConnectionState ConnectionState.IDLE; private config: ConnectionConfig; private reconnectAttempts 0; private heartbeatTimer: ReturnTypetypeof setInterval | null null; private heartbeatTimeoutTimer: ReturnTypetypeof setTimeout | null null; // 待发送消息队列保存未确认的消息用于重发 private pendingQueue: Array{ id: string; data: string; timestamp: number } []; // 消息确认映射已发送等待 ACK 的消息 private waitingAck: Mapstring, { data: string; retries: number } new Map(); // 事件回调 private listeners: Mapstring, SetFunction new Map(); constructor(config: PartialConnectionConfig {}) { this.config { ...defaultConfig, ...config }; } /** 建立连接 */ connect(url?: string): void { if (url) { this.config.url url; } if (!this.config.url) { throw new Error(WebSocket URL 未配置); } if (this.state ConnectionState.CONNECTED || this.state ConnectionState.CONNECTING) { console.warn([WS] 已有活跃连接跳过重复连接); return; } this.state ConnectionState.CONNECTING; this.emit(stateChange, this.state); try { this.ws new WebSocket(this.config.url); this.ws.onopen this.handleOpen.bind(this); this.ws.onmessage this.handleMessage.bind(this); this.ws.onclose this.handleClose.bind(this); this.ws.onerror this.handleError.bind(this); } catch (err) { console.error([WS] 创建连接失败:, (err as Error).message); this.scheduleReconnect(); } } private handleOpen(): void { this.state ConnectionState.CONNECTED; this.reconnectAttempts 0; this.startHeartbeat(); // 重发队列中的消息 this.flushPendingQueue(); this.emit(stateChange, this.state); this.emit(connected, null); } private handleMessage(event: MessageEvent): void { try { const message JSON.parse(event.data); // 处理 ACK 确认 if (message.type ack) { this.waitingAck.delete(message.messageId); return; } // 收到服务端消息立刻回复 ACK if (message.messageId) { this.sendAck(message.messageId); } this.emit(message, message); } catch (err) { console.warn([WS] 消息解析失败:, (err as Error).message); } } private handleClose(event: CloseEvent): void { this.stopHeartbeat(); // 非正常关闭1000 为正常关闭 if (event.code ! 1000) { console.warn([WS] 连接异常关闭, code${event.code}, reason${event.reason}); this.scheduleReconnect(); } else { this.state ConnectionState.CLOSED; this.emit(stateChange, this.state); } } private handleError(event: Event): void { console.error([WS] 连接错误:, event); // onerror 后通常会触发 onclose此处不做额外处理 } }三、重连策略指数退避与状态同步重连不是简单的「断开后重试」需要考虑退避策略避免服务端压力、重连后同步离线消息、以及重连次数上限。指数退避Exponential Backoff的核心公式为delay min(initial × multiplier^(attempt-1), max)。配合 jitter随机抖动可以避免多个客户端同时重连造成的「惊群效应」。/** 重连逻辑续 WebSocketManager */ private scheduleReconnect(): void { if (this.state ConnectionState.CLOSED) { return; // 已主动关闭不重连 } if (this.reconnectAttempts this.config.maxReconnectAttempts) { console.error( [WS] 已达最大重连次数(${this.config.maxReconnectAttempts})停止重连 ); this.state ConnectionState.CLOSED; this.emit(stateChange, this.state); this.emit(reconnectFailed, { attempts: this.reconnectAttempts, }); return; } this.state ConnectionState.RECONNECTING; this.emit(stateChange, this.state); // 指数退避delay min(initial * multiplier^attempt, max) const delay Math.min( this.config.initialReconnectDelay * Math.pow(this.config.reconnectBackoffMultiplier, this.reconnectAttempts), this.config.maxReconnectDelay ); // 添加随机抖动±20%避免惊群效应 const jitter delay * 0.2 * (Math.random() * 2 - 1); const actualDelay delay jitter; console.log( [WS] 第 ${this.reconnectAttempts 1}/${this.config.maxReconnectAttempts} 次重连, 等待 ${Math.round(actualDelay)}ms ); setTimeout(() { this.reconnectAttempts; this.connect(); }, actualDelay); } /** 强制重连用户手动触发或网络恢复时 */ forceReconnect(): void { this.reconnectAttempts 0; this.close(); this.connect(); }四、心跳机制与消息可靠投递心跳机制WebSocket 协议本身支持 ping/pong 帧但浏览器的 WebSocket API 不暴露 ping/pong 方法。因此在应用层实现心跳客户端定时发送 ping 消息服务端在收到后回复 pong。如果超过heartbeatTimeout未收到 pong认为连接已断开主动关闭并触发重连。/** 心跳机制 */ private startHeartbeat(): void { this.stopHeartbeat(); // 清除旧定时器 this.heartbeatTimer setInterval(() { if (this.ws?.readyState WebSocket.OPEN) { this.sendRaw(JSON.stringify({ type: ping })); // 设置 pong 超时检测 this.heartbeatTimeoutTimer setTimeout(() { console.warn([WS] 心跳超时主动断开); this.ws?.close(4001, Heartbeat timeout); }, this.config.heartbeatTimeout); } }, this.config.heartbeatInterval); } private stopHeartbeat(): void { if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer null; } if (this.heartbeatTimeoutTimer) { clearTimeout(this.heartbeatTimeoutTimer); this.heartbeatTimeoutTimer null; } }消息可靠投递WebSocket 是可靠的传输层协议基于 TCP但连接中断期间发送的消息会丢失。可靠投递需要应用层配合核心机制为消息序号 ACK 确认 重发。/** 消息序号生成器 */ let messageSeq 0; function nextMessageId(): string { return msg_${Date.now()}_${messageSeq}_${Math.random().toString(36).slice(2, 8)}; } /** 发送消息带可靠投递 */ async sendWithAck( data: Recordstring, unknown, ackTimeout: number 5_000, maxRetries: number 3 ): Promisevoid { return new Promise((resolve, reject) { if (this.state ! ConnectionState.CONNECTED) { // 未连接时加入队列连接恢复后重发 this.pendingQueue.push({ id: nextMessageId(), data: JSON.stringify(data), timestamp: Date.now(), }); // 维护队列最大长度 if (this.pendingQueue.length this.config.maxSendQueueSize) { this.pendingQueue.shift(); } reject(new Error(连接未建立消息已加入待发送队列)); return; } const messageId nextMessageId(); const message { ...data, messageId }; this.waitingAck.set(messageId, { data: JSON.stringify(message), retries: 0 }); const attempt () { const entry this.waitingAck.get(messageId); if (!entry) return; // 已收到 ACK if (entry.retries maxRetries) { this.waitingAck.delete(messageId); reject(new Error(消息 ${messageId} 超过最大重试次数)); return; } this.sendRaw(entry.data); entry.retries; setTimeout(() { if (this.waitingAck.has(messageId)) { attempt(); } }, ackTimeout); }; attempt(); // 设置 ACK 监听一次性 const onAck (msg: any) { if (msg.type ack msg.messageId messageId) { this.waitingAck.delete(messageId); this.off(message, onAck); resolve(); } }; this.on(message, onAck); }); } private sendRaw(data: string): void { if (this.ws?.readyState WebSocket.OPEN) { this.ws.send(data); } } /** 发送 ACK 确认 */ private sendAck(messageId: string): void { this.sendRaw(JSON.stringify({ type: ack, messageId })); } /** 连接恢复后清空待发送队列 */ private flushPendingQueue(): void { const queue [...this.pendingQueue]; this.pendingQueue []; for (const item of queue) { this.sendRaw(item.data); } }事件系统供业务层使用/** 事件监听与分发 */ on(event: string, callback: Function): void { if (!this.listeners.has(event)) { this.listeners.set(event, new Set()); } this.listeners.get(event)!.add(callback); } off(event: string, callback: Function): void { this.listeners.get(event)?.delete(callback); } private emit(event: string, data: unknown): void { this.listeners.get(event)?.forEach((cb) { try { cb(data); } catch (err) { console.error([WS] 事件 ${event} 回调执行异常:, err); } }); } /** 主动关闭连接 */ close(): void { this.stopHeartbeat(); this.state ConnectionState.CLOSED; this.ws?.close(1000, Client close); this.ws null; this.emit(stateChange, this.state); }五、总结WebSocket 前端架构的三个核心组件——重连、心跳、可靠投递——需要联动设计而非各自独立心跳检测到超时后应立即触发重连流程而非等待onclose事件某些网络中断不代表关闭。可靠投递依赖消息序号和 ACK 机制重连后需要重发未确认的消息但需设置最大重试避免消息风暴。重连策略的退避算法需要逐次递增间隔配合随机抖动降低服务端瞬时负载。工程实践中建议将 WebSocket 管理层封装为独立模块后通过事件系统暴露给业务层。业务代码只消费on(message, ...)和sendWithAck(...)两个接口连接状态的维护细节对业务透明。