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

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完成前就耗尽了活跃异步任务,导致进程直接终止,具体触发场景包括:

  1. 部分套接字的error或timeout事件触发时机早于事件监听器绑定完成,对应的Promise始终处于pending状态,Node.js检测到事件循环无活跃资源后直接退出,不会等待pending的Promise。
  2. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 13:03:14