将单个 WebSocket 客户端封装为实例

核心设计思路

封装独立客户端类:将单个 WS 客户端的连接、发送、重连、资源清理逻辑封装为 WsTestClient 类,每个实例拥有独立的 socket、定时器、消息索引;

并发实例管理:通过配置 clientCount 指定并发数,批量创建实例并启动;

启动时间错开:配置 startOffset 让实例分批启动(避免瞬间建立大量连接,服务端扛不住);

全局资源管控:维护所有实例的列表,支持统一关闭、错误隔离(单个实例崩溃不影响其他实例)。

复制代码
const WebSocket = require('ws');

// 全局配置
const config = {
  url: "ws://localhost:18888", // WS服务端地址
  clientCount: 5, // 并发客户端实例数
  startOffset: 5, // 实例启动错开时间(毫秒):避免同时连服务端,可设0
  messages: [
    "CwAzdQoAEgdhbmRyb2lk",
    "DgA1dQgBEMXGCBiJlQY=",
    "DgA1dQgBEMXGCBiJlQY=",
    "DgA1dQgBEMXGCBiJlQY=",
    "DgA1dQgBEMXGCBiJlQY=",
    "EgA2dQgBEMXGCBgBIJz5BygB",
    "EgA2dQgBEMXGCBgBIJz5BygB",
    "EgA2dQgBEMXGCBgBIJz5BygB",
    "DAA3dQgBEMXGCBgF",
    "DAA3dQgBEMXGCBgF",
    "DAA3dQgBEMXGCBgF",
    "EQA4dQgBEPXPrjYYtfgHIAE=",
    "EQA4dQgBEPXPrjYYtfgHIAE=",
  ],
  interval: 1*1000, // 单个实例的消息发送间隔
  duration: 300*1000, // 单个实例的运行时长(从连接成功开始计时)
  reconnectMaxTimes: 0, // 单个实例最大重连次数
  reconnectInterval: 3000, // 单个实例重连间隔
};

// 全局状态:管理所有客户端实例
const globalState = {
  clients: [], // 存储所有WS客户端实例
  isStopped: false, // 是否全局停止
};

// 通用日志工具(增加实例ID标识)
function log(clientId, type, msg) {

    function coverTimeStr2 (timestamp) {
        const date = new Date(timestamp * 1000); // 根据时间戳创建Date对象
        const year = date.getFullYear(); // 获取年份
        let month = date.getMonth() + 1; // 获取月份,需要加1
        let day = date.getDate(); // 获取日期
        let hour = date.getHours(); // 获取小时
        let minute = date.getMinutes(); // 获取分钟
        let second = date.getSeconds(); // 获取秒钟

        month = month<10 ? ("0"+month) : (month)
        day = day<10 ? ("0"+day) : (day)
        hour = hour<10 ? ("0"+hour) : (hour)
        minute = minute<10 ? ("0"+minute) : (minute)
        second = second<10 ? ("0"+second) : (second)
        return `${year}/${month}/${day} ${hour}:${minute}:${second}`; // 拼接成格式化后的日期字符串
    }
    const now = (new Date() ).getTime()/1000
    const timestamp1 = coverTimeStr2(now)
    console.log(`[${timestamp1}] [客户端${clientId}] [${type}] ${msg}`);
}

/**
 * WebSocket测试客户端类(单个实例)
 * @param {number} clientId 实例唯一标识
 */
class WsTestClient {
  constructor(clientId) {
    this.clientId = clientId; // 实例ID,用于日志区分
    this.socket = null; // 单个实例的WS连接
    this.intervalId = null; // 发送消息的定时器
    this.timeoutId = null; // 运行时长超时定时器
    this.reconnectCount = 0; // 重连计数
    this.messageIndex = 0; // 已发送消息索引(单个实例独立)
  }

