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随程序运行时间缓慢增长,存在内存泄漏
现有代码核心问题
- 事件监听器重复绑定:每次调用
connect都会给socket绑定新的data事件监听器,socket为实例级对象,多次重连会导致监听器堆积,无法被垃圾回收(GC) - Promise引用未及时清理:
this.resolve和this.reject作为实例属性保存,请求异常(超时、连接断开等)时未正确解除引用,导致Promise对象无法被回收 - 超时处理上下文错误:
timeoutHandler中直接调用this.reject,但setTimeout回调的默认上下文并非Client实例,会引发错误同时残留引用 - 异常分支资源未清理:
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
相关产品推荐
相关产品推荐

