WebSocket实现长链接

一、为什么要封装webSocket?

原生 WebSocket API 提供了基础的连接、收发消息能力,但在实际项目中直接使用会遇到以下问题:

  • 连接断开后无法自动重连:网络抖动或服务器重启都会导致连接丢失,需要手动处理。

  • 缺乏心跳保活:部分网络设备(如防火墙、负载均衡器)会断开长时间无数据传输的连接,导致"假死"。

  • 消息顺序与队列:服务器推送可能很频繁,如果回调处理耗时,容易造成消息乱序或丢失。

  • 重复连接:应用中可能多处需要使用长连接,如果每个地方都新建实例,会浪费资源并导致状态不一致。

  • 错误处理不统一:缺少统一的错误日志、重试策略等。

二、整体架构

我们的封装主要由三部分组成:

  • 配置接口 LongLinkOptions:定义连接所需的参数(服务器地址、用户信息、订阅类型等)。

  • 核心类 LongLink:管理 WebSocket 生命周期、心跳、重连、消息队列。

  • 单例工厂 initLongLink:保证全局只有一个长连接实例。

代码结构清晰,职责分离,易于维护和扩展

javascript 复制代码
class LongLink {
  // 私有属性:ws 实例、定时器、状态标记、队列等
  // 私有方法:连接、处理事件、心跳、重连等
  // 公开方法:onMessage、close
}

3.1 配置与默认值

通过 options 对象传入所有配置,并提供合理的默认值,减少调用者的心智负担。

javascript 复制代码
interface LongLinkOptions {
  url: string;                       // 必须:WebSocket 地址
  uid: string | number;              // 必须:用户标识
  clientInfo: string;                // 必须:客户端信息
  opTypes: string | string[];        // 必须:订阅类型
  conversationTypes?: string | string[]; // 可选:会话类型,默认 '9'
  heartbeat?: number;                // 可选:心跳间隔(秒),默认 60
  maxRetries?: number;               // 可选:最大重试次数,默认 5
  retryDelay?: number;               // 可选:重连初始延迟(ms),默认 500
  extraSendOptions?: Record<string, any>; // 可选:额外扩展参数
}

3.2 连接建立与初始化

在构造函数中调用 connect() 立即创建 WebSocket 实例,并绑定四个核心事件:

  • onopen:连接成功 → 重置重试计数、启动心跳、发送订阅消息

  • onmessage:收到消息 → 解析、入队、消费

  • onclose:连接关闭 → 清理资源、触发重连(除非被标记为 kill)

  • onerror:发生错误 → 主动关闭连接(会触发 onclose)

初始化消息 :连接成功后,根据配置的 opTypesconversationTypes 发送一个或多个 type: 1 消息,告知服务器该连接要接收哪些业务数据。

3.3 心跳保活

心跳的作用是定期向服务器发送一个很小的数据包(type: 2),保持连接活跃,同时也能检测网络是否通畅。

  • onopen 中启动定时器,每隔 heartbeat 秒发送一次。

  • onclose 中停止定时器,避免内存泄漏。

  • 发送前检查 readyState,确保连接是打开的。

javascript 复制代码
private startHeartbeat(): void {
  this.stopHeartbeat();
  const interval = (this.options.heartbeat ?? 60) * 1000;
  this.heartbeatTimer = setInterval(() => {
    if (this.ws?.readyState === WebSocket.OPEN) {
      this.send({ type: 2, data: 1 });
    }
  }, interval);
}

3.4 自动重连策略

网络波动或服务器重启会导致连接关闭,我们需要自动恢复连接。

  • 重连条件 :不是被主动关闭(kill 标记为 false)且未达到最大重试次数。

  • 重连延迟 :采用线性退避,即 retryDelay * (retryCount + 1)。第一次重连延迟 500ms,第二次 1000ms,依此类推。

  • 最大重试 :设置 maxRetries 避免无限重连,达到上限后停止并打印错误。

  • 重试计数:连接成功后重置为 0,失败时递增。

javascript 复制代码
private handleClose(): void {
  // ... 清理心跳
  if (this.kill) return;
  if (this.retryCount < maxRetries) {
    const delay = retryDelay * (this.retryCount + 1);
    this.reconnectTimer = setTimeout(() => {
      this.retryCount++;
      this.connect();
    }, delay);
  }
}

3.5 消息队列与顺序处理