  // 清理当前实例的所有资源(定时器+连接)
  cleanResources() {
    if (this.intervalId) {
      clearInterval(this.intervalId);
      this.intervalId = null;
    }
    if (this.timeoutId) {
      clearTimeout(this.timeoutId);
      this.timeoutId = null;
    }
    if (this.socket && this.socket.readyState === WebSocket.OPEN) {
      this.socket.close(1000, `客户端${this.clientId}正常关闭`);
    }
    this.socket = null;
    this.messageIndex = 0;
  }

  // 单个实例的重连逻辑
  reconnect() {
    if (globalState.isStopped) return; // 全局停止则不重连
    if (this.reconnectCount >= config.reconnectMaxTimes) {
      log(this.clientId, 'ERROR', `重连${config.reconnectMaxTimes}次失败,停止重连`);
      this.cleanResources();
      return;
    }
    this.reconnectCount++;
    log(this.clientId, 'WARN', `第${this.reconnectCount}次重连中...`);
    setTimeout(() => this.connect(), config.reconnectInterval);
  }

  // 单个实例的消息发送逻辑
  startSending() {
    if (globalState.isStopped) return;
    this.intervalId = setInterval(() => {
      // 1. 消息发完:停止发送
      if (this.messageIndex >= config.messages.length) {
        // log(this.clientId, 'INFO', "⏹️ 消息序列发送完成");
        // this.cleanResources();
        // return;
        this.messageIndex=0
      }
      // 2. 连接未打开:跳过发送
      if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
        log(this.clientId, 'WARN', "❌ 连接未打开,跳过本次发送");
        return;
      }

      // 3. 发送消息(容错处理)
      try {
        const buffer = Buffer.from(config.messages[this.messageIndex], 'base64');
        this.socket.send(buffer);
        log(this.clientId, 'INFO', `📤 发送Protobuf消息 ${this.messageIndex + 1}/${config.messages.length}`);
        this.messageIndex++;
      } catch (error) {
        log(this.clientId, 'ERROR', `❌ 发送消息${this.messageIndex + 1}失败:${error.message}`);
        this.messageIndex++; // 失败也递增,避免卡死(可改为重试)
      }
    }, config.interval);
  }

  // 单个实例的连接逻辑
  connect() {
    if (globalState.isStopped) return;
    this.cleanResources(); // 连接前清理旧资源

    this.socket = new WebSocket(config.url);
    // 连接成功
    this.socket.onopen = () => {
      log(this.clientId, 'INFO', "✅ 连接到WebSocket服务端成功");
      this.reconnectCount = 0; // 重连成功,重置计数
      this.startSending(); // 启动消息发送

      // 重置运行时长超时:从连接成功开始计时
      this.timeoutId = setTimeout(() => {
        log(this.clientId, 'INFO', "⏰ 运行时长超时,关闭连接");
        this.cleanResources();
      }, config.duration);
    };

    // 接收服务端消息
    this.socket.onmessage = (event) => {
      log(this.clientId, 'INFO', `📥 收到服务端消息:${event.data}`);
    };

    // 连接关闭
    this.socket.onclose = (code, reason) => {
      log(this.clientId, 'WARN', `🔌 连接断开(code: ${code}, reason: ${reason})`);
      this.reconnect(); // 断开自动重连
    };

    // 连接错误
    this.socket.onerror = (error) => {
      log(this.clientId, 'ERROR', `❌ 连接错误:${error.message}`);
      this.cleanResources();
      this.reconnect(); // 错误自动重连
    };
  }

  // 启动单个实例
  start() {
    log(this.clientId, 'INFO', "🚀 启动客户端实例");
    this.connect();
  }

  // 停止单个实例
  stop() {
    log(this.clientId, 'INFO', "🛑 停止客户端实例");
    this.cleanResources();
  }
}

/**
 * 全局启动所有客户端实例
 */
