nodeで空いているポートを見つける

サーバのテストをするときなどに未使用のポートを使ってテストコードを走らせたい、というときがあって、PerlだとTest::TCPにempty_portというのがあって簡単に取得出来る。

$ perl -MTest::TCP -E 'say Test::TCP::empty_port'
10256

引数を与えない場合は毎回違う値になるけれど、基本的に必ず空いているポート番号が返ってくる。


nodeでも同じようなのがあればいいな、と思ったのだけど多分ないのでTest::TCPを参考に自分で書いてみた。

exports.empty_port = function(callback) {
    port = 10000 + Math.floor(Math.random() * 1000);

    var net = require('net');
    var socket = new net.Socket();
    var server = new net.Server();

    socket.on('error', function(e) {
        try {
            server.listen(port, '127.0.0.1');
            server.close();
            callback(port);
        } catch(e) {
            loop();
        };
    });
    function loop() {
        if (port++ >= 20000) {
            callback(null);
            return;
        }

        socket.connect(port, '127.0.0.1', function() {
            socket.destroy();
            loop();
        });
    };
    loop();
};

10000〜11000くらいで初期値を決定、net.Socketで127.0.0.1のそのportに繋げるか試し、繋がってしまった場合は既に他のプロセスがlistenしているということなので切って次の番号で再試行。繋がらなかった場合は今度は127.0.0.1のそのportでnet.Serverがlistenできるか試してみて、問題なければcloseした後そのportをcallbackで返す。これが成功するまではport番号をインクリメントさせながら再試行を繰り返す。20000くらいまで試しても見つからなかったら探索を諦めてnullを返すようにしてみた。
Test::TCPのempty_portは同期的に処理を行うようになっているけど、nodeではsocket.connectの際の処理が非同期になるので試行ループをfor文で回して、のようなことができなかった。ので必然的にempty_port関数自体にcallbackを渡して空きportを得る形になる。

$ node -e 'require("./empty_port").empty_port(function(port) { console.log(port) })'
undefined
10119

追記

@さんからツッコミをいただきました。ありがとうございます!

exports.empty_port = function(callback) {
    port = 10000 + Math.floor(Math.random() * 1000);

    var net = require('net');
    var socket = new net.Socket();
    var server = new net.Server();

    socket.on('error', function(e) {
        try {
            server.listen(port, '127.0.0.1');
            server.close();
            callback(null, port);
        } catch(e) {
            loop();
        };
    });
    function loop() {
        if (port++ >= 20000) {
            callback(new Error('empty port not found'));
            return;
        }

        socket.connect(port, '127.0.0.1', function() {
            socket.destroy();
            loop();
        });
    };
    loop();
};

callbackに渡す第1引数は正常時はnull, 見つからなければErrorオブジェクト、第2引数に見つかった空きポートを渡すようにしました。