2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【入門】PHPでWebサーバーを立ててみた リベンジ

Posted at

前回の反省から始まる

前回とはいっても昨日のことなのだけれど、なんとも愚かな投稿をしてしまった...。
恥ずかしいけど戒めのために残しておく。

犯した過ち

  • ワーカープロセスが再生成されていなかった
    for ($i = 0; $i < $workerCount; $i++) {
       $pid = pcntl_fork();
       if ($pid === 0) {
           workerProcess($socket, $baseDir);
           exit;
       }
    }
       
    for ($i = 0; $i < $workerCount; $i++) {
       pcntl_wait($status);
    }
    
    全てのプロセスがなんらかの理由で終了すると新しい処理をするプロセスがいなくなる。
  • ソケットタイムアウト設定がない
    stream_socket_acceptにタイムアウトを指定していなかったためクライアントが接続していない際に無制限に待機をしていた。
  • ストリームを利用したファイル送信
    静的ファイルの処理にfile_get_contetnsを利用していたため複数同時のリクエストを処理する際にメモリ消費量が増えてしまった。
  • 頭が悪った

理論最大値:360リクエスト(30秒 × 4プロセス × 3サーバー)

siege -c10 -t30S http://localhost:8080

実行コマンドから最大は300。-c12としなければならなかった。
126hitsという不甲斐ない結果に終わったけどな。

リベンジ

気がついたら関数作ってしまって型宣言も消したつもりが残っていた。
甘えや嫉妬やずるさを抱えながら俺は生きている。

捨てる

<?php

$host = '0.0.0.0';
$port = $argv[1] ?? 8080;
$baseDir = rtrim(realpath(__DIR__ . '/public'), '/');
$timeout = 10;
$workerCount = 5;

$socket = stream_socket_server("tcp://{$host}:{$port}", $errorCode, $errorMessage);
if (!$socket) {
    die("Server startup failure: $errorMessage ($errorCode)\n");
}

echo "Server Listening on http://{$host}:{$port}\n";

$activeWorkers = 0;
for ($i = $workerCount; 0 <= --$i;) {
    $pid = pcntl_fork();
    if ($pid === 0) {
        while (true) {
            $client = @stream_socket_accept($socket, $timeout);
            if (!$client) continue;

            stream_set_timeout($client, $timeout);
            handleRequest($client, $baseDir);
        }
    } else {
        ++$activeWorkers;
    }
}

while (true) {
    $status = null;
    $exitedPid = pcntl_wait($status, WNOHANG);
    if (0 < $exitedPid) {
        --$activeWorkers;
        if ($activeWorkers < $workerCount) {
            $newPid = pcntl_fork();
            if ($newPid === 0) {
                while (true) {
                    $client = @stream_socket_accept($socket, $timeout);
                    if (!$client) continue;
                    stream_set_timeout($client, $timeout);

                    handleRequest($client, $baseDir);
                }
            }

            if (0 < $newPid) {
                $activeWorkers++;
            }
        }
    }
    sleep(1);
}

