こんぶにのブログ

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

formでデータを送る時の違い。ファイルをアップロードするにはentypeを指定する必要あり。

POSTデータを送信

送るものによって色々種類を分ける必要あり

formに何も指定しなかった場合

input type fileで入力したデータのファイルの中身が送信されない。
だからファイルのアップロードには使えない。

chromeの開発者モードでcontent-typeを見てみるとこんな感じ

application/x-www-form-urlencoded```  
という謎のコンテンツタイプが追加されている。  
これがformに特に何も指定せずにsubmitした時のデフォルトらしい。  
以下に詳細。  

[https://zenn.dev/bigen1925/books/introduction-to-web-application-with-python/viewer/post-parameters#application%2Fx-www-form-urlencoded-%E3%81%AB%E5%AF%BE%E5%BF%9C%E3%81%99%E3%82%8B:title]  
formでデータを送る時の区切りの文字列の扱いだったり、改行の扱いだったりを定義している。  
要は、サーバがちゃんとpostデータの中身なんだなって認識するためのデフォルトの複雑なことが何もない時用の諸々のルール。  


###formのentypeにmultipart/form-dataを指定した場合
試しに送信してみるとbodyがこんな感じになっていた。  
[f:id:k0nbuni:20231126181103p:plain]  
リクエストのヘッダにはこんなものが追加されていた。  

multipart/form-data; boundary=----WebKitFormBoundaryK8oEoBaqC4Xlrzay```
boundary、つまりデータとデータの区切りはこの文字列ですよ~というのをヘッダに入れ込んでいる。
こうすることでファイルが始めてアップロードできるようになるらしい。
この構造にすると、たくさんの複雑なデータを持つことが出来る。
一つ上で紹介したデータの送り方だと、ひとつひとつ&とか=で区切るから、あんまり複雑なデータが送れない。
で、ここで言う複雑なデータっていうのがファイルのバイナリ。
バイナリっていうのはファイルをぐっちゃぐちゃな文字列にしたやつ。こいつは人間には理解できないが、コンピュータには理解できる。
コンピュータが読めば、「あ、これはこういうファイルなんだな」となる。

【Docker】Laravel SailでDockerコンテナを複数起動したけど、めちゃくちゃ難しかった話【未解明】

先日、Laravel SailでDockerコンテナを複数起動し、片方のコンテナの中に内蔵されているmysqlにアクセスするということをした。
大変に時間がかかってしまったので、ここに記しておこうと思った。
ちなみに、未だに仕組みが良く分かっていない。
こんな感じなんだろうという理解なので、あまり参考にしすぎないでほしい。

やったこと

以下のsail upでのインストールを2つのプロジェクトで同じように行った。
Laravel Sail 9.x Laravel
最初に起動した方にはmysqlも含めた。
2つ目に起動するコンテナにはmysqlを含めず、最初に起動したほうのmysqlコンテナにアクセスする、というものだ。
この時点でDockerの仕組みをあまり理解してない自分は、??状態だった。

一つ目のdocker-compose.yml

# For more information: https://laravel.com/docs/sail
version: '3'
services:
    laravel.test:
        build:
            context: ./vendor/laravel/sail/runtimes/8.1
            dockerfile: Dockerfile
            args:
                WWWGROUP: '${WWWGROUP}'
        image: sail-8.1/app
        extra_hosts:
            - 'host.docker.internal:host-gateway'
        ports:
            - '${APP_PORT:-80}:80' //.envにAPP_PORTがあればそれを、なければホストの80番でアプリのコンテナを起動。dockerの80番へポートフォワーディング。
            - '${VITE_PORT:-5173}:${VITE_PORT:-5173}'
        environment:
            WWWUSER: '${WWWUSER}'
            LARAVEL_SAIL: 1
            XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}'
            XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}'
        volumes:
            - '.:/var/www/html'
        networks:
            - sail
        depends_on:
            - mysql
    mysql:
        image: 'mysql/mysql-server:8.0'
        ports:
            - '4306:3306'
        environment:
            MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}'
            MYSQL_ROOT_HOST: "%"
            MYSQL_DATABASE: '${DB_DATABASE}'
            MYSQL_USER: '${DB_USERNAME}'
            MYSQL_PASSWORD: '${DB_PASSWORD}'
            MYSQL_ALLOW_EMPTY_PASSWORD: 1
        volumes:
            - 'sail-mysql:/var/lib/mysql'
            - './vendor/laravel/sail/database/mysql/create-testing-database.sh:/docker-entrypoint-initdb.d/10-create-testing-database.sh'
        networks:
            - sail
        healthcheck:
            test: ["CMD", "mysqladmin", "ping", "-p${DB_PASSWORD}"]
            retries: 3
            timeout: 5s
