欢迎光临
我们一直在努力

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

前端 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(…) 两个接口,连接状态的维护细节对业务透明。

赞(0)
未经允许不得转载:171主机测评 » 前端 WebSocket 实时通信的架构设计:重连、心跳与消息可靠投递
分享到: 更多 (0)

评论 抢沙发

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