服务器推送的消息可能同时到达,如果直接调用外部回调,可能因回调的异步性导致消息乱序。使用队列可以保证消息按接收顺序逐个处理。

  • 入队 :收到业务消息(type: 消息)后,解析并 push 到数组末尾。

  • 消费 :调用 consume() 方法,从数组头部 shift 取出一个消息,并通过 messageCallback 抛出。

  • 触发时机 :每次入队后立即消费;当外部通过 onMessage 注册回调后,也会立即消费已有消息(避免积压)。

javascript 复制代码
private handleMessage(evt: MessageEvent): void {
  // 解析消息...
  if (msg.type === '消息') {
    const data = JSON.parse(msg.data);
    this.messageQueue.push(data);
    this.consume();
  }
}

private consume(): void {
  if (!this.messageQueue.length) return;
  if (typeof this.messageCallback !== 'function') return;
  const msg = this.messageQueue.shift();
  this.messageCallback(msg);
}

3.6 错误处理与资源清理

  • 错误事件onerror 触发时,调用 this.ws.close() 主动关闭连接,这会触发 onclose 中的重连逻辑,避免在错误状态下继续尝试发送消息。

  • 定时器清理 :在 connect() 开始前调用 clearTimers() 停止所有定时器(心跳和重连),防止旧的定时器干扰新连接。

  • 连接状态检查 :发送消息前必须判断 readyState === WebSocket.OPEN,避免向已关闭的连接发送数据导致报错。

3.7 单例模式

长连接通常是一个全局资源,应该全局唯一。通过 initLongLink 工厂函数实现单例:

javascript 复制代码
let instance: LongLink | null = null;

export function initLongLink(options: LongLinkOptions) {
  if (!instance) {
    instance = new LongLink(options);
  }
  return {
    onMessage: instance.onMessage.bind(instance),
    close: instance.close.bind(instance),
  };
}

优点

  • 避免重复连接浪费资源。

  • 保证连接状态一致,不会出现多个实例同时操作。

  • 对外只暴露必要的方法(onMessageclose),隐藏内部实现细节。

实现代码:

javascript 复制代码
// ---------- 类型定义 ----------
interface LongLinkOptions {
  url: string; // WebSocket 服务地址
  uid: string | number; // 用户标识
  clientInfo: string; // 客户端信息
  opTypes: string | string[]; // 订阅的操作类型
  conversationTypes?: string | string[]; // 会话类型,默认 '9'
  heartbeat?: number; // 心跳间隔(秒),默认 60
  maxRetries?: number; // 最大重试次数,默认 5
  retryDelay?: number; // 重连初始延迟(ms),默认 500
  extraSendOptions?: Record<string, any>; // 额外发送参数
}

interface SocketMessage {
  type: number;
  data: any;
}

// 默认配置
const defaultOptions: Partial<LongLinkOptions> = {
  conversationTypes: "9",
  heartbeat: 60,
  maxRetries: 5,
  retryDelay: 500,
};

class WsLink {
  private readonly options: LongLinkOptions;
  private ws: WebSocket | null = null;
  private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
  private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
  private kill = false;
  private retryCount = 0;
  private messageQueue: any[] = [];
  private messageCallback: (msg: any) => void = () => {};

  constructor(options: LongLinkOptions) {
    this.options = { ...defaultOptions, ...options };
    this.connect();
  }

  /* 创建webSocket链接 */
  private connect(): void {
    /* 清理之前的定时器和链接 */
    this.clearTimers();
    if (this.ws) {
      this.ws.close();
      this.ws = null;
    }
    this.ws = new WebSocket(this.options.url, "协议名称");
    // 绑定连接成功事件
    this.ws.onopen = this.handleOpen.bind(this);
    // 绑定收到消息事件
    this.ws.onmessage = this.handleMessage.bind(this);
    //  绑定连接关闭事件
    this.ws.onclose = this.handleClose.bind(this);
    // 绑定连接错误事件
    this.ws.onerror = this.handleError.bind(this);
  }

