LoginSignup
2
4

More than 5 years have passed since last update.

プリフォークの TCP サーバでプロセス数を超えた接続に RST を返す

Posted at

下記のコードのようにプロセスをプリフォークして子プロセスで accept した場合、プロセス数が2なので同時に処理できるクライアントは2個までということになりますが、接続するだけなら2よりも多くできます。

Backlog という確立済みの TCP 接続をカーネルが保持しておいて、accept でユーザー空間にソケットのファイルディスクリプタを返す、という仕組みがあるためで、ユーザー空間で accept しなくても Backlog の数まではクライアントからの接続が受け入れられます。

なので、接続数が2を超えた場合にクライアントから見ると、接続できる、データも送信できる(サーバ側の受信バッファがいっぱいになるまでは)、けどなにも応答が帰ってこない、という状態になります。

require_once __DIR__ . '/vendor/autoload.php';

use Parallel\Prefork;

function main()
{
    $pp = new Prefork(array(
        'max_workers'  => 2,
    ));

    init();

    while ($pp->signalReceived() !== SIGTERM) {
        if ($pp->start()) {
            continue;
        }
        work();
        $pp->finish();
    }
    $pp->waitAllChildren();
}

$listenSocket = null;
$listenPort = 12345;

function init()
{
    global $listenSocket;
    global $listenPort;

    $listenSocket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
    socket_set_option($listenSocket, SOL_SOCKET, SO_REUSEADDR, 1);
    socket_set_option($listenSocket, SOL_SOCKET, SO_REUSEPORT, 1);
    socket_bind($listenSocket, "0.0.0.0", $listenPort);
    socket_listen($listenSocket, 1);

    printf("server listen %d\n", $listenPort);
}

function work()
{
    global $listenSocket;
    global $listenPort;

    printf("start worker process %d\n", posix_getpid());

    for (;;) {
        $socket = socket_accept($listenSocket);

        socket_getpeername($socket, $addr, $port);
        printf("connected %s:%s\n", $addr, $port);

        for (;;) {
            $str = socket_read($socket, 1024);
            if (strlen($str) === 0) {
                break;
            }
            printf("receive %d byte\n", strlen($str));
            while (strlen($str) > 0) {
                $len = socket_write($socket, $str);
                $str = substr($str, $len);
            }
        }

        printf("disconnect %s:%s\n", $addr, $port);
        $socket = null;
    }
}

main();

接続数が上限を超えたら直ちにクライアントに RST を応答するようにはできないものか、と思って調べていたら下記を発見しました。

こんなのがありなのかどうかわかりませんが、次のように accept したら直ぐにリッスンソケットをクローズするようにすれば、接続数が2を超えるとクライアントに直ちに RST が返されるようになります。

require_once __DIR__ . '/vendor/autoload.php';

use Parallel\Prefork;

function main()
{
    $pp = new Prefork(array(
        'max_workers'  => 2,
    ));

    while ($pp->signalReceived() !== SIGTERM) {
        if ($pp->start()) {
            continue;
        }
        work();
        $pp->finish();
    }
    $pp->waitAllChildren();
}

$listenSocket = null;
$listenPort = 12345;

function work()
{
    global $listenSocket;
    global $listenPort;

    printf("start worker process %d\n", posix_getpid());

    for (;;) {
        $listenSocket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
        socket_set_option($listenSocket, SOL_SOCKET, SO_REUSEADDR, 1);
        socket_set_option($listenSocket, SOL_SOCKET, SO_REUSEPORT, 1);
        socket_bind($listenSocket, "0.0.0.0", $listenPort);
        socket_listen($listenSocket, 1);
        $socket = socket_accept($listenSocket);
        socket_close($listenSocket);
        $listenSocket = null;

        socket_getpeername($socket, $addr, $port);
        printf("connected %s:%s\n", $addr, $port);

        for (;;) {
            $str = socket_read($socket, 1024);
            if (strlen($str) === 0) {
                break;
            }
            printf("receive %d byte\n", strlen($str));
            while (strlen($str) > 0) {
                $len = socket_write($socket, $str);
                $str = substr($str, $len);
            }
        }

        printf("disconnect %s:%s\n", $addr, $port);
        $socket = null;
    }
}

main();

つまり、接続数が2になるとリッスンソケットが全部閉じられるのでリッスンしていないことになり、それ以上の接続に対しては RST が返されるようになります。

2
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
4