欢迎光临
我们一直在努力

前端 WebSocket 心跳重连与离线消息补偿机制:保障弱网高可用

前端 WebSocket 心跳重连与离线消息补偿机制:保障弱网高可用

封面信息图

在大型在线协作文档、IM 即时通讯或金融实时行情等高并发前端应用中,WebSocket 长连接是维系服务端与客户端双向实时通信的生命线。

然而,在移动端真实网络环境下,用户经常面临各种恶劣的“弱网与假死连接”:

  • 手机进入电梯或地下车库,底层 4G/5G 信号瞬间中断;
  • 手机合盖锁屏后,操作系统为了省电强制挂起浏览器的后台网络 Socket,但并没有触发 TCP FIN 挥手(形成幽灵假死连接);
  • 当用户走出电梯网络重新恢复时,前端在断网期间错过了服务端下发的十几条关键状态推送,导致界面数据与服务端严重脱节。

一套真正高可用的前端 WebSocket 架构,绝不能仅仅调用一个裸 new WebSocket()。

本文拆解一套在线上高并发场景验证过的完整长连接防御体系:双向心跳自愈(Dual Heartbeat)、指数退避与随机扰动重连(Exponential Backoff with Jitter)、以及基于递增序号(SeqID)的离线消息滑动补偿机制。

高可用 WebSocket 弹性生命周期拓扑

[WebSocket 连接建立成功 (状态: CONNECTED)]
│
▼
【机制 1: 双向自适应心跳检测 (双重超时保险)】
– 客户端每 15 秒发送 Ping ──> 期望服务端 5 秒内回 Pong
– 若连续 2 次未收到 Pong ──> 判定为假死连接, 主动 destroy 并触发重连
│
▼ (遭遇断网或心跳失联)
【机制 2: 带随机抖动的指数退避重连 (Exponential Backoff + Jitter)】
– 重连间隔: 1s -> 2s -> 4s -> 8s -> max 15s (+ 0~500ms 随机扰动)
– 彻底杜绝数万客户端在网关恢复时同时发起重连风暴 (Thundering Herd)
│
▼ (连接重新握手成功)
【机制 3: 离线消息拉取与序号对齐 (Offline Message Sync)】
– 握手 Payload 附带客户端本地维护的 `last_seq_id: 1042`
– 服务端直接从 Redis 中补发 1043 ~ 1050 期间的未读增量事件

核心实现:生产级 RobustWebSocket 状态机

export interface WsMessagePayload {
seqId: number; // 全局单调递增消息序号
type: string;
data: any;
timestamp: number;
}

export interface RobustWebSocketConfig {
url: string;
heartbeatIntervalMs?: number; // 默认 15000ms
heartbeatTimeoutMs?: number; // 默认 5000ms
maxReconnectDelayMs?: number; // 最大重连间隔 15000ms
onMessage: (msg: WsMessagePayload) => void;
onStatusChange?: (status: 'CONNECTING' | 'OPEN' | 'RECONNECTING' | 'CLOSED') => void;
}