  /* 链接成功 */
  private handleOpen(): void {
    console.warn("[LongLink] 已连接");
    this.retryCount = 0;
    this.startHeartbeat();
    const opList = this.normalize(this.options.opTypes);
    const convList = this.normalize(this.options.conversationTypes!);
    opList.forEach((op, i) => {
      const conv = convList[i] || convList[0];
      this.send({
        type: 1,
        data: {
          uid: this.options.uid,
          "client-info": this.options.clientInfo,
          conversationType: conv,
          opType: op,
          heartbeatExpire: this.options.heartbeat,
          ...this.options.extraSendOptions,
        },
      });
    });
  }
  // 接收消息
  private handleMessage(evt: MessageEvent): void {
    let msg: any;
    try {
      msg = JSON.parse(evt.data);
    } catch (e) {
      console.error("[LongLink] 消息解析失败", e);
      return;
    }

    if (msg.type === 'error' && msg.data && typeof msg.data === "string") {
      try {
        const data = JSON.parse(msg.data);
        this.messageQueue.push(data);
        this.consume();
      } catch (e) {
        console.error("[LongLink] 业务数据解析失败", e);
      }
    }
    if (msg.type === 0 && msg.data === 401) {
      this.close(true);
    }
  }
  // 连接关闭
  private handleClose(): void {
    console.warn("[LongLink] 连接关闭");
    this.stopHeartbeat();
    this.ws = null;

    if (this.kill) return;

    const max = this.options.maxRetries ?? 5;
    if (this.retryCount < max) {
      const delay = (this.options.retryDelay ?? 500) * (this.retryCount + 1);
      console.warn(
        `[LongLink] ${delay}ms 后重试 (${this.retryCount + 1}/${max})`,
      );
      this.reconnectTimer = setTimeout(() => {
        this.retryCount++;
        this.connect();
      }, delay);
    } else {
      console.error("[LongLink] 已达最大重试次数,停止重连");
    }
  }
  // 连接错误
  private handleError(err: Event): void {
    console.error("[LongLink] 错误", err);
    this.ws?.close(); // 触发重连
  }

  // 心跳
  private startHeartbeat(): void {
    this.stopHeartbeat();
    const interval = (this.options.heartbeat ?? 60) * 1000;
    this.heartbeatTimer = setInterval(() => {
      if (this.ws?.readyState === WebSocket.OPEN) {
        this.send({ type: 2, data: 1 });
      }
    }, interval);
  }

  /**
   * 停止心跳
   */
  private stopHeartbeat(): void {
    if (this.heartbeatTimer) {
      clearInterval(this.heartbeatTimer);
      this.heartbeatTimer = null;
    }
  }
  /**
   * 清除所有定时器(心跳、重连)
   */
  private clearTimers(): void {
    this.stopHeartbeat();
    if (this.reconnectTimer) {
      clearTimeout(this.reconnectTimer);
      this.reconnectTimer = null;
    }
  }
  // 发送消息(仅连接打开时)
  private send(msg: SocketMessage): void {
    if (this.ws?.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify(msg));
    }
  }

  // 数组化处理
  private normalize<T>(val: T | T[]): T[] {
    return Array.isArray(val) ? val : [val];
  }

  // 消费队列
  private consume(): void {
    if (!this.messageQueue.length) return;
    if (typeof this.messageCallback !== "function") return;
    const msg = this.messageQueue.shift();
    this.messageCallback(msg);
  }

  // ---------- 公开 API ----------
  public onMessage(cb: (msg: any) => void): void {
    this.messageCallback = cb;
    this.consume(); // 立即消费已有消息
  }

  public close(kill: boolean = false): void {
    this.kill = kill;
    this.clearTimers();
    if (this.ws) {
      this.ws.close();
      this.ws = null;
    }
  }
}

// ---------- 单例 ----------
let instance: WsLink | null = null;

export function initLongLink(options: LongLinkOptions): Pick<WsLink, 'onMessage' | 'close'> {
  if (!instance) {
    instance = new WsLink(options);
  }
  return {
    onMessage: instance.onMessage.bind(instance),
    close: instance.close.bind(instance),
  };
}
相关推荐
2501_921649492 小时前
WebSocket 金融实时行情推送 API 实战解析:低延迟、高可用架构设计与落地
websocket·网络协议·金融·node.js
混迹中的咸鱼2 小时前
UE5 网络联机常用命令
网络·ue5
wqww_12 小时前
Java 前后端 WebSocket 完整实现
java·开发语言·websocket
xiaomo22492 小时前
javaee-网络编程(基础)
运维·服务器·网络
云飞云共享云桌面2 小时前
8人SolidWorks研发共享一台服务器——性能算力共享智能按需分配
运维·服务器·网络·数据库·3d·电脑
wanhengidc2 小时前
云手机 操作简单易上手
网络·安全·智能手机
weixin_425023003 小时前
Spring Boot 2.7+JDK8+WebSocket对接阿里云百炼Qwen3.5-Plus 实现流式对话+思考过程实时展示
java·spring boot·websocket·ai编程
王家视频教程图书馆3 小时前
你在 HTTPS 页面 里加载 HTTP 资源 → ,不支持 HTTPS → 握手失败。浏览器自动升级为 HTTPS。你的 8080 端口只支持 HTTP
网络协议·http·https
aodunsoft3 小时前
安全月报 | 傲盾DDoS攻击防御2026年3月简报
网络·安全·ddos