こんぶにのブログ

エンジニアという職業を通して学んだことを発信するブログです。

phpでソケット通信・Webサーバを学ぶ。htmlに埋め込まれた情報を全て取得できるようにする。

今回の目標

前回はhtmlファイルをブラウザに返してくれるところまで実装した。
今回の目標として、

  • htmlファイルに埋め込まれた画像を返却できるようにする。

  • htmlファイル・画像等ファイルの情報を全てブラウザで表示できるようにする。

  • リクエストしたファイルが見つからなかったら、404エラーを返す。

ソースコード

Server.php

<?php
// localhostのport8001を使用


class Server
{
    public $address = '127.0.0.1';
    public $port = '8001';
    // DocumentRoot
    const DOCUMENT_ROOT = 'C:\Repo\basic-web-app\http-test\document';
    const CONTENT_TYPE_TEXT_HTML = 'text/html';
    const CONTENT_TYPE_IMAGE_JPEG = 'image/jpeg';
    const CONTENT_TYPE_IMAGE_PNG = 'image/png';
    
    const CONTENT_TYPE_MAPPING_LIST = [
        'html' => self::CONTENT_TYPE_TEXT_HTML,
        'htm' => self::CONTENT_TYPE_TEXT_HTML,
        'jpg' => self::CONTENT_TYPE_IMAGE_JPEG,
        'jpeg' => self::CONTENT_TYPE_IMAGE_JPEG,
        'png' => self::CONTENT_TYPE_IMAGE_PNG,
    ];
    
    public function serverStart() : void 
    {
        // ソケットを作成。引数はphpに定義されている関数を使用。公式より。
        $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
        
        // ソケットに名前を付けます。名前がないとどのアドレスにも関連付けられていないとみなされ、何も出来ません。
        socket_bind($socket, $this->address, $this->port);
        
        // ソケット上で接続待ち(listen)します。クライアントが上記で作ったソケットに接続するのをひたすら待ちます。
        // 5個の処理用キューを用意しておきます。
        socket_listen($socket, 5);
        
        while(true) {
            // もしクライアントからソケットへの接続が来た場合、それを許可します。
            $acceptSocket = socket_accept($socket);
            
            $msg = "TcpServerに接続できました。\n";
            echo $msg;
            
            // ソケット接続先のクライアントから書き込まれたデータを読み取ります。
            $out = socket_read($acceptSocket, 2048);
            
            $fileName = 'text/server_recv.txt';
            $recvFile = fopen($fileName, 'w+');
            fwrite($recvFile, $out);
            
            // 今は上の書き込んだ文の最後のところにポインタがあるはずだから
            // ファイルポインタを一番前まで戻す。
            rewind($recvFile);
            
            // 受け取ったリクエストを1行ずつ解釈する
            // どのファイルがリクエストされているかを、httpリクエストの中から見つける
            $requestFileName = '';
            while($line = fgets($recvFile)) {
                if (substr($line, 0, 3) == 'GET') {
                    $back = strripos($line, " ");
                    $front = mb_strpos($line, " ");
                    $length = $back - $front;
                    // それぞれ±2にすることで、ついでに先頭のスラッシュも消す
                    $requestFileName = substr($line, $front + 2, $length - 2);
            
                    break;
                };
            }
            
            $path = fopen(self::DOCUMENT_ROOT . "\\" . $requestFileName, 'rb');
            
            // $pathのファイルがあるかどうかを基準に、ステータスラインを作成
            if ($path != false) {
                $responseLine = "HTTP/1.1 200 OK\r\n";
                // 取得したindex.htmlの中身をレスポンスボディとして送信する
                // 全部取得できるからこっちのが楽(どでかいのも全部取得しようとするけど)
                $responseBody = stream_get_contents($path);
            } else {
                $responseLine = "HTTP/1.1 404 Not' Found\r\n";
                // リクエストされたファイルがなかったらこっちをレスポンスボディにする
                $responseBody = "
                    <html>
                    <head>
                        <meta http-equiv=\"Content-Type\" content=\"text/html;charset=UTF-8\">
                    </head>
                    <body>
                        <h1>404 NotFound</h1>
                        <h2>{$requestFileName}が見つかりませんでした。</h2>
                    </body>
                    
                    </html>
                ";
                $statusNotFoundFlag = true;
            };
            
            // Content-Typeをリクエストされたファイルの拡張子から判断
            $requestFileName;
            $extension = pathinfo($requestFileName, PATHINFO_EXTENSION);
            var_dump($extension);
            // $headerContentType = "Content-type: text/html\r\n";
            
            if (isset($statusNotFoundFlag) && !$statusNotFoundFlag) {
                $extension = 'html';
            }
            
            $contentTypeMime = self::CONTENT_TYPE_MAPPING_LIST[$extension];
            
            $headerContentType =  "Content-type: " . $contentTypeMime . "\r\n";
            
            
            
            $responseHeader = '';
            $responseHeader .= "Host: modoki/0.1\r\n";
            $responseHeader .= "Connection: Close\r\n";
            // $headerContentType = "Content-type: image/jpg\r\n";
            
            $responseHeader .= $headerContentType;
            $responseHeader .= "\r\n";
            
            $httpResponse = $responseLine . $responseHeader . $responseBody;

            socket_write($acceptSocket, $httpResponse, strlen($httpResponse));
            
        }
        
        // 確立された接続を閉じます
        socket_close($acceptSocket);
        
        // ソケットそのものを終了
        socket_close($socket);
    }
}