networks:
    sail:
        driver: bridge
        name: app-1
volumes:
    sail-mysql:
        driver: local

起動するときは0.0.0.0:80。ローカルでも80で動いてるから。

二つ目のdocker-compose.yml

version: '3'
services:
    laravel.test:
        build:
            context: ./vendor/laravel/sail/runtimes/8.2
            dockerfile: Dockerfile
            args:
                WWWGROUP: '${WWWGROUP}'
        image: sail-8.2/app
        extra_hosts:
            - 'host.docker.internal:host-gateway'
        ports:
            - '${APP_PORT:-8081}:80' //ホストの8081でアプリのコンテナ起動、dockerの80番に処理はお願いする
            - '${VITE_PORT:-5174}:${VITE_PORT:-5174}'
        environment:
            WWWUSER: '${WWWUSER}'
            LARAVEL_SAIL: 1
            XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}'
            XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}'
            IGNITION_LOCAL_SITES_PATH: '${PWD}'
        volumes:
            - '.:/var/www/html'
        networks:
            - sail
networks:
    sail:
        driver: bridge
        external: true
        name: app-1 // 最初に起動したほうを指定する ??
volumes:
    sail-mysql:
        driver: local

起動するときは0.0.0.0:8001。ローカルでは8001で動いてるから。

分からないこと

コメントにしているポートフォワーディングの部分が良く分かってない。
ふたつのアプリがdockerの80番へリクエストを送っているのに、どうしてアプリ1と2で別の結果が返ってくるんだろう?
docker:80番は、Webサーバだから来たリクエストを認識して、そのパスのファイルを返すなり、routeにcontrollerの処理が書かれていればその先にお願いをするはず。
そこまではいい。ここからが分からん。
ローカル:80番がdocker80番にリクエストを送るときはこんな感じだろうか。
・ブラウザで0.0.0.0:80を入力
・ホストの80番が「お、おれの出番だな!ん?dockerの80番に処理はお願いするのね、了解!」
・docker80番「お、ローカルの80から依頼が来てる。なるほど80番からこのリクエストが来てるってことはこっちのアプリのこの処理をすればいいのね。 はい、生成できたからページのデータ返すよ~」
・ブラウザ「ページのデータが返ってきた!」
というイメージをしている。
だから、ホストの8001でリクエストをするときも同じはずだ。
・ブラウザで0.0.0.0:8001を入力
・ホストの8001番が「お、おれの出番だな!ん?dockerの80番に処理はお願いするのね、了解!」
・docker80番「お、ローカルの8001から依頼が来てる。なるほど8001番からこのリクエストが来てるってことはこっちのアプリのこの処理をすればいいのね。 はい、生成できたからページのデータ返すよ~」
・ブラウザ「ページのデータが返ってきた!」
こうなるんだろう。