function runAllClients() {
  log('GLOBAL', 'INFO', `🚀 开始启动${config.clientCount}个WebSocket客户端实例`);
  globalState.isStopped = false;

  // 批量创建并启动实例(错开时间)
  for (let i = 0; i < config.clientCount; i++) {
    const client = new WsTestClient(i + 1); // 实例ID从1开始
    globalState.clients.push(client);
    // 错开启动时间
    setTimeout(() => client.start(), i * config.startOffset);
  }

  // 全局错误捕获:避免单个实例错误导致进程崩溃
  process.on('uncaughtException', (err) => {
    log('GLOBAL', 'ERROR', `未捕获异常:${err.message}`);
    stopAllClients();
  });
  process.on('unhandledRejection', (err) => {
    log('GLOBAL', 'ERROR', `未处理的Promise拒绝:${err.message}`);
    stopAllClients();
  });

  // 监听进程退出,清理所有资源
  process.on('SIGINT', () => {
    log('GLOBAL', 'INFO', "🛑 接收到退出信号,关闭所有客户端");
    stopAllClients();
    process.exit(0);
  });
}

/**
 * 全局停止所有客户端实例
 */
function stopAllClients() {
  globalState.isStopped = true;
  log('GLOBAL', 'INFO', `🛑 开始关闭所有${globalState.clients.length}个客户端实例`);
  globalState.clients.forEach(client => client.stop());
  globalState.clients = []; // 清空实例列表
}

// 执行启动
runAllClients();

核心配置说明(重点关注)

配置项 作用
clientCount 并发客户端实例数(比如设为 5,就会启动 5 个独立的 WS 连接)
startOffset 实例启动错开时间(毫秒):设为 500 表示第 1 个实例立即启动,第 2 个 500ms 后,以此类推,避免瞬间压垮服务端
其他配置 与单实例一致,但作用于每个独立实例 (比如每个实例都会按interval发送消息)

关键特性

  1. 实例隔离 :每个客户端实例有独立的 socket、定时器、消息索引,单个实例出错 / 重连不会影响其他实例;
  2. 日志区分 :所有日志都带「客户端 ID」,比如 [客户端3],能清晰区分每个实例的连接 / 发送状态;
  3. 柔性启动startOffset 配置让实例分批启动,避免短时间内建立大量连接,服务端更易承受;
  4. 全局管控 :支持 stopAllClients() 统一关闭所有实例,也能监听进程退出信号自动清理资源;
  5. 容错保留:单个实例仍保留重连、消息发送容错、超时关闭等逻辑,稳定性不打折。
相关推荐
Java.熵减码农6 小时前
解决Linux修改环境变量后导致登录循环进不去系统的问题
linux·运维·服务器
明天好,会的7 小时前
分形生成实验(五):人机协同破局--30万token揭示Actix-web状态管理的微妙边界
运维·服务器·前端
徐同保8 小时前
nginx转发,指向一个可以正常访问的网站
linux·服务器·nginx
不一样的故事1269 小时前
下的 “Wi-Fi参数配置” 列表,但您当前选中的导航菜单项是 “IP规划”。您遇到的 “IP加载不出来” 问题,很可能
网络协议·tcp/ip·华为
我是苏苏9 小时前
Web开发:C#通过ProcessStartInfo动态调用执行Python脚本
java·服务器·前端
相偎11 小时前
Ubuntu搭建svn服务器
服务器·ubuntu·svn
咕噜企业分发小米12 小时前
有哪些开源的直播云服务器安全防护方案?
运维·服务器·云计算
开开心心_Every12 小时前
安卓后台录像APP:息屏录存片段,行车用
java·服务器·前端·学习·eclipse·edge·powerpoint
qq_3168377512 小时前
IP网段冲突 配置指定ip使用指定的网络接口发送,而不经过默认网关
服务器·网络·tcp/ip
枷锁—sha14 小时前
彻底解决 Google Gemini 报错:异常流量与 IP 地址冲突排查指南
网络·网络协议·tcp/ip