ryoakg
12/25/2015 - 12:09 PM

PHP でチャットサーバー

PHP でチャットサーバー

<?php

error_reporting(E_ALL);

set_time_limit(0);

ob_implicit_flush();

declare(ticks = 1);

$sock = null;

function main() {
    global $sock;
    // シグナルハンドラのセット
    pcntl_signal(SIGTERM, "sig_handler");
    pcntl_signal(SIGHUP,  "sig_handler");
    pcntl_signal(SIGUSR1, "sig_handler");

    // デーモン化
    // daemonize();

    $address = '127.0.0.1';
    $port = 10000;

    // サーバーソケット作成
    $sock = server_socket($address, $port);
    // 接続待ち受けループに入る
    accept_loop($sock);
    socket_close($sock);
}

// シグナルハンドラ関数
function sig_handler($signo) {
    global $sock;

    switch ($signo) {
        case SIGTERM:
            // シャットダウンの処理
            socket_close($sock);
            exit;
            break;
        case SIGHUP:
            // 再起動の処理
            break;
        case SIGUSR1:
            echo "SIGUSR1 を受け取りました...\n";
            socket_close($sock);
            break;
        default:
        // それ以外のシグナルの処理
    }
}

function daemonize() {
    // PHPのプロセスをデーモン状態にする
    $pid = pcntl_fork();
    if ($pid < 0) {
        die("フォーク失敗\n");
    } else if ($pid > 0) {
        // 親プロセス
        exit();
    }
    // 子プロセス
    // 制御端末の切り離し
    $sid = posix_setsid();
    if ($sid < 0) {
        die("セッションを生成できませんでした。\n");
    }
    // セッションリーダーでなくする
    // 2回めの fork()
    $pid2 = pcntl_fork();
    if ($pid2 < 0) {
        die("2回めのフォーク失敗\n");
    } else if ($pid2 > 0) {
        // 子プロセス
        exit();
    }
    // 孫プロセス
    // ルートディレクトリをカレントディレクトリへ
    chdir("/");

    // 標準入出力を閉じる
    fclose(STDIN);
    fclose(STDOUT);
    fclose(STDERR);
    // デーモンになった!
}

function server_socket($address, $port) {
    if (($sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP)) === false) {
        die("socket_create() に失敗\n");
    }

    if (socket_bind($sock, $address, $port) === false) {
        die("socket_bind() に失敗\n");
    }

    if (socket_listen($sock, 5) === false) {
        die("socket_listen() に失敗\n");
    }

    return $sock;
}

// 待ち受けループ
function accept_loop($sock) {
    $clients = [$sock];

    while (true) {
        $read = $clients;
        $write = NULL;
        $except = NULL;
        if (socket_select($read, $write, $except, NULL) < 1) {
            continue;
        }
        // 接続受け付け
        if (in_array($sock, $read)) {
            // 待ち受けソケットに新しい接続が来た
            $newsock = socket_accept($sock);
            $clients[] = $newsock;

            $msg = "\nチャットサーバーに接続しました。\n" .
                    "やめるには 'quit' と打ってください。\n";
            socket_write($newsock, $msg);
            $key = array_search($sock, $read);
            unset($read[$key]);
        }

        // メッセージ受信
        foreach ($read as $read_sock) {
            $data = @socket_read($read_sock, 1024, PHP_NORMAL_READ);
            if ($data === false) {
                $key = array_search($read_sock, $clients);
                unset($clients[$key]);
                echo "client disconnected.\n";
                continue;
            }
            $data = trim($data);
            if (empty($data)) {
                continue;
            }
            if ($data == 'quit') {
                socket_close($read_sock);
                $key = array_search($read_sock, $read);
                echo "quit key = $key\n";
                unset($clients[$key]);
                continue;
            }
            $data = $data . "\n"; // 改行を再び付加する
            // 他のクライアントに配信
            broad_cast($data, $clients, $read_sock, $sock);
        }
    }
}

// メッセージ配信処理
function broad_cast($message, $clients, $sender, $server_sock) {
    foreach ($clients as $sock) {
        if ($sock === $sender || $sock === $server_sock) {
            // 送信者自身と待受ソケットは除外
            continue;
        }
        socket_write($sock, $message, strlen($message));
    }
}

main();