You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

NodeJS中Promise封装TCP Socket为何出现内存泄漏?

问题分析与解决方案:Node.js TCP客户端Promise封装的内存泄漏问题

环境信息

  • 开发环境:NodeJS 18.9.0(Windows)
  • 生产环境:树莓派(最新Raspbian系统、NodeJS)

问题背景

使用NodeJS内置的node:net库实现TCP端口持续轮询远程设备,自行编写事件转Promise的封装器后,出现内存泄漏现象。

现象对比

  • 直接使用事件监听器实现持续轮询:heapTotal保持稳定,heapUsed正常上下波动
  • 使用Promise封装器实现:heapTotal和heapUsed随程序运行时间缓慢增长,存在内存泄漏

现有代码核心问题

  1. 事件监听器重复绑定:每次调用connect都会给socket绑定新的data事件监听器,socket为实例级对象,多次重连会导致监听器堆积,无法被垃圾回收(GC)
  2. Promise引用未及时清理:this.resolve和this.reject作为实例属性保存,请求异常(超时、连接断开等)时未正确解除引用,导致Promise对象无法被回收
  3. 超时处理上下文错误:timeoutHandler中直接调用this.reject,但setTimeout回调的默认上下文并非Client实例,会引发错误同时残留引用
  4. 异常分支资源未清理:read方法初始阶段直接reject(未连接/已有pending请求)时,未清理可能残留的定时器或Promise引用

正确的事件转Promise封装方案

修复后的代码解决了内存泄漏问题,同时优化了逻辑健壮性:

const net = require("node:net");

class TCPClient {
  constructor() {
    this.socket = null;
    this.connected = false;
    this.timeout = 0;
    this.pendingReq = null; // 统一存储当前pending请求的resolve/reject/定时器
  }

  connect(uri, port, timeout) {
    // 清理旧socket资源(如果存在)
    if (this.socket) {
      this.socket.removeAllListeners();
      this.socket.destroy();
    }

    this.timeout = timeout;
    this.socket = new net.Socket();
    const socket = this.socket;

    return new Promise((resolve, reject) => {
      // 定义监听器清理函数,避免重复绑定
      const cleanupConnListeners = () => {
        socket.removeListener('connect', onConnect);
        socket.removeListener('error', onError);
      };

      const onConnect = () => {
        this.connected = true;
        cleanupConnListeners();
        resolve();
      };

      const onError = (err) => {
        cleanupConnListeners();
        reject(err);
      };

      // data监听器仅绑定一次(socket长期复用)
      socket.on('data', (data) => this._handleData(data));
      // 监听连接关闭,清理pending请求
      socket.on('close', () => {
        this.connected = false;
        if (this.pendingReq) {
          this.pendingReq.reject(new Error('connection closed'));
          this._clearPendingReq();
        }
      });

      socket.connect(port, uri);
    });
  }

  read(props) {
    if (!this.connected) {
      return Promise.reject(new Error('disconnected'));
    }
    if (this.pendingReq) {
      return Promise.reject(new Error('pending request exists'));
    }

    return new Promise((resolve, reject) => {
      // 封装请求信息,避免分散的实例属性
      const timer = setTimeout(() => {
        reject(new Error('Timeout'));
        this._clearPendingReq();
      }, this.timeout);

      this.pendingReq = { resolve, reject, timer };

      // 处理write失败的场景
      this.socket.write(props.requestMessage, (err) => {
        if (err) {
          reject(err);
          this._clearPendingReq();
        }
      });
    });
  }

  _handleData(data) {
    // 数据校验逻辑
    if (!this._isDataValid(data)) return;

    if (!this.pendingReq) return;

    // 完成请求并清理资源
    this.pendingReq.resolve(data);
    this._clearPendingReq();
  }

  _isDataValid(data) {
    // 实现你的数据校验逻辑
    return true;
  }

  _clearPendingReq() {
    if (this.pendingReq) {
      clearTimeout(this.pendingReq.timer);
      this.pendingReq = null; // 解除引用,让GC回收相关对象
    }
  }

  // 手动销毁客户端,释放所有资源
  destroy() {
    this._clearPendingReq();
    if (this.socket) {
      this.socket.removeAllListeners();
      this.socket.destroy();
      this.socket = null;
    }
    this.connected = false;
  }
}

优化后的调用示例

const client = new TCPClient();
client.connect("my_url", 1234, 1000)
  .then(() => {
    console.log("连接成功!");
    startPolling();
  })
  .catch((err) => {
    console.error("连接失败:", err);
  });

function startPolling() {
  client.read({ requestMessage: "someAddress" })
    .then((data) => {
      console.log("读取到数据:", data);
      // 用setImmediate替代setTimeout(0),性能更优
      setImmediate(startPolling);
    })
    .catch((err) => {
      console.error("读取失败:", err);
      // 异常后延迟重试,避免频繁报错
      setTimeout(startPolling, 1000);
    });
}

关键优化点说明

  • 集中管理pending请求:用pendingReq对象统一存储请求相关资源,避免分散的实例属性,方便统一清理
  • 事件监听器生命周期管理:连接成功/失败后立即清理对应监听器,data监听器仅绑定一次;销毁客户端时清理所有监听器与socket
  • 及时解除引用:请求完成(成功/失败/超时)后立即调用_clearPendingReq,将pendingReq置为null,确保相关对象能被GC回收
  • 覆盖所有异常分支:处理socket关闭、write失败等场景,保证pending请求能被正确处理并清理
  • 上下文安全:内部方法均使用类方法或箭头函数,避免上下文丢失

内容的提问来源于stack exchange,提问作者Ryan Griggs

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 01:08:13