export class RobustWebSocket {
private ws: WebSocket | null = null;
private config: RobustWebSocketConfig;
private isExplicitlyClosed = false;
private reconnectAttempts = 0;

// 心跳定时器
private pingTimer: NodeJS.Timeout | null = null;
private pongTimeoutTimer: NodeJS.Timeout | null = null;

// 消息同步游标
private lastReceivedSeqId = 0;

constructor(config: RobustWebSocketConfig) {
this.config = {
heartbeatIntervalMs: 15000,
heartbeatTimeoutMs: 5000,
maxReconnectDelayMs: 15000,
…config,
};
// 从本地缓存恢复历史最后收到的序号
const cachedSeq = localStorage.getItem(`ws_last_seq_${config.url}`);
if (cachedSeq) {
this.lastReceivedSeqId = parseInt(cachedSeq, 10);
}
this.connect();
}

private connect() {
if (this.isExplicitlyClosed) return;

this.config.onStatusChange?.(this.reconnectAttempts === 0 ? 'CONNECTING' : 'RECONNECTING');

// 握手 URL 携带本地当前最后的 SeqID,服务端握手成功后自动补发离线消息
const connectUrl = `${this.config.url}?last_seq_id=${this.lastReceivedSeqId}`;
this.ws = new WebSocket(connectUrl);

this.ws.onopen = () => {
console.log('⚡ WebSocket 连接已建立,重置重连计数并启动心跳');
this.reconnectAttempts = 0;
this.config.onStatusChange?.('OPEN');
this.startHeartbeat();
};

this.ws.onmessage = (event) => {
this.resetHeartbeatWatchdog();

try {
const payload: WsMessagePayload = JSON.parse(event.data);

// 收到服务端心跳响应
if (payload.type === 'PONG') {
return;
}

// 序号去重与滑动游标推进
if (payload.seqId > this.lastReceivedSeqId) {
this.lastReceivedSeqId = payload.seqId;
localStorage.setItem(`ws_last_seq_${this.config.url}`, String(payload.seqId));
this.config.onMessage(payload);
}
} catch (err) {
console.error('[WS] 消息解析失败:', err);
}
};

this.ws.onclose = () => {
this.cleanupTimers();
if (!this.isExplicitlyClosed) {
this.scheduleReconnect();
} else {
this.config.onStatusChange?.('CLOSED');
}
};

this.ws.onerror = (err) => {
console.warn('[WS] 连接异常,关闭并触发重连', err);
this.ws?.close();
};
}

// 指数退避 + 随机打散重连算法
private scheduleReconnect() {
this.reconnectAttempts++;

// 指数增长: 1s, 2s, 4s, 8s, 15s…
const baseDelay = Math.min(
this.config.maxReconnectDelayMs!,
1000 * Math.pow(2, this.reconnectAttempts – 1)
);
// 注入 0~500ms 随机 Jitter,打散客户端瞬时重连洪峰
const jitter = Math.floor(Math.random() * 500);
const delay = baseDelay + jitter;

console.log(`⏳ 将在 ${delay}ms 后尝试第 ${this.reconnectAttempts} 次重连…`);
setTimeout(() => this.connect(), delay);
}

// 心跳保活与假死看门狗
private startHeartbeat() {
this.cleanupTimers();
this.pingTimer = setInterval(() => {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({ type: 'PING', timestamp: Date.now() }));

// 开启 Pong 响应倒计时看门狗
this.pongTimeoutTimer = setTimeout(() => {
console.warn('🚨 心跳超时未收到 PONG,判定连接假死,强制断开!');
this.ws?.close();
}, this.config.heartbeatTimeoutMs);
}
}, this.config.heartbeatIntervalMs);
}

private resetHeartbeatWatchdog() {
if (this.pongTimeoutTimer) {
clearTimeout(this.pongTimeoutTimer);
this.pongTimeoutTimer = null;
}
}

private cleanupTimers() {
if (this.pingTimer) clearInterval(this.pingTimer);
if (this.pongTimeoutTimer) clearTimeout(this.pongTimeoutTimer);
}

public close() {
this.isExplicitlyClosed = true;
this.cleanupTimers();
this.ws?.close();
}
}

落地成效

  • 彻底攻克假死幽灵连接:通过 5 秒 Pong 超时看门狗,客户端在电梯弱网断线后能在 20 秒内主动感知并自愈重连,消除了用户的长时间卡死。
  • 离线消息 100% 不丢失:基于单调递增 SeqID 握手对齐,断网期间产生的任何变更在重连后以增量包毫秒级回填,彻底杜绝数据版本断层。
  • 消除服务端雷暴重连冲击:随机 Jitter 机制将数万并发断线用户的重连请求平滑分散到 15 秒的时间窗口内,保护后端 WebSocket 集群不会在恢复瞬间被再次打崩。
  • 赞(0)
    未经允许不得转载:171主机测评 » 前端 WebSocket 心跳重连与离线消息补偿机制:保障弱网高可用
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址