WebSocket 复习记录

77 阅读6分钟

为什么这个2分钟后就不能完成交卷了?记一次bug修复,复习记录一下WebSocket。

一、为什么需要 WebSocket?

在 WebSocket 出现之前,面对实现实时通信的业务场景,比较常采用的方式是长、短轮询,在特定时间间隔(比如每秒)由浏览器发出请求,服务器返回最新的数据。但这种轮询方式的缺陷也相对比较明显。

  • HTTP 请求一般包含的头部信息比较多,其中有效的数据可能只占很小的一部分,导致带宽浪费;
  • 服务器被动接收浏览器的请求然后响应,数据没有更新时仍然要接收并处理请求,导致服务器 CPU 占用
  • 短轮询频繁轮询对服务器压力较大,即使使用长轮询方案,客户端较多时仍会对客户端造成不小压力;

而 WebSocket 的出现就很好的解决上述问题:

  • WebSocket 的头部信息少,通常只有 2Bytes 左右,能节省带宽;
  • WebSocket 支持服务端主动推送消息,更好地支持实时通信;

二、WebSocket 是什么?

WebSocket API 是一种先进的技术,可在用户浏览器和服务器之间开启双向交互式通信会话。利用该 API,可以向服务器发送信息,并接收事件驱动的响应,而无需轮询服务器以获得回复。

WebSocket 作为一种网络传输协议,可在单个 TCP 连接上进行全双工通信,位于 OSI 模型的应用层。

WebSocket 属性

名称描述
binaryType使用二进制的数据类型连接
bufferedAmount(只读)未发送至服务器的字节数
extensions(只读)服务器选择的扩展
onclose用于指定连接关闭后的回调函数
onerror用于指定连接失败后的回调函数
onmessage用于指定当从服务器接受到信息时的回调函数
onopen用于指定连接成功后的回调函数
protocol(只读)用于返回服务器端选中的子协议的名字
readyState(只读)返回当前 WebSocket 的连接状态,共有 4 种状态: 0 - 表示连接尚未建立。1 - 表示连接已建立,可以进行通信。2 - 表示连接正在进行关闭。3 - 表示连接已经关闭或者连接不能打开。

WebSocket 方法

(已创建Socket对象)

方法描述
Socket.send()使用连接发送数据
Socket.close()关闭连接

三、 WebSocket 优缺点

优点

  • 实时性: WebSocket 提供了双向通信,服务器可以主动向客户端推送数据,实现实时性非常高,适用于实时聊天、在线协作等应用。
  • 减少网络延迟: 与轮询和长轮询相比,WebSocket 可以显著减少网络延迟,因为不需要在每个请求之间建立和关闭连接。
  • 较小的数据传输开销: WebSocket 的数据帧相比于 HTTP 请求报文较小,减少了在每个请求中传输的开销,特别适用于需要频繁通信的应用。
  • 较低的服务器资源占用: 由于 WebSocket 的长连接特性,服务器可以处理更多的并发连接,相较于短连接有更低的资源占用。
  • 跨域通信: 与一些其他跨域通信方法相比,WebSocket 更容易实现跨域通信。

缺点

  • 连接状态保持: 长时间保持连接可能会导致服务器和客户端都需要维护连接状态,可能增加一些负担。
  • 不适用于所有场景: 对于一些请求-响应模式较为简单的场景,WebSocket 的实时特性可能并不是必要的,使用 HTTP 请求可能更为合适。
  • 复杂性: 与传统的 HTTP 请求相比,WebSocket 的实现和管理可能稍显复杂,尤其是在处理连接状态、异常等方面。

四、WebSocket 适用场景

  • 实时聊天应用: WebSocket 是实现实时聊天室、即时通讯应用的理想选择,因为它能够提供低延迟和高实时性。
  • 在线协作和协同编辑: 对于需要多用户协同工作的应用,如协同编辑文档或绘图,WebSocket 的实时性使得用户能够看到其他用户的操作。
  • 实时数据展示: 对于需要实时展示数据变化的应用,例如股票行情、实时监控系统等,WebSocket 提供了一种高效的通信方式。
  • 在线游戏: 在线游戏通常需要快速、实时的通信,WebSocket 能够提供低延迟和高并发的通信能力。
  • 推送服务: 用于实现消息推送服务,向客户端主动推送更新或通知。

