将单个 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. 容错保留:单个实例仍保留重连、消息发送容错、超时关闭等逻辑,稳定性不打折。
相关推荐
宠..5 小时前
创建单选按钮控件
java·服务器·数据库
@Mr Wang6 小时前
云服务器之使用jupyter运行ipynb文件
服务器·python·jupyter·notebook
2401_865854886 小时前
服务器的windows和Linux系统有什么区别
linux·运维·服务器
海域云-罗鹏6 小时前
企业服务器防黑客攻击:WAF防火墙部署全攻略
运维·服务器
云飞云共享云桌面6 小时前
SolidWorks服务器怎么实现研发软件多人共享、数据安全管理
java·linux·运维·服务器·数据库·自动化
ZeroNews内网穿透6 小时前
EasyNode 结合 ZeroNews,实现远程管理服务器
运维·服务器·网络协议·安全·http
晚晶7 小时前
[C++/流媒体/tcp/rtsp]构建一个简单的流媒体转发服务器,用于将rtsp推流转发出去
服务器·c++·tcp/ip·流媒体·转发·rtsp
翼龙云_cloud7 小时前
阿里云渠道商:在更换阿里云 GPU 公网 IP 时,如何确保数据的安全性?
运维·服务器·tcp/ip·阿里云·云计算
山川而川-R7 小时前
在香橙派5pro上的ubuntu22.04系统烧录镜像_2_12.23
linux·运维·服务器