ただ、その設定をしているのはどこなんだろう?
dockerの80番はどうやって、どのアプリにどの処理を依頼するのかを判断しているのか。
「ホスト8001で動いている者です!このパスにあるこのフォルダ名にあるものに処理を依頼してください!」という挨拶はどこに書いてあるんだろう。
リクエストヘッダーのHostとかに色々パスとか情報が入ってて、docker80番はそれを元に判別しているんだろうか。
う~ん。。。dockerの仕組みが分かっていないって話なのか、そもそもWebサーバーへの理解が不足しているのか。
多分、docker80番で動いてるのはphpのビルトインサーバなのかな。

phpの抽象クラス、インターフェース、namespaceについてざっと復習

抽象クラス

abstract classで定義するやつ。
基本はclassで、継承元になる。
abstractで定義されたメソッドは、継承先では絶対に実装しなければならない。
abstractが付いていないメソッドなら、実装しなくてもオッケー。
このメソッドは絶対実装したいよね~っていうのがあって、継承を使いたいとき。

インターフェース

抽象メソッド(名前だけ定義されてて、中身がないやつ)が書かれているファイル。
クラスではない。
ただ、使う時はクラスの継承みたいにする。
継承の時はextendsだったところをimplementsとする。
継承ではなく、実装と呼ぶ。
中身のないメソッドの名前だけを引き継いで、中身を作っていくから、実装なのだろう。
継承ってほど元クラスの動きを使いたいわけではないが、決まったメソッドを必ず実装したい時に使う。
なお、継承も実装も、行った場合は親クラスの型になる模様。

namespace

classをぶち込むフォルダみたいなのを勝手にphpが頭の中で作ってくれる。
例えば、class dog { }みたいなのが定義されたphpファイルがあったとする。
このdogクラスをnamespaceで定義した場所に保存してくれる。
何が良いかというと、同じクラス名のものがあったときにnamespaceフォルダを分けて保存すれば、同じ名前のまま呼び出したいほうを呼び出せる。
使い方としてはnamespace doubutsu\inuとclass dogを定義したファイルに書いておく。
で、dogをインスタンス化したいファイル内で、use doubutsu\inuと書いてみる
で、インスタンス化するときにいつも通りnew dog()とすればいい。
このときのdogはdoubutsu\inuにあるdogのことね、とphpが解釈してくれる。
だから、use doubutsuとだけしても行ける。
new \inu\dog()と、相対パス的に書けばよい。

phpの抽象クラス、インターフェース、namespaceについてざっと復習

抽象クラス

abstract classで定義するやつ。
基本はclassで、継承元になる。
abstractで定義されたメソッドは、継承先では絶対に実装しなければならない。
abstractが付いていないメソッドなら、実装しなくてもオッケー。
このメソッドは絶対実装したいよね~っていうのがあって、継承を使いたいとき。

インターフェース

抽象メソッド(名前だけ定義されてて、中身がないやつ)が書かれているファイル。
クラスではない。
ただ、使う時はクラスの継承みたいにする。
継承の時はextendsだったところをimplementsとする。
継承ではなく、実装と呼ぶ。
中身のないメソッドの名前だけを引き継いで、中身を作っていくから、実装なのだろう。
継承ってほど元クラスの動きを使いたいわけではないが、決まったメソッドを必ず実装したい時に使う。
なお、継承も実装も、行った場合は親クラスの型になる模様。

namespace

classをぶち込むフォルダみたいなのを勝手にphpが頭の中で作ってくれる。
例えば、class dog { }みたいなのが定義されたphpファイルがあったとする。
このdogクラスをnamespaceで定義した場所に保存してくれる。
何が良いかというと、同じクラス名のものがあったときにnamespaceフォルダを分けて保存すれば、同じ名前のまま呼び出したいほうを呼び出せる。
使い方としてはnamespace doubutsu\inuとclass dogを定義したファイルに書いておく。
で、dogをインスタンス化したいファイル内で、use doubutsu\inuと書いてみる
で、インスタンス化するときにいつも通りnew dog()とすればいい。
このときのdogはdoubutsu\inuにあるdogのことね、とphpが解釈してくれる。
だから、use doubutsuとだけしても行ける。
new \inu\dog()と、相対パス的に書けばよい。