大きな変更点としては、socket_acceptを無限ループにしている点。
こうすることで、ソケット通信を何度でも行える。
ソケットでhttpリクエストが送られてくるので接続承認→それ以下の処理を行う→もう一回リクエストが来るので…
をリクエストの数だけ繰り返す。
本来であれば、ここは並列処理にして、しまったほうがいいと思う。
しかし、windowsでのphpは並列処理のライブラリを導入するのがややめんどくさく、断念。
macであればpeclで簡単に出来そうであった。

リクエストされたファイルの拡張子の判断をおこなっている。
画像の拡張子であれば、httpレスポンスのContent-Typeを合わせたものに設定している。

ドキュメントルートにリクエストラインに指定されたファイルが無かったら、
レスポンスボディに404のhtmlを書き込むように設定。

Main.php

<?php
    require_once('Server.php');

    $server = new Server();
    $server->serverStart();
?>

もはや説明の必要は無さそうだが、念のため。
今までは毎回php TcpServer.phpとかコマンドを打ってやるのが結構めんどくさかった。
なので名前を分かりやすくかえたかったのと、ついでにphpの基礎も勉強しちゃおうってことで、
classファイルにしてみた。
やってることは、ただインスタンス化したServerClassの中のメソッドを実行しているだけ。
めっちゃシンプルにきれいに見えるようになった。

実行

また、ドキュメントルートにはindex.htmlファイルを作成し、中にはimgタグを書き込む。
同じディレクトリ内に適当な画像を置き、それを指定。
コマンドとしてはphp Main.phpを実行。
ブラウザではlocalhost:8001/index.htmlをURLに入力。
すると、index.htmlに載っているすべての情報が返ってくるはず。

404エラーのために、localhost:8001/indexxxx.htmlとか入力してみる。
べた書きした404用のメッセージがブラウザの画面に表示されるはずだ。

終わり

だいぶ駆け足になってしまった。
ハンズオンをメインで進めたいため、のちに追記出来たらしていきたい。
phpの並列処理が全然できなくて、一回挫折しかけましたが、
本質はそこじゃないと思い、無限ループで対応する形としました。
諦めも肝心。

メモ

ファイルポインタはそのファイルをどこまで読み込んだかを示す位置。
だから最初に戻さないと、最後まで読んだ位置からスタートしちゃう。

CR LF CRは行頭までカーソルを戻す(キャリッジリターン
LFは真下にカーソルを落とす(ラインフィード
つまりこれが合わさって改行になる
ソースで表すと\r\n 特にキャリッジリターンはかっこいいので覚えておこうと思った。

extension=mbstringをコメント外さないとmb_strstrが使えなかった

並行処理と並列処理は違う
並行は一人の人(cpu)がいくつも作業する
あたかも、複数の処理を並行しているように見せながら、実際に処理してるのは一つだけ
並列は複数の人(cpu)がそれぞれ作業する

requireでファイルを読み込む
require_onceは一回だけ読み込む

phpの閉じタグはphpだけのソースなら不要