我在使用 dingtalk-stream v2.1.5的过程中遇到了一个比较严重的问题:跑了两天的机器人直接把我电脑的网络端口全部耗尽了,整台电脑断网,所有程序都报Can't assign requested address。
排查下来发现是 SDK 里有两个 bug 导致 TCP 连接泄漏:
Bug 1:_connect() 不销毁旧的 WebSocket
_connect() 里直接 this.socket = new WebSocket(...) 覆盖了旧的引用,但旧的socket 对应的 TCP 连接并没有被关闭,每次重连就泄漏一个连接。
// client.mjs L134
this.socket = new WebSocket(this.dw_url, this.sslopts);
// 旧的 socket 就这么丢了,TCP 连接永远不释放
Bug 2:disconnect() 无法阻止已排队的 autoReconnect
close 事件里 setTimeout(this.connect.bind(this), this.reconnectInterval) 一旦排进队列,之后再调 disconnect() 设 userDisconnect = true已经来不及了,因为 connect() 方法开头不检查 userDisconnect,只在 catch分支才检查。
另外 disconnect() 用的是 socket.close(),如果对端不响应关闭握手,TCP连接就永远不会释放。
影响:这两个问题叠加起来,心跳超时一触发重连就不断泄漏 socket。我这边一个 node进程最后积累了 18000+ 个 ESTABLISHED 连接,直接把系统临时端口用完了。
建议修复
1. _connect() 开头加一行销毁旧 socket:
if (this.socket) {
try { this.socket.removeAllListeners(); this.socket.terminate(); } catch (e)
{}
this.socket = null;
}
2. connect() 开头检查 userDisconnect,阻止已排队的定时器继续执行:
async connect() {
if (this.userDisconnect) return;
// ...
}
3. disconnect() 用 terminate() 替代 close(),并清除事件监听器:
disconnect() {
this.userDisconnect = true;
if (this.config.keepAlive && this.heartbeatIntervallId !== void 0) {
clearInterval(this.heartbeatIntervallId);
}
if (this.socket) {
try { this.socket.removeAllListeners(); this.socket.terminate(); } catch
(e) {}
this.socket = null;
}
}
我这边已经用 patch-package打了补丁在跑了,目前稳定运行没有再泄漏。希望官方能在后续版本里修复一下,不然其他开发者也可能踩到这个坑。
我在使用 dingtalk-stream v2.1.5的过程中遇到了一个比较严重的问题:跑了两天的机器人直接把我电脑的网络端口全部耗尽了,整台电脑断网,所有程序都报Can't assign requested address。
排查下来发现是 SDK 里有两个 bug 导致 TCP 连接泄漏:
Bug 1:_connect() 不销毁旧的 WebSocket
_connect() 里直接 this.socket = new WebSocket(...) 覆盖了旧的引用,但旧的socket 对应的 TCP 连接并没有被关闭,每次重连就泄漏一个连接。
// client.mjs L134
this.socket = new WebSocket(this.dw_url, this.sslopts);
// 旧的 socket 就这么丢了,TCP 连接永远不释放
Bug 2:disconnect() 无法阻止已排队的 autoReconnect
close 事件里 setTimeout(this.connect.bind(this), this.reconnectInterval) 一旦排进队列,之后再调 disconnect() 设 userDisconnect = true已经来不及了,因为 connect() 方法开头不检查 userDisconnect,只在 catch分支才检查。
另外 disconnect() 用的是 socket.close(),如果对端不响应关闭握手,TCP连接就永远不会释放。
影响:这两个问题叠加起来,心跳超时一触发重连就不断泄漏 socket。我这边一个 node进程最后积累了 18000+ 个 ESTABLISHED 连接,直接把系统临时端口用完了。
建议修复
1. _connect() 开头加一行销毁旧 socket:
if (this.socket) {
try { this.socket.removeAllListeners(); this.socket.terminate(); } catch (e)
{}
this.socket = null;
}
2. connect() 开头检查 userDisconnect,阻止已排队的定时器继续执行:
async connect() {
if (this.userDisconnect) return;
// ...
}
3. disconnect() 用 terminate() 替代 close(),并清除事件监听器:
disconnect() {
this.userDisconnect = true;
if (this.config.keepAlive && this.heartbeatIntervallId !== void 0) {
clearInterval(this.heartbeatIntervallId);
}
if (this.socket) {
try { this.socket.removeAllListeners(); this.socket.terminate(); } catch
(e) {}
this.socket = null;
}
}
我这边已经用 patch-package打了补丁在跑了,目前稳定运行没有再泄漏。希望官方能在后续版本里修复一下,不然其他开发者也可能踩到这个坑。