私はいつまでSESを続ければいい?この先に何がある?

SESをいつまで続けるんだろうか。
もう業界に入って結構経っている。
いずれは独立、いずれは転職。
まだ早い、まだ技術が足りない、フリーで活躍しているあの人に比べると自分はまだまだ・・・
そんな言い訳を重ね、早数年。
結局自分の人生は何も変わらないままだ。

変えたい自分と変えたくない自分

今の自分の生活を変えたい、そう強く願う自分がいる。
今の給料は私の年齢に対して、あまりに低い。
同世代の中なら、トップレベルに低いだろう。
これだけ休日に勉強する業界なんて他にない。
なのに、これしか給料はもらえない。
時間もない、金もない、楽しくない。
最悪の3コンボだ。
こんな生活変えたいに決まってる。
せめてどれか一つは欲しい。
時間か、金か、楽しさか。
現状、どれもそんなにない。

こんな状況でも、変えたくないと思っている自分がどこかにいるはずだ。
だから、行動をしないのだろう。
先に進めば何があるか分からない不安に敗北し、今の悪い状況に適応して落ち着いてしまっている自分がいるのだろう。
意味が分からない。
これより悪いことなんてそうそうないように思う。
それでもSESを続けている。
自分が不思議でたまらない。
ahoか。

会社に不満があるわけじゃない あーだこーだ言っているが、会社が嫌いとかそういうことはない。
みんな良い人だし、学べることも多い。
それでも、今のままでは自分は何も変わらないと思う。
自分の人生が何も変わらないと思う。
良い方にも悪い方にも行かないと思う。
それをよしと出来る人間であれば良かったのだが、そうではないようだ。

退職

やはり、退職か。
というかそれしかないと思う。
今の会社で十年後も続ける気はない。
なら、今やめたほうがいい。
この先には何もない。
私が望むものは何もない。
今年度いっぱいで退職できるよう、頑張ろう。
今の給料のまま死んだら、俺の人生なんだったんだろうってなる。

継承・classについて【phpを学び直す】

phpのclassについて

phpのclassはあくまで、何かを作成するための型。
だから、これを使って何かを作る必要がある。
つまり、classだけでは何かを作ることができず、classを使用する何か別のファイルだったり、処理が必要。

classの使い方

まずはどこかでインスタンス化をする。
$class = new Class(); その上で、作成した$classに対して色々やっていく。
また、class{}内に記述したメソッド内で、class内の他のメソッドを使う時は
$this->メソッド()とする
変数名や、メソッド名をstaticとすると、上記のようにインスタンス化しなくても使えるようになる。

継承・オーバーライド

phpはclassからclassの能力を引き継げる。
プロパティやメソッドをまるごと引き継げる。
使い方としてはextendsとするだけ。
あとは子クラスをインスタンス化して、親クラスの方にしか定義されていないはずのメソッドとかを使うことが出来る。
また、オーバーライドは全く同じメソッド名を作ればよい。
その際、オーバーライドしたメソッド内で親クラスの本来のメソッドの力を使いたいこともあるはず。
そんなときはparent::メソッド名とすればよい。

【エンジニア3年目でやっと分かった】リダイレクトとURLエンコードの仕組みと必要性

前回

以下の部分まで進んだ。

konbuni.net

リダイレクト

実際にやってみる

今まではあるページに遷移するもの、と認識。
もっと厳密にいうと、
「レスポンスヘッダーのLocationに記されたURLにファイルを取得しに行っている」
ということらしい。

試しにchromeの開発者モードを開き、以下の二つのURLにアクセスしてみる。

https://kmaebashi.com/programmer/webserver

https://kmaebashi.com/programmer/webserver/

/が付いていないほうはリダイレクト(301)が発生。

付いていないほうは発生せず、一発で200 OKが返ってくる。

何故こうなるか・必要か

webserverの後に/をつけないと、Webサーバー側ではディレクトリだと認識されない。
つまり、webserverという名前のファイルをください!という命令だとWebサーバーは解釈する。
しかし、そんなファイルはないのでこのままなにもしなければ、404Not Foundとなってしまう。
そこで、もしwebserverというファイルが無くても、そういう名前のディレクトリ(フォルダ)があった場合は、
自動的にそっちのURLを見てもらうようにする。
すると、利用者は特に何もせずに、勝手にサーバー側でファイルがありそうなディレクトリを見てくれて、
ファイルがあれば返してくれる。
(特にファイル名を指定しないまま末尾/へリダイレクトした場合は、このファイル名のファイルを返すというのを設定できる) つまり、404にすることなく、良い感じに画面を返してくれる仕組みなわけだ。
これが悪用されて、変なサイトに飛ばされたりもするが、私は善の使い方を心がけようと思った。
ちなみに、TopページのURLの場合(例なら、https://kmaebashi.com)、
末尾に/はつけないのが普通。
何故なら、/をつけなかった場合に返却するファイルをWebサーバ側で設定するから。
これがTopページじゃない場合だと、/はつけたほうが良い。
何故なら、/なしだと一回リダイレクトを挟んで、/ありに移動するという二度手間になって、サーバーに負担がかかるから。

URLエンコード

存在意義

URLには基本的にアルファベットを入力する。
しかし、リクエストしたいファイル名が日本語である場合がある。
その場合はURLに日本語を入力しなければいけないが、それが出来ないので、変換しようという話。

確認してみた

Main.phpを起動し、chromeで以下のリクエストを送ってみた。
http://localhost:8001/ファイル名.jpg

実際には以下のような感じで送られていることが分かった。
http://localhost:8001/%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E5%90%8D.jpg
一文字ずつ、%区切りで元の文字列を解釈、16進数の対応コードに変換されている。

クエリストリング (query-stirng)

URLの末尾に?を付けて送ることができる文字。
例えばGoogleで「ごはん」としらべてみるとURLはこんな感じになる。
https://www.google.com/search?q=ごはん(略)

クエリストリングの困った点

クエリストリングの部分だけはブラウザによってエンコードの方法が違う。
これだと、挙動がブラウザによって変わってしまうらしい。
対策として、form属性でgetを指定して、accept-charsetを指定する。
そうすると、そのhtmlと同じ文字コードでクエリストリングを送ってくれるらしいので
ブラウザごとの違いが出ずに済む。
今度使ってみよう。

実装してみた

Server.phpはこんな感じ。
これをインスタンス化してserverStartを実行するのみなので、Main.phpは省略。

<?php
// localhostのport8001を使用
class Server
{
    public $address = '127.0.0.1';
    public $port = '8001';
    // DocumentRoot
    const DOCUMENT_ROOT = 'C:\Repo\basic-web-app\redirect-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);

                    // urlをdecode。日本語のファイル名の場合は16進コードで送られて来ちゃうので、変換してからファイルを探す
                    $requestFileName = urldecode($requestFileName);
                    var_dump($requestFileName);
                    break;
                };
            }

            // 連結したパスの文字列
            $path = self::DOCUMENT_ROOT . "\\" . $requestFileName;

            var_dump('strposの結果' . strpos($requestFileName, '.') != false);
            if (strpos($requestFileName, '.') != false && file_exists($path)) {
                // リクエストに.を含む場合はファイルと判断し、開こうとする
                var_dump('ファイル開きまーす');
                $file = fopen(self::DOCUMENT_ROOT . "\\" . $requestFileName, 'rb');
            }

            // $pathのファイルがあるかどうかを基準に、ステータスラインを作成
            if (isset($file) && $file != false) {
                var_dump('200');
                $responseLine = "HTTP/1.1 200 OK\r\n";
                // 取得したindex.htmlの中身をレスポンスボディとして送信する
                // 全部取得できるからこっちのが楽(どでかいのも全部取得しようとするけど)
                $responseBody = stream_get_contents($file);
                $extension = pathinfo($requestFileName, PATHINFO_EXTENSION);
                $contentTypeMime = self::CONTENT_TYPE_MAPPING_LIST[$extension];
                $httpResponse = $this->createHttpResponse($responseLine, $responseBody ,$contentTypeMime);
            } elseif (file_exists($path)) {
                var_dump('300');
                $responseLine = "HTTP/1.1 301 Moved Permanently\r\n";
                // パターン1:トップページにスラッシュを付けずにリダイレクトした場合 -> 自動で/が付けられて2へ(ディレクトリなし、/なし)
                // パターン2:トップページにスラッシュが付けられてきた場合 -> 末尾にindex.htmlを付与(ディレクトリなし、/あり)
                // パターン3:ディレクトリにスラッシュを付けずにリダイレクトした場合 -> 末尾に/index.htmlを付与(ディレクトリあり、/なし)
                // パターン4:ディレクトリにスラッシュが付けられてきた場合 -> 末尾にindex.htmlを付与(ディレクトリあり、/あり)
                var_dump('stroposの位置' . strpos($requestFileName,  "/"));
                if (strpos($requestFileName,  "/") != false) {
                    var_dump('/あり');
                    $requestFileName =  '/' . $requestFileName;
                    $requestFileName = $this->addIndexHtml($requestFileName);
                } else {
                    var_dump('/なし');
                    $requestFileName =  '/' . $requestFileName . '/';
                    $requestFileName = $this->addIndexHtml($requestFileName);
                }
                $location = "Location: " . 'http://localhost:8001' . $requestFileName;
                $requestFileName = str_replace('//', "/", $location);

                $httpResponse = $this->createRedirectResponse($responseLine, $location);
                var_dump($location);
            } else {
                var_dump('404');
                $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>
                ";
                $contentTypeMime = self::CONTENT_TYPE_MAPPING_LIST['html'];
                $httpResponse = $this->createHttpResponse($responseLine, $responseBody ,$contentTypeMime);
            };
            socket_write($acceptSocket, $httpResponse, strlen($httpResponse));
        }
        
        // 確立された接続を閉じます
        socket_close($acceptSocket);
        
        // ソケットそのものを終了
        socket_close($socket);
    }

    /**
     * ファイルありのhttpレスポンスを返す
     * 200、400番台
     */
    function createHttpResponse($responseLine, $responseBody, $contentTypeMime) : string 
    {
        $headerContentType =  "Content-type: " . $contentTypeMime . "\r\n";

        $responseHeader = '';
        $responseHeader .= "Host: modoki/0.1\r\n";
        $responseHeader .= "Connection: Close\r\n";
        
        $responseHeader .= $headerContentType;
        $responseHeader .= "\r\n";

        $httpResponse = $responseLine . $responseHeader . $responseBody;

        return $httpResponse;
    }

    /**
     * リダイレクト用httpレスポンス作成
     */
    function createRedirectResponse($responseLine, $location) : string 
    {
        $responseHeader = '';
        // $responseHeader += '';// 余裕あれば時間も送信する
        $responseHeader .= "Host: modoki/0.1\r\n";
        $responseHeader .= "Connection: Close\r\n";

        $httpResponse = $responseLine . $responseHeader . $location;

        return $httpResponse;

    }

    /**
     * 送られてきたパスの最後に/を付与して返却
     */
    function addIndexHtml($path) : string 
    {
        return $path . 'index.html';
    }
}

理解最優先で作ったのであまりきれいではない。
あとは欠陥があって、
トップページ/なしから/ありへブラウザが自動リダイレクトしてくれるパターンの時、
localhost:8001//みたいになってしまう。
頑張って直そうと奮闘したのだけど、「Webサーバを理解する」という本質からそれていることに気付きやめた。

並列処理もできてないし、上記の欠陥はあるが、リクエストはしっかり返ってくるのでよし。