五、WebSocket 个人实践经验,仅供参考

export interface WebSocketConfig {
  /** WebSocket服务器地址 */
  url: string;
  /** 心跳间隔时间(毫秒),默认30秒 */
  heartbeatInterval?: number;
  /** 心跳超时时间(毫秒),默认10秒 */
  heartbeatTimeout?: number;
  /** 重连间隔时间(毫秒),默认5秒 */
  reconnectInterval?: number;
  /** 最大重连次数,默认5次,-1表示无限重连 */
  maxReconnectAttempts?: number;
  /** 心跳消息内容,默认为'ping' */
  heartbeatMessage?: string;
  /** 是否启用自动重连,默认true */
  autoReconnect?: boolean;
  /** 是否启用心跳检测,默认true */
  enableHeartbeat?: boolean;
  /** WebSocket协议 */
  protocols?: string | string[];
}

export interface WebSocketEvents {
  /** 连接打开 */
  onOpen?: (event: Event) => void;
  /** 接收到消息 */
  onMessage?: (data: any, event: MessageEvent) => void;
  /** 连接关闭 */
  onClose?: (event: CloseEvent) => void;
  /** 连接错误 */
  onError?: (event: Event) => void;
  /** 心跳超时 */
  onHeartbeatTimeout?: () => void;
  /** 重连开始 */
  onReconnect?: (attempt: number) => void;
  /** 重连失败 */
  onReconnectFailed?: () => void;
}

export enum WebSocketState {
  CONNECTING = 0,
  OPEN = 1,
  CLOSING = 2,
  CLOSED = 3,
}

export class WebSocketManager {
  private ws: WebSocket | null = null;
  private config: Required<WebSocketConfig>;
  private events: WebSocketEvents;
  private heartbeatTimer: number | null = null;
  private heartbeatTimeoutTimer: number | null = null;
  private reconnectTimer: number | null = null;
  private reconnectAttempts = 0;
  private isManualClose = false;
  private lastHeartbeatTime = 0;

  constructor(config: WebSocketConfig, events: WebSocketEvents = {}) {
    this.config = {
      heartbeatInterval: 30000,
      heartbeatTimeout: 10000,
      reconnectInterval: 5000,
      maxReconnectAttempts: 5,
      heartbeatMessage: "ping",
      autoReconnect: true,
      enableHeartbeat: true,
      protocols: [],
      ...config,
    };
    this.events = events;
  }

  /**
   * 连接WebSocket
   */
  public connect(): void {
    if (this.ws && this.ws.readyState === WebSocket.OPEN) {
      return;
    }

    try {
      this.ws = new WebSocket(this.config.url, this.config.protocols);
      this.setupEventListeners();
    } catch (error) {
      console.error("WebSocket连接失败:", error);
      this.events.onError?.(error as Event);
    }
  }

  /**
   * 断开WebSocket连接
   */
  public disconnect(): void {
    this.isManualClose = true;
    this.clearTimers();

    if (this.ws) {
      this.ws.close();
      this.ws = null;
    }
  }

  /**
   * 发送消息
   */
  public send(data: string | ArrayBuffer | Blob): boolean {
    if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
      return false;
    }

    try {
      this.ws.send(data);
      return true;
    } catch (error) {
      console.error("发送消息失败:", error);
      return false;
    }
  }

  /**
   * 获取连接状态
   */
  public getState(): WebSocketState {
    return this.ws?.readyState ?? WebSocketState.CLOSED;
  }

  /**
   * 是否已连接
   */
  public isConnected(): boolean {
    return this.ws?.readyState === WebSocket.OPEN;
  }

  /**
   * 获取重连次数
   */
  public getReconnectAttempts(): number {
    return this.reconnectAttempts;
  }

  /**
   * 设置事件监听器
   */
  private setupEventListeners(): void {
    if (!this.ws) return;

    this.ws.onopen = (event) => {
      this.isManualClose = false;
      this.reconnectAttempts = 0;
      this.lastHeartbeatTime = Date.now();

      if (this.config.enableHeartbeat) {
        this.startHeartbeat();
      }

      this.events.onOpen?.(event);
    };

    this.ws.onmessage = (event) => {
      try {
        // 检查是否是心跳响应
        if (this.isHeartbeatResponse(event.data)) {
          this.lastHeartbeatTime = Date.now();
          this.clearHeartbeatTimeout();
          return;
        }

        // 解析消息数据
        let data: any;
        try {
          data = JSON.parse(event.data);
        } catch {
          data = event.data;
        }

        this.events.onMessage?.(data, event);
      } catch (error) {
        console.error("处理消息失败:", error);
      }
    };

    this.ws.onclose = (event) => {
      this.clearTimers();

      this.events.onClose?.(event);

      // 如果不是手动关闭且启用自动重连,则尝试重连
      if (!this.isManualClose && this.config.autoReconnect) {
        this.attemptReconnect();
      }
    };

    this.ws.onerror = (event) => {
      this.events.onError?.(event);
    };
  }

  /**
   * 开始心跳检测
   */
  private startHeartbeat(): void {
    this.clearHeartbeat();

    this.heartbeatTimer = window.setInterval(() => {
      if (this.ws?.readyState === WebSocket.OPEN) {
        this.send(this.config.heartbeatMessage);
        this.startHeartbeatTimeout();
      }
    }, this.config.heartbeatInterval);
  }

  /**
   * 开始心跳超时检测
   */
  private startHeartbeatTimeout(): void {
    this.clearHeartbeatTimeout();

    this.heartbeatTimeoutTimer = window.setTimeout(() => {
      this.events.onHeartbeatTimeout?.();

      if (this.ws) {
        this.ws.close();
      }
    }, this.config.heartbeatTimeout);
  }

  /**
   * 清除心跳定时器
   */
  private clearHeartbeat(): void {
    if (this.heartbeatTimer) {
      clearInterval(this.heartbeatTimer);
      this.heartbeatTimer = null;
    }
  }

  /**
   * 清除心跳超时定时器
   */
  private clearHeartbeatTimeout(): void {
    if (this.heartbeatTimeoutTimer) {
      clearTimeout(this.heartbeatTimeoutTimer);
      this.heartbeatTimeoutTimer = null;
    }
  }

  /**
   * 清除所有定时器
   */
  private clearTimers(): void {
    this.clearHeartbeat();
    this.clearHeartbeatTimeout();
    this.clearReconnectTimer();
  }

  /**
   * 清除重连定时器
   */
  private clearReconnectTimer(): void {
    if (this.reconnectTimer) {
      clearTimeout(this.reconnectTimer);
      this.reconnectTimer = null;
    }
  }

  /**
   * 尝试重连
   */
  private attemptReconnect(): void {
    if (
      this.config.maxReconnectAttempts !== -1 &&
      this.reconnectAttempts >= this.config.maxReconnectAttempts
    ) {
      this.events.onReconnectFailed?.();
      return;
    }

    this.reconnectAttempts++;

    this.events.onReconnect?.(this.reconnectAttempts);

    this.reconnectTimer = window.setTimeout(() => {
      this.connect();
    }, this.config.reconnectInterval);
  }

  /**
   * 检查是否是心跳响应
   */
  private isHeartbeatResponse(data: string): boolean {
    return data === "pong" || data === this.config.heartbeatMessage;
  }
}

/**
 * 创建WebSocket实例的工厂函数
 */
export function createWebSocket(
  config: WebSocketConfig,
  events?: WebSocketEvents
): WebSocketManager {
  return new WebSocketManager(config, events);
}

/**
 * 默认配置
 */
export const defaultWebSocketConfig: Partial<WebSocketConfig> = {
  heartbeatInterval: 30000,
  heartbeatTimeout: 10000,
  reconnectInterval: 5000,
  maxReconnectAttempts: 5,
  heartbeatMessage: "ping",
  autoReconnect: true,
  enableHeartbeat: true,
};