function handleRequest($client, $baseDir)
{
    $request = '';
    while (($line = fgets($client)) !== false) {
        $request .= $line;
        if (trim($line) === '') {
            break;
        }
    }

    if (preg_match('/Content-Length: (\d+)/i', $request, $matches)) {
        $request .= fread($client, (int) $matches[1]);
    }

    $lines = explode("\r\n", $request);
    $headers = [];
    $body = '';
    $isBody = false;
    foreach ($lines as $line) {
        if ($isBody) {
            $body .= $line;
        } elseif (trim($line) === '') {
            $isBody = true;
        } elseif (str_contains($line, ': ')) {
            [$key, $value] = explode(': ', $line, 2);
            $headers[$key] = $value;
        } elseif (str_contains($line, ' ')) {
            [$method, $uri] = explode(' ', $line, 3);
            $headers['REQUEST_METHOD'] = $method;
            $headers['REQUEST_URI'] = $uri;
        }
    }

    $uri = $headers['REQUEST_URI'] ?? '/';
    $method = $headers['REQUEST_METHOD'] ?? 'GET';

    $queryParams = [];
    if ($queryString = parse_url($uri, PHP_URL_QUERY)) {
        parse_str($queryString, $queryParams);
    }

    $_GET = $queryParams;
    $_REQUEST = $_GET;

    if ($method === 'POST' || $method === 'PUT' || $method === 'PATCH') {
        $contentType = $headers['Content-Type'] ?? '';
        if (str_contains($contentType, 'application/x-www-form-urlencoded')) {
            parse_str($body, $_POST);
        } elseif (str_contains($contentType, 'application/json')) {
            $_POST = json_decode($body, true) ?? [];
        } else {
            $_POST = [];
        }

        $_REQUEST = array_merge($_REQUEST, $_POST);
    }

    $filePath = realpath($baseDir.parse_url($uri, PHP_URL_PATH));
    if (!$filePath || !str_starts_with($filePath, $baseDir)) {
        fwrite($client, "HTTP/1.1 404 Not Found\r\nContent-Length: 13\r\n\r\n404 Not Found");
        fclose($client);
        return;
    }

    if (!is_readable($filePath)) {
        fwrite($client, "HTTP/1.1 500 Internal Server Error\r\nContent-Length: 21\r\n\r\nInternal Server Error");
        fclose($client);
        return;
    }

    if (is_dir($filePath)) {
        $filePath = rtrim($filePath, '/').'/index.php';
    }

    if (pathinfo($filePath, PATHINFO_EXTENSION) === 'php') {
        if (ob_get_level() === 0) ob_start();
        include $filePath;
        $output = ob_get_clean();
        $response = "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=UTF-8\r\nContent-Length: ".strlen($output)."\r\n\r\n".$output;
        fwrite($client, $response);
        fclose($client);
        return;
    }

    $mimeType = mime_content_type($filePath) ?: 'application/octet-stream';
    $fileSize = filesize($filePath);

    fwrite($client, "HTTP/1.1 200 OK\r\n");
    fwrite($client, "Content-Type: {$mimeType}\r\n");
    fwrite($client, "Content-Length: {$fileSize}\r\n");
    fwrite($client, "\r\n");

    $fp = fopen($filePath, 'rb');
    if ($fp !== false) {
        stream_copy_to_stream($fp, $client, $fileSize);
        fclose($fp);
    }

    fclose($client);
}

しっかり反省してこうした。
さて,リベンジです。以下を実行していく。

<?php
echo 'OK';
sleep(1);

参る

まずは分散なしで確認する

  • プロセス数: 5
  • 分散なし
siege -c5 -t10S http://127.0.0.1:8080

Transactions:		          50 hits
Availability:		      100.00 %
Elapsed time:		       10.22 secs
Data transferred:	        0.00 MB
Response time:		     1903.60 ms
Transaction rate:	        4.89 trans/sec
Throughput:		            0.00 MB/sec
Concurrency:		        9.31
Successful transactions:      50
Failed transactions:	       0
Longest transaction:     2010.00 ms
Shortest transaction:	 1010.00 ms
  • 理論値最大: 50
  • 実行結果: 50

分散する

  • プロセス数: 5
  • 分散数: 3
siege -c15 -t10S http://127.0.0.1:8080
Transactions:		         150 hits
Availability:		      100.00 %
Elapsed time:		       10.80 secs
Data transferred:	        0.00 MB
Response time:		     1012.47 ms
Transaction rate:	       13.89 trans/sec
Throughput:		            0.00 MB/sec
Concurrency:		       14.06
Successful transactions:     150
Failed transactions:	       0
Longest transaction:	 1030.00 ms
Shortest transaction:	 1000.00 ms
  • 理論値最大: 150
  • 実行結果: 150

やったか。
では前回126hitsという不甲斐ない結果に終わってしまった以下を実行する。

siege -c10 -t30S http://localhost:8080
siege -c10 -t30S http://localhost:8080

Transactions:		         300 hits
Availability:		      100.00 %
Elapsed time:		       30.59 secs
Data transferred:	        0.00 MB
Response time:		     1010.90 ms
Transaction rate:	        9.81 trans/sec
Throughput:		            0.00 MB/sec
Concurrency:		        9.91
Successful transactions:     300
Failed transactions:	       0
Longest transaction:	 1020.00 ms
Shortest transaction:	 1000.00 ms
  • 理論値最大: 300
  • 実行結果: 300

やりました。
読んでくれてありがとう。

2
1
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
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?