WebSocket 复习记录

为什么这个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 个人实践经验,仅供参考

ts 复制代码
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,
};
相关推荐
CodeCaptain1 天前
可直接落地的「Flutter 桥接鸿蒙 WebSocket」端到端实施方案
websocket·flutter·harmonyos
心无旁骛~1 天前
Socket和Websocket编程的区别
网络·websocket·网络协议
BAGAE2 天前
HTTPS 加密原理介绍
java·c++·websocket·http·均值算法·启发式算法·最小二乘法
咖啡教室3 天前
每日一个计算机小知识:Socket
后端·websocket
paishishaba3 天前
HTTP、HTTPS 和 WebSocket 协议和开发
websocket·http·https·实时聊天
小范同学_4 天前
Spring集成WebSocket
java·spring boot·websocket·spring·1024程序员节
YUELEI1186 天前
Springboot WebSocket
spring boot·后端·websocket
Greedy Alg9 天前
Socket编程学习记录
网络·websocket·学习
Cxiaomu9 天前
React Native 项目中 WebSocket 的完整实现方案
websocket·react native·react.js