WebSocket客户端封装类

关于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);
相关推荐
四眼肥鱼1 小时前
全网最全的 qiankun 基于 react18+(主应用)、vue3.4+(微应用)实现页签缓存,页面缓存
前端·javascript
dorisrv1 小时前
优雅地处理前端错误边界
前端
狗哥哥1 小时前
Pinia Store 平滑迁移:用代理模式实现零风险重构
前端·架构
老前端的功夫2 小时前
前端水印技术深度解析:从基础实现到防破解方案
开发语言·前端·javascript·前端框架
霍格沃兹测试学院-小舟畅学2 小时前
性能测试入门:使用 Playwright 测量关键 Web 性能指标
开发语言·前端·php
tangbin5830852 小时前
iOS Swift 工具类:数据转换工具 ParseDataTool
前端
潜水豆2 小时前
AI 时代的前端究竟还能积累什么
前端
www_stdio2 小时前
手写 instanceof:深入理解 JavaScript 原型与继承机制
前端·javascript·html
boombb2 小时前
国际化方案:多环境、多语言、动态加载的完整实践
前端