关于WebSocket的基础概念在上一篇文章中已经详细说明,本文不再赘述。接下来我们将重点实现一个具备心跳检测和断线重连功能的WebSocket封装类。
TypeScript
/**
* WebSocket客户端封装类
* 特性:心跳检测、断线重连、事件回调、可配置化、连接状态管理
*/
interface WebSocketClientConfig {
url: string;
// 初始重连间隔(ms),指数退避会基于此值递增
reconnectInterval?: number;
// 最大重连次数
maxReconnectAttempts?: number;
// 心跳发送间隔(ms)
heartbeatInterval?: number;
// 心跳超时时间(ms),超过该时间未收到心跳响应则判定连接异常
heartbeatTimeout?: number;
// 日志输出开关
enableLog?: boolean;
}
// 默认配置
const DEFAULT_CONFIG: Required<Omit<WebSocketClientConfig, 'url'>> = {
reconnectInterval: 5000,
maxReconnectAttempts: 5,
heartbeatInterval: 5000,
heartbeatTimeout: 10000,
enableLog: true
};
class WebSocketClient {
private readonly config: Required<WebSocketClientConfig>;
private socket: WebSocket | null = null;
private reconnectAttempts: number = 0;
private heartbeatTimer: number | null = null;
private heartbeatTimeoutTimer: number | null = null;
private stopWs: boolean = false;
private callbacks = {
onOpen: [] as (() => void)[],
onMessage: [] as ((data: string) => void)[],
onClose: [] as ((code?: number, reason?: string) => void)[],
onError: [] as ((error: Event) => void)[]
};
constructor(config: WebSocketClientConfig) {
this.config = { ...DEFAULT_CONFIG, ...config };
this.connect();
}
private log(message: string, type: 'log' | 'warn' | 'error' = 'log'): void {
if (this.config.enableLog) {
console[type](`[WebSocketClient] ${message}`);
}
}
public connect(): void {
if (this.socket?.readyState === WebSocket.OPEN) {
this.log('连接已建立,无需重复初始化');
return;
}
if (this.reconnectAttempts === 0) {
this.log(`初始化连接WebSocket: ${this.config.url}`);
}
this.cleanup();
try {
this.socket = new WebSocket(this.config.url);
// 连接成功回调
this.socket.onopen = (event) => {
this.stopWs = false;
this.reconnectAttempts = 0;
this.startHeartbeat();
this.log('连接成功');
this.callbacks.onOpen.forEach(cb => cb());
};
this.socket.onmessage = (event) => {
const data = event.data.toString();
this.log(`收到数据: ${data}`);
if (data === 'pong' || (data.startsWith('{') && JSON.parse(data).type === 'heartbeat')) {
this.resetHeartbeatTimeout();
return;
}
this.callbacks.onMessage.forEach(cb => cb(data));
};
this.socket.onclose = (event) => {
this.log(`连接关闭 [code: ${event.code}, reason: ${event.reason}]`, 'warn');
this.cleanup();
this.callbacks.onClose.forEach(cb => cb(event.code, event.reason));
if (!this.stopWs) {
this.reconnect();
}
};
this.socket.onerror = (error) => {
this.log(`连接错误: ${error}`, 'error');
this.callbacks.onError.forEach(cb => cb(error));
};
} catch (error) {
this.log(`连接初始化失败: ${error}`, 'error');
this.reconnect();
}
}
public send(message: string | Record<string, any>): void {
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
this.log('连接未建立,无法发送消息', 'warn');
return;
}
try {
const sendData = typeof message === 'object'
? JSON.stringify(message)
: message;
this.socket.send(sendData);
this.log(`发送数据: ${sendData}`);
} catch (error) {
this.log(`发送消息失败: ${error}`, 'error');
}
}
public disconnect(code: number = 1000, reason: string = '主动关闭连接'): void {
this.stopWs = true;
this.cleanup();
if (this.socket) {
this.socket.close(code, reason);
this.socket = null;
}
this.log(`主动断开连接 [code: ${code}, reason: ${reason}]`);
}
private reconnect(): void {
if (this.reconnectAttempts >= this.config.maxReconnectAttempts) {
this.log(`重连失败:已达到最大重连次数(${this.config.maxReconnectAttempts})`, 'error');
this.callbacks.onClose.forEach(cb => cb(1013, '重连次数超限'));
return;
}
const backoffInterval = this.config.reconnectInterval * Math.pow(2, this.reconnectAttempts);
this.reconnectAttempts++;
this.log(`准备重连 (${this.reconnectAttempts}/${this.config.maxReconnectAttempts}),间隔${backoffInterval}ms`, 'warn');
setTimeout(() => {
if (!this.stopWs) {
this.connect();
}
}, backoffInterval);
}
private startHeartbeat(): void {
this.stopHeartbeat();
this.heartbeatTimer = setInterval(() => {
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
this.stopHeartbeat();
return;
}
this.log('发送心跳包...');
this.send({ type: 'heartbeat', timestamp: Date.now() });
// 启动心跳超时检测
this.heartbeatTimeoutTimer = setTimeout(() => {
this.log('心跳超时,判定连接异常,触发重连', 'warn');
this.socket?.close(1006, '心跳超时');
}, this.config.heartbeatTimeout);
}, this.config.heartbeatInterval);
}
private resetHeartbeatTimeout(): void {
if (this.heartbeatTimeoutTimer) {
clearTimeout(this.heartbeatTimeoutTimer);
this.heartbeatTimeoutTimer = null;
}
}
private stopHeartbeat(): void {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
if (this.heartbeatTimeoutTimer) {
clearTimeout(this.heartbeatTimeoutTimer);
this.heartbeatTimeoutTimer = null;
}
}
private cleanup(): void {
this.stopHeartbeat();
if (this.socket) {
try {
this.socket.close(1001, '资源清理');
} catch (e) {}
this.socket = null;
}
}
public getState(): {
readyState: number;
stateText: 'CONNECTING' | 'OPEN' | 'CLOSING' | 'CLOSED' | 'UNKNOWN';
reconnectAttempts: number;
} {
const stateMap = {
0: 'CONNECTING',
1: 'OPEN',
2: 'CLOSING',
3: 'CLOSED'
};
return {
readyState: this.socket?.readyState ?? -1,
stateText: (stateMap[this.socket?.readyState as keyof typeof stateMap] as any) || 'UNKNOWN',
reconnectAttempts: this.reconnectAttempts
};
}
public onOpen(callback: () => void): void {
this.callbacks.onOpen.push(callback);
}
public onMessage(callback: (data: string) => void): void {
this.callbacks.onMessage.push(callback);
}
public onClose(callback: (code?: number, reason?: string) => void): void {
this.callbacks.onClose.push(callback);
}
public onError(callback: (error: Event) => void): void {
this.callbacks.onError.push(callback);
}
public off(
type: 'open' | 'message' | 'close' | 'error',
callback: Function
): void {
const keyMap = {
open: 'onOpen',
message: 'onMessage',
close: 'onClose',
error: 'onError'
};
const key = keyMap[type];
this.callbacks[key] = this.callbacks[key].filter(cb => cb !== callback);
}
}
const wsClient = new WebSocketClient({
url: 'ws://localhost:8080/ws',
reconnectInterval: 3000,
maxReconnectAttempts: 8,
heartbeatInterval: 10000,
heartbeatTimeout: 15000
});
wsClient.onOpen(() => {
console.log('外部监听:连接成功');
wsClient.send({ type: 'test', content: 'hello websocket' });
});
wsClient.onMessage((data) => {
console.log('外部监听:收到消息', data);
});
wsClient.onClose((code, reason) => {
console.log('外部监听:连接关闭', code, reason);
});
wsClient.onError((error) => {
console.log('外部监听:连接错误', error);
});
setTimeout(() => {
wsClient.send('手动发送的消息');
}, 2000);
setInterval(() => {
console.log('当前连接状态:', wsClient.getState());
}, 3000);