一、核心概念与必要性
为什么需要心跳检测和重连?
-
网络不稳定:移动网络、Wi-Fi切换、代理服务器可能导致连接断开
-
服务器限制:Nginx/负载均衡器默认30-60秒超时
-
浏览器限制:部分浏览器标签页休眠时暂停WebSocket
-
资源清理:服务器需要清理僵尸连接
二、完整实现方案
1. WebSocket管理器类(TypeScript实现)
typescript
复制
下载
interface WebSocketConfig {
url: string;
protocols?: string | string[];
reconnectInterval?: number; // 重连间隔(ms)
maxReconnectAttempts?: number; // 最大重连次数
heartbeatInterval?: number; // 心跳间隔(ms)
heartbeatTimeout?: number; // 心跳超时(ms)
}
enum WebSocketState {
CONNECTING = 0,
OPEN = 1,
CLOSING = 2,
CLOSED = 3
}
class WebSocketManager {
private ws: WebSocket | null = null;
private config: Required<WebSocketConfig>;
private reconnectAttempts = 0;
private heartbeatTimer: NodeJS.Timeout | null = null;
private heartbeatTimeoutTimer: NodeJS.Timeout | null = null;
private reconnectTimer: NodeJS.Timeout | null = null;
private isManualClose = false; // 是否手动关闭
// 事件监听器
private listeners = {
open: [] as ((event: Event) => void)[],
message: [] as ((data: any) => void)[],
close: [] as ((event: CloseEvent) => void)[],
error: [] as ((event: Event) => void)[],
reconnect: [] as ((attempt: number) => void)[],
};
constructor(config: WebSocketConfig) {
this.config = {
url: config.url,
protocols: config.protocols || [],
reconnectInterval: config.reconnectInterval || 3000,
maxReconnectAttempts: config.maxReconnectAttempts || 5,
heartbeatInterval: config.heartbeatInterval || 30000,
heartbeatTimeout: config.heartbeatTimeout || 10000,
};
}
/**
* 连接WebSocket
*/
public connect(): void {
if (this.ws?.readyState === WebSocketState.OPEN ||
this.ws?.readyState === WebSocketState.CONNECTING) {
console.warn('WebSocket is already connecting or connected');
return;
}
this.isManualClose = false;
try {
this.ws = new WebSocket(this.config.url, this.config.protocols);
this.setupEventListeners();
} catch (error) {
console.error('WebSocket creation error:', error);
this.handleReconnect();
}
}
/**
* 设置事件监听器
*/
private setupEventListeners(): void {
if (!this.ws) return;
this.ws.onopen = (event: Event) => {
console.log('WebSocket connected');
this.reconnectAttempts = 0;
this.startHeartbeat();
this.emit('open', event);
};
this.ws.onmessage = (event: MessageEvent) => {
try {
const data = JSON.parse(event.data);
// 处理心跳响应
if (data.type === 'pong' || data.type === 'heartbeat') {
this.resetHeartbeat();
return;
}
this.emit('message', data);
} catch (error) {
// 非JSON数据直接传递
this.emit('message', event.data);
}
};
this.ws.onclose = (event: CloseEvent) => {
console.log(`WebSocket closed: ${event.code} ${event.reason}`);
this.cleanupHeartbeat();
// 非手动关闭才重连
if (!this.isManualClose) {
this.handleReconnect();
}
this.emit('close', event);
};
this.ws.onerror = (event: Event) => {
console.error('WebSocket error:', event);
this.emit('error', event);
// 错误时也尝试重连
if (!this.isManualClose) {
this.handleReconnect();
}
};
}
/**
* 开始心跳检测
*/
private startHeartbeat(): void {
this.cleanupHeartbeat();
this.heartbeatTimer = setInterval(() => {
if (this.ws?.readyState === WebSocketState.OPEN) {
this.sendHeartbeat();
this.startHeartbeatTimeout();
}
}, this.config.heartbeatInterval);
}
/**
* 发送心跳包
*/
private sendHeartbeat(): void {
if (this.ws?.readyState === WebSocketState.OPEN) {
const heartbeatMsg = JSON.stringify({
type: 'ping',
timestamp: Date.now(),
});
this.ws.send(heartbeatMsg);
}
}
/**
* 开始心跳超时检测
*/
private startHeartbeatTimeout(): void {
this.cleanupHeartbeatTimeout();
this.heartbeatTimeoutTimer = setTimeout(() => {
console.warn('Heartbeat timeout, reconnecting...');
this.close();
this.handleReconnect();
}, this.config.heartbeatTimeout);
}
/**
* 重置心跳(收到响应时调用)
*/
private resetHeartbeat(): void {
this.cleanupHeartbeatTimeout();
}
/**
* 清理心跳定时器
*/
private cleanupHeartbeat(): void {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
this.cleanupHeartbeatTimeout();
}
/**
* 清理心跳超时定时器
*/
private cleanupHeartbeatTimeout(): void {
if (this.heartbeatTimeoutTimer) {
clearTimeout(this.heartbeatTimeoutTimer);
this.heartbeatTimeoutTimer = null;
}
}
/**
* 处理重连逻辑
*/
private handleReconnect(): void {
this.cleanupReconnectTimer();
// 达到最大重连次数
if (this.reconnectAttempts >= this.config.maxReconnectAttempts) {
console.error('Max reconnection attempts reached');
return;
}
this.reconnectAttempts++;
console.log(`Reconnecting... Attempt ${this.reconnectAttempts}/${this.config.maxReconnectAttempts}`);
this.emit('reconnect', this.reconnectAttempts);
// 指数退避算法:重连间隔逐渐增加
const delay = Math.min(
this.config.reconnectInterval * Math.pow(1.5, this.reconnectAttempts - 1),
30000 // 最大30秒
);
this.reconnectTimer = setTimeout(() => {
this.connect();
}, delay);
}
/**
* 清理重连定时器
*/
private cleanupReconnectTimer(): void {
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
}
/**
* 发送消息
*/
public send(data: any): boolean {
if (this.ws?.readyState !== WebSocketState.OPEN) {
console.error('WebSocket is not connected');
return false;
}
try {
const message = typeof data === 'string' ? data : JSON.stringify(data);
this.ws.send(message);
return true;
} catch (error) {
console.error('Send message error:', error);
return false;
}
}
/**
* 手动关闭连接
*/
public close(code?: number, reason?: string): void {
this.isManualClose = true;
this.cleanupHeartbeat();
this.cleanupReconnectTimer();
if (this.ws) {
this.ws.close(code || 1000, reason);
this.ws = null;
}
}
/**
* 获取连接状态
*/
public getState(): WebSocketState {
return this.ws?.readyState || WebSocketState.CLOSED;
}
/**
* 添加事件监听
*/
public on<T extends keyof typeof this.listeners>(
event: T,
callback: typeof this.listeners[T][number]
): void {
this.listeners[event].push(callback as any);
}
/**
* 移除事件监听
*/
public off<T extends keyof typeof this.listeners>(
event: T,
callback: typeof this.listeners[T][number]
): void {
const index = this.listeners[event].indexOf(callback as any);
if (index > -1) {
this.listeners[event].splice(index, 1);
}
}
/**
* 触发事件
*/
private emit<T extends keyof typeof this.listeners>(
event: T,
...args: Parameters<typeof this.listeners[T][number]>
): void {
this.listeners[event].forEach(callback => {
try {
(callback as any)(...args);
} catch (error) {
console.error(`Error in ${event} event handler:`, error);
}
});
}
/**
* 销毁实例
*/
public destroy(): void {
this.close();
this.listeners = {
open: [],
message: [],
close: [],
error: [],
reconnect: [],
};
}
}
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc
需要全套面试笔记及答案
【点击此处即可/免费获取】
2. React Hook封装(React + TypeScript)
typescript
复制
下载
import { useEffect, useRef, useCallback, useState } from 'react';
interface UseWebSocketOptions {
onMessage?: (data: any) => void;
onOpen?: (event: Event) => void;
onClose?: (event: CloseEvent) => void;
onError?: (event: Event) => void;
onReconnect?: (attempt: number) => void;
heartbeatInterval?: number;
reconnectInterval?: number;
}
export const useWebSocket = (
url: string,
options: UseWebSocketOptions = {}
) => {
const wsManager = useRef<WebSocketManager | null>(null);
const [isConnected, setIsConnected] = useState(false);
const [reconnectCount, setReconnectCount] = useState(0);
const connect = useCallback(() => {
if (wsManager.current) {
wsManager.current.destroy();
}
wsManager.current = new WebSocketManager({
url,
heartbeatInterval: options.heartbeatInterval,
reconnectInterval: options.reconnectInterval,
});
wsManager.current.on('open', (event) => {
setIsConnected(true);
setReconnectCount(0);
options.onOpen?.(event);
});
wsManager.current.on('message', (data) => {
options.onMessage?.(data);
});
wsManager.current.on('close', (event) => {
setIsConnected(false);
options.onClose?.(event);
});
wsManager.current.on('error', (event) => {
options.onError?.(event);
});
wsManager.current.on('reconnect', (attempt) => {
setReconnectCount(attempt);
options.onReconnect?.(attempt);
});
wsManager.current.connect();
}, [url, options]);
const disconnect = useCallback(() => {
wsManager.current?.close();
setIsConnected(false);
}, []);
const send = useCallback((data: any) => {
return wsManager.current?.send(data) || false;
}, []);
// 自动连接和清理
useEffect(() => {
connect();
return () => {
wsManager.current?.destroy();
};
}, [connect]);
// 网络状态变化监听
useEffect(() => {
const handleOnline = () => {
console.log('Network online, reconnecting...');
if (!isConnected) {
connect();
}
};
const handleOffline = () => {
console.log('Network offline');
disconnect();
};
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, [connect, disconnect, isConnected]);
return {
isConnected,
reconnectCount,
send,
connect,
disconnect,
};
};
// 使用示例
export const ChatComponent: React.FC = () => {
const [messages, setMessages] = useState<string[]>([]);
const { isConnected, send } = useWebSocket('wss://api.example.com/ws', {
onMessage: (data) => {
setMessages(prev => [...prev, data.content]);
},
onReconnect: (attempt) => {
console.log(`Reconnection attempt ${attempt}`);
},
});
const handleSend = () => {
send({ type: 'chat', content: 'Hello' });
};
return (
<div>
<div>Status: {isConnected ? 'Connected' : 'Disconnected'}</div>
<button onClick={handleSend} disabled={!isConnected}>
Send Message
</button>
<div>
{messages.map((msg, index) => (
<div key={index}>{msg}</div>
))}
</div>
</div>
);
};
3. 服务器端心跳处理(Node.js)
javascript
复制
下载
const WebSocket = require('ws');
class WebSocketServerWithHeartbeat {
constructor(server) {
this.wss = new WebSocket.Server({ server });
this.clients = new Map(); // clientId -> {ws, lastPing, isAlive}
this.heartbeatInterval = 30000;
this.maxMissedPings = 3;
this.setup();
}
setup() {
this.wss.on('connection', (ws, request) => {
const clientId = this.generateClientId();
console.log(`New connection: ${clientId}`);
this.clients.set(clientId, {
ws,
lastPing: Date.now(),
isAlive: true,
});
// 发送欢迎消息
ws.send(JSON.stringify({
type: 'welcome',
clientId,
timestamp: Date.now(),
}));
// 消息处理
ws.on('message', (data) => {
try {
const message = JSON.parse(data);
this.handleMessage(clientId, message);
} catch (error) {
console.error('Message parse error:', error);
}
});
// 心跳响应
ws.on('pong', () => {
const client = this.clients.get(clientId);
if (client) {
client.isAlive = true;
client.lastPing = Date.now();
}
});
// 连接关闭
ws.on('close', () => {
console.log(`Connection closed: ${clientId}`);
this.clients.delete(clientId);
});
// 错误处理
ws.on('error', (error) => {
console.error(`WebSocket error for ${clientId}:`, error);
this.clients.delete(clientId);
});
});
// 定期心跳检测
setInterval(() => {
this.checkHeartbeats();
}, this.heartbeatInterval);
}
handleMessage(clientId, message) {
const client = this.clients.get(clientId);
if (!client) return;
switch (message.type) {
case 'ping':
// 心跳响应
client.ws.send(JSON.stringify({
type: 'pong',
timestamp: Date.now(),
}));
client.isAlive = true;
client.lastPing = Date.now();
break;
case 'chat':
// 广播消息
this.broadcast({
type: 'chat',
clientId,
content: message.content,
timestamp: Date.now(),
});
break;
default:
console.log(`Unknown message type: ${message.type}`);
}
}
checkHeartbeats() {
const now = Date.now();
for (const [clientId, client] of this.clients.entries()) {
const timeSinceLastPing = now - client.lastPing;
// 超过最大时间没有心跳
if (timeSinceLastPing > this.heartbeatInterval * this.maxMissedPings) {
console.log(`Client ${clientId} heartbeat timeout, closing connection`);
client.ws.terminate();
this.clients.delete(clientId);
continue;
}
// 发送心跳检测
if (!client.isAlive) {
console.log(`Client ${clientId} not responding to pings`);
client.ws.terminate();
this.clients.delete(clientId);
continue;
}
// 标记为需要检测,发送ping
client.isAlive = false;
try {
client.ws.ping();
} catch (error) {
console.error(`Ping error for ${clientId}:`, error);
this.clients.delete(clientId);
}
}
}
broadcast(message) {
const data = JSON.stringify(message);
this.wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(data);
}
});
}
sendToClient(clientId, message) {
const client = this.clients.get(clientId);
if (client && client.ws.readyState === WebSocket.OPEN) {
client.ws.send(JSON.stringify(message));
}
}
generateClientId() {
return `client_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
getConnectedCount() {
return this.clients.size;
}
}
// 使用示例
const http = require('http');
const server = http.createServer();
const wssWithHeartbeat = new WebSocketServerWithHeartbeat(server);
server.listen(8080, () => {
console.log('WebSocket server with heartbeat listening on port 8080');
});
4. 高级特性:连接质量监控
typescript
复制
下载
interface ConnectionMetrics {
latency: number; // 延迟(ms)
jitter: number; // 抖动(ms)
packetLoss: number; // 丢包率(%)
bandwidth: number; // 带宽(kbps)
uptime: number; // 连接时长(ms)
}
class ConnectionMonitor {
private metrics: ConnectionMetrics = {
latency: 0,
jitter: 0,
packetLoss: 0,
bandwidth: 0,
uptime: 0,
};
private latencySamples: number[] = [];
private startTime: number = Date.now();
private sentPackets = 0;
private receivedPackets = 0;
recordLatency(latency: number) {
this.latencySamples.push(latency);
// 保留最近100个样本
if (this.latencySamples.length > 100) {
this.latencySamples.shift();
}
// 计算平均延迟
this.metrics.latency = this.latencySamples.reduce((a, b) => a + b, 0) /
this.latencySamples.length;
// 计算抖动(延迟的标准差)
const variance = this.latencySamples.reduce((sum, sample) => {
return sum + Math.pow(sample - this.metrics.latency, 2);
}, 0) / this.latencySamples.length;
this.metrics.jitter = Math.sqrt(variance);
}
recordPacket(sent: boolean) {
if (sent) {
this.sentPackets++;
} else {
this.receivedPackets++;
}
if (this.sentPackets > 0) {
this.metrics.packetLoss = ((this.sentPackets - this.receivedPackets) /
this.sentPackets) * 100;
}
}
updateUptime() {
this.metrics.uptime = Date.now() - this.startTime;
}
getMetrics(): ConnectionMetrics {
this.updateUptime();
return { ...this.metrics };
}
reset() {
this.startTime = Date.now();
this.sentPackets = 0;
this.receivedPackets = 0;
this.latencySamples = [];
this.metrics = {
latency: 0,
jitter: 0,
packetLoss: 0,
bandwidth: 0,
uptime: 0,
};
}
}
// 集成到WebSocketManager
class EnhancedWebSocketManager extends WebSocketManager {
private connectionMonitor = new ConnectionMonitor();
private metricsTimer: NodeJS.Timeout | null = null;
// 重写发送心跳方法
protected sendHeartbeat(): void {
if (this.ws?.readyState === WebSocketState.OPEN) {
const sendTime = Date.now();
const heartbeatMsg = JSON.stringify({
type: 'ping',
timestamp: sendTime,
});
this.connectionMonitor.recordPacket(true);
this.ws.send(heartbeatMsg);
this.startHeartbeatTimeout();
}
}
// 处理心跳响应时记录延迟
protected resetHeartbeat(): void {
super.resetHeartbeat();
// 假设在消息处理中记录了接收时间
const receiveTime = Date.now();
// 这里需要存储发送时间来计算延迟
}
// 开始监控
startMetricsCollection() {
this.metricsTimer = setInterval(() => {
const metrics = this.connectionMonitor.getMetrics();
console.log('Connection metrics:', metrics);
// 根据指标调整策略
if (metrics.packetLoss > 20) {
console.warn('High packet loss, consider reducing message frequency');
}
if (metrics.latency > 1000) {
console.warn('High latency, adjusting heartbeat interval');
}
}, 5000);
}
stopMetricsCollection() {
if (this.metricsTimer) {
clearInterval(this.metricsTimer);
this.metricsTimer = null;
}
}
}
三、最佳实践与优化建议
1. 心跳参数配置建议
javascript
复制
下载
// 不同场景下的推荐配置
const configs = {
// 实时聊天应用
CHAT: {
heartbeatInterval: 25000, // 25秒
heartbeatTimeout: 10000, // 10秒
reconnectInterval: 2000, // 2秒
maxReconnectAttempts: 10, // 最多重连10次
},
// 实时数据监控
MONITORING: {
heartbeatInterval: 15000, // 15秒
heartbeatTimeout: 5000, // 5秒
reconnectInterval: 1000, // 1秒
maxReconnectAttempts: 20, // 最多重连20次
},
// 游戏应用
GAMING: {
heartbeatInterval: 10000, // 10秒
heartbeatTimeout: 3000, // 3秒
reconnectInterval: 500, // 0.5秒
maxReconnectAttempts: 5, // 最多重连5次
},
};
2. 重连策略优化
typescript
复制
下载
// 智能重连策略
class SmartReconnectStrategy {
private failures = 0;
private lastFailureTime = 0;
shouldReconnect(): boolean {
const now = Date.now();
// 短时间内连续失败,可能是网络问题
if (now - this.lastFailureTime < 10000 && this.failures > 3) {
console.log('Too many failures in short time, waiting...');
return false;
}
// 失败次数太多,可能是服务器问题
if (this.failures > 10) {
console.log('Too many failures, giving up');
return false;
}
return true;
}
recordFailure() {
this.failures++;
this.lastFailureTime = Date.now();
// 逐渐重置失败计数
setTimeout(() => {
if (this.failures > 0) {
this.failures--;
}
}, 60000); // 每分钟减少一次失败计数
}
recordSuccess() {
this.failures = 0;
}
}
3. 离线消息队列
typescript
复制
下载
class OfflineMessageQueue {
private queue: Array<{data: any, timestamp: number}> = [];
private maxQueueSize = 100;
private maxAge = 5 * 60 * 1000; // 5分钟
addMessage(data: any) {
// 清理过期消息
this.cleanup();
// 队列满时移除最旧的消息
if (this.queue.length >= this.maxQueueSize) {
this.queue.shift();
}
this.queue.push({
data,
timestamp: Date.now(),
});
}
getMessages(): any[] {
this.cleanup();
return this.queue.map(item => item.data);
}
clear() {
this.queue = [];
}
private cleanup() {
const now = Date.now();
this.queue = this.queue.filter(item =>
now - item.timestamp <= this.maxAge
);
}
}
// 在WebSocketManager中使用
class WebSocketManagerWithQueue extends WebSocketManager {
private offlineQueue = new OfflineMessageQueue();
send(data: any): boolean {
const success = super.send(data);
if (!success) {
// 发送失败,存储到离线队列
this.offlineQueue.addMessage(data);
return false;
}
return true;
}
protected onOpen(event: Event): void {
super.onOpen(event);
// 连接成功后发送离线消息
const offlineMessages = this.offlineQueue.getMessages();
offlineMessages.forEach(message => {
super.send(message);
});
this.offlineQueue.clear();
}
}
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc
需要全套面试笔记及答案
【点击此处即可/免费获取】
四、常见问题与解决方案
问题1:心跳包增加了流量开销
解决方案:
-
使用二进制ping/pong帧(1字节)
-
动态调整心跳频率(根据网络质量)
-
允许客户端协商心跳间隔
问题2:重连风暴导致服务器压力
解决方案:
-
使用随机延迟避免同时重连
-
实现退避算法(exponential backoff)
-
服务器端限制重连频率
问题3:移动端网络切换
解决方案:
-
监听网络状态变化事件
-
网络恢复后延迟重连
-
使用短连接心跳确认网络质量
问题4:多标签页重复连接
解决方案:
typescript
复制
下载
// 使用BroadcastChannel或SharedWorker协调多标签页
class MultiTabWebSocketManager {
private channel: BroadcastChannel;
constructor() {
this.channel = new BroadcastChannel('websocket_control');
this.channel.addEventListener('message', (event) => {
if (event.data.type === 'tab_connected') {
// 其他标签页已连接,本标签页保持只读
this.setReadonlyMode();
}
});
// 通知其他标签页
this.channel.postMessage({ type: 'tab_connected' });
}
}
五、测试方案
typescript
复制
下载
// 模拟网络不稳定的测试工具
class NetworkSimulator {
static async testWebSocket(wsManager: WebSocketManager) {
// 测试正常连接
console.log('Test 1: Normal connection');
wsManager.connect();
await this.delay(2000);
// 模拟网络中断
console.log('Test 2: Simulating network loss');
this.simulateNetworkLoss();
await this.delay(5000);
// 恢复网络
console.log('Test 3: Network recovery');
this.restoreNetwork();
await this.delay(10000);
// 模拟服务器重启
console.log('Test 4: Simulating server restart');
wsManager.close(1001, 'Server restart');
await this.delay(3000);
// 验证自动重连
console.log('Test 5: Verifying auto-reconnect');
await this.delay(15000);
}
private static delay(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
private static simulateNetworkLoss() {
// 实际项目中可能需要修改网络配置
console.log('Network loss simulated');
}
private static restoreNetwork() {
console.log('Network restored');
}
}
六、总结
关键要点:
-
心跳检测:防止连接被中间设备断开,及时检测连接状态
-
自动重连:提供无缝的用户体验,减少手动干预
-
智能策略:根据网络质量动态调整参数
-
资源管理:及时清理无效连接和定时器
-
错误处理:优雅处理各种网络异常情况
推荐库:
-
客户端:
reconnecting-websocket、@gamestdio/websocket -
服务器端:
ws(Node.js)、gorilla/websocket(Go)、Spring WebSocket(Java)
通过以上实现,可以构建一个健壮、可靠的WebSocket连接,能够应对各种网络异常情况,提供良好的用户体验。