前端 WebSocket 实时通信的架构设计:重连、心跳与消息可靠投递
一、实时通信的场景划分与技术选型
前端实时通信的场景涵盖即时消息、协作编辑、数据推送、监控大屏等。不同场景对实时性、可靠性和带宽的要求差异显著。
技术选型上,WebSocket 是双向实时通信的标准协议,适用于消息频率高(> 1次/秒)、延迟敏感(< 100ms)的场景。SSE(Server-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: ReturnType<typeof setInterval> | null = null; private heartbeatTimeoutTimer: ReturnType<typeof setTimeout> | null = null; // 待发送消息队列:保存未确认的消息用于重发 private pendingQueue: Array<{ id: string; data: string; timestamp: number }> = []; // 消息确认映射:已发送等待 ACK 的消息 private waitingAck: Map<string, { data: string; retries: number }> = new Map(); // 事件回调 private listeners: Map<string, Set<Function>> = new Map(); constructor(config: Partial<ConnectionConfig> = {}) { 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: Record<string, unknown>, ackTimeout: number = 5_000, maxRetries: number = 3 ): Promise<void> { 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(...)两个接口,连接状态的维护细节对业务透明。