Node.js Promise结合TCP套接字为何出现执行异常?
问题描述
我编写了一个Node.js程序,通过Promise批量连接一系列TCP套接字,测试目标为发送消息后等待套接字超时或触发连接错误。但程序出现不一致行为:有时执行后无任何输出(console.timeEnd未执行),程序直接退出,100次执行仅40次正常输出。
代码片段如下:
const net = require('node:net'); function handshake (peer, socket) { return new Promise ((resolve, reject) => { socket.setTimeout(3000); socket.on('timeout', () => (socket.destroy(), reject('timeout'))); socket.on('error', reject); socket.connect(peer.port, peer.ip, () => socket.write('Hi')); }); }; (async function () { // 'peer_list' is an array with 200 objects in this shape: // { ip:<string>, port:<number> } const peer_list = require('./peer-list.json'); let promises = []; for (let peer of peer_list) promises.push( handshake(peer, new net.Socket()) ); console.time('Timer'); const result = await Promise.allSettled(promises); console.timeEnd('Timer'); })();
说明:peer-list.json为包含200个IPv4/IPv6随机地址的数组,handshake中的Promise仅会被拒绝不会被resolve。运行环境为NodeJS v21.6.2、Windows 10 v22H2。发现设置较小超时值(100-400ms)时正常率约90%,更大超时值无明显变化。请问为何程序有时未完成执行就退出?
问题分析与解决方案
核心原因
程序提前退出的本质是Node.js事件循环在所有Promise完成前就耗尽了活跃异步任务,导致进程直接终止,具体触发场景包括:
- 部分套接字的
error或timeout事件触发时机早于事件监听器绑定完成,对应的Promise始终处于pending状态,Node.js检测到事件循环无活跃资源后直接退出,不会等待pending的Promise。 - Windows系统TCP栈的特殊行为放大了问题:部分无效地址的连接错误会被系统快速重置,事件触发速度超过了代码绑定监听器的速度,导致Promise永远无法被settle。
修复方案
1. 确保事件监听器优先绑定,避免事件丢失
修改handshake函数,先完成所有事件监听器的绑定,再执行connect操作,同时添加兜底逻辑确保Promise一定会被settle:
function handshake(peer, socket) { return new Promise((resolve, reject) => { // 标记是否已触发拒绝,避免重复调用 let isRejected = false; const rejectOnce = (reason) => { if (isRejected) return; isRejected = true; !socket.destroyed && socket.destroy(); reject(typeof reason === 'string' ? reason : reason.message); }; // 先绑定所有事件监听器 socket.setTimeout(3000); socket.on('timeout', () => rejectOnce('timeout')); socket.on('error', rejectOnce); // 添加close事件兜底,防止极端情况下事件丢失 socket.on('close', (hadError) => hadError && rejectOnce('closed with error')); // 最后执行连接操作 socket.connect(peer.port, peer.ip, () => socket.write('Hi')); }); }
2. 添加进程兜底逻辑,防止静默退出
在异步自执行函数中添加finally块释放资源,同时监听未处理的Promise拒绝和进程退出事件,便于排查问题:
(async function () { const peer_list = require('./peer-list.json'); const promises = []; const sockets = []; for (let peer of peer_list) { const socket = new net.Socket(); sockets.push(socket); promises.push(handshake(peer, socket)); } console.time('Timer'); try { const result = await Promise.allSettled(promises); console.timeEnd('Timer'); // 可选:输出测试统计 const failedCount = result.filter(r => r.status === 'rejected').length; console.log(`测试完成:共${result.length}个连接,${failedCount}个失败`); } finally { // 强制销毁所有套接字,释放系统资源 sockets.forEach(s => !s.destroyed && s.destroy()); } })().catch(err => console.error('未捕获全局错误:', err)); // 捕获未处理的Promise拒绝,避免进程静默退出 process.on('unhandledRejection', (reason) => { console.error('未处理的Promise拒绝:', reason); }); // 监听进程退出,排查是否存在未完成任务 process.on('exit', (code) => { console.log(`进程即将退出,退出码:${code}`); });
3. 限制并发连接数,避免系统资源竞争
Windows系统TCP栈对高并发连接的处理能力有限,限制并发数可以减少异常触发概率:
// 限制同时发起的连接数为20 const CONCURRENCY_LIMIT = 20; (async function () { const peer_list = require('./peer-list.json'); let results = []; console.time('Timer'); // 分批处理连接请求 for (let i = 0; i < peer_list.length; i += CONCURRENCY_LIMIT) { const batch = peer_list.slice(i, i + CONCURRENCY_LIMIT); const batchPromises = batch.map(peer => { const socket = new net.Socket(); return handshake(peer, socket); }); const batchResults = await Promise.allSettled(batchPromises); results.push(...batchResults); } console.timeEnd('Timer'); console.log(`测试完成:共${results.length}个连接,${results.filter(r => r.status === 'rejected').length}个失败`); })().catch(err => console.error(err));
验证效果
修改后,无论超时时间设置为多少,程序都会等待所有Promise完成后再退出,不会出现无输出直接终止的情况。兜底事件监听器也能帮助排查极端场景下的异常问题。
内容的提问来源于stack exchange,提问作者Tom Luchesi
相关产品推荐
相关产品推荐

