HTTPサーバを触りながら、HTTPを勉強する

普段何気なく使っているHTTPですが、「よく考えたらHTTPのことよくわかってないやんけ」と思いまして、調べることにしました。
自分用にメモを残します。

# HTTPとは
おさらい程度ですが、
正式名は Hyper Text Transfer Protocolで、通信プロトコルの一つです。
通信手順や機能を階層分けしたOSI参照モデルでは第7層のアプリケーション層にあたり、ソフトウェア同士で通信をする時の規格の一つになります。
インターネットを介して通信する時に、一般的にこのHTTPが用いられます。
HTTPにもバージョンがあり、多くのものはHTTP/1.1に対応がされています。最近HTTP/3.0 QUICが話題になったかと思われます。
詳細な仕様はRFCの7230~7235(HTTP/1.1)に書いてあります。
[※RFC7230](https://httpwg.org/specs/rfc7230.html)
（軽く読んでみましたが、本職でない人はこれを読むのはしんどそうです。）

これを読めば解決ではあるのですが非常に大変であるので、
今回は他者様の記事を参考にしつつ、HTTPサーバを触りながら、HTTP通信の流れ、データの中身などを理解していきます。

# HTTPサーバの処理
[こちらの方](https://speakerdeck.com/todokr/xin-siihurokuraminkuyan-yu-falsexue-hifang-httpsahawozuo-tutexue-hu-java-scala-clojure)のスライドがめちゃくちゃわかりやすいです。
HTTPサーバの処理としては下記です。

①ソケット（通信のエンドポイント）を用意して、リクエストを待ち受ける
②リクエストの中身をHTTPの仕様に則って確認し、その中身を解析して、レスポンスを返す

もっと下の階層のやり取りであるTCPのSYN/ACKのやり取りなどは、OSのTCP/IPスタックがやってくれるため、Webアプリ屋としては、ヘッダーやボディの中身を確認するまでで良さそうです。

また、実際のHTTPサーバでは、性能を上げるためにコネクションの再利用や、TLSに則って暗号化（HTTPS）などをしてくれています。今回触るのは、本当に基本的なところだけになります。

# HTTPサーバを触ってみる
こちらの[他者様の記事](https://qiita.com/opengl-8080/items/ca152658a0e52c786029)に書いてある[実装](https://github.com/opengl-8080/simple-http-server)をお借りして確認します。

## 0.Webサーバ起動
Gradeでビルドします。Gradleについてはこちらの[他者様の記事](https://qiita.com/tatesuke/items/6e30dc8891d0857f240e)を参考

```
# 使えるtasksを確認
$ gradle tasks
# ビルド
$ gradle build
# javaコマンドで実行
$ java -cp build/classes/java/main gl8080.http.Main
```

私の環境の場合は、80番ポートで起動するとsudo権限が必要だったため、8080ポートで立ち上げました。
起動後、`http://localhost:8080/にほんご.html`にアクセスすると、ページが表示されます。
![2021-09-12_17-48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275351/46df56a6-83f1-b1e1-b832-a997c903e10f.png)

これで準備完了です。

## 1.リクエストの中身を確認
コードを読みますと、
SimpleHttpServer.java内で、serverProcessメソッドがソケットを用意してループで実行されています。
このソケットへの入力をHttpRequestというクラスに渡し、headerという変数を返しています。

```java
    private void serverProcess(ServerSocket server) throws IOException {
        Socket socket = server.accept();
        
        this.service.execute(() -> {
            try (
                InputStream in = socket.getInputStream();
                OutputStream out = socket.getOutputStream();
                ) {
                
                HttpRequest request = new HttpRequest(in);
                
                HttpHeader header = request.getHeader();
```

HttpRequest.java見てみます。
コンストラクタで入力を受け付け、HttpHeaderというクラスでheaderとbodyTextというメンバー変数を作っています。HttpRequestクラスはこのheaderで処理を変えています。

```java
public HttpRequest(InputStream input) {
        try {
            this.header = new HttpHeader(input);
            this.bodyText = this.readBody(input);
```

HttpHeader.javaを見てみます。
こちらもコンストラクタで入力を受け付けています。こちらのクラスでは、入力をパースをして、中身がどういうものか見ています。パースは、readRequestLineメソッドとreadMessageLineメソッドの２つで行われています。

```java
    public HttpHeader(InputStream in) throws IOException {
        StringBuilder header = new StringBuilder();

        header.append(this.readRequestLine(in))
              .append(this.readMessageLine(in));
        
        this.headerText =  header.toString();
```

readRequestLineメソッドの方では、request-lineのパースをしています。
readMessageLineメソッドの方では、header-fieldのパースをしています。
ここは[この他者様の記事](https://qiita.com/ksugimori/items/b128e3aed0aa7ed2c89c)のものをお借りします

### request-line

```
request-line = Method space Request-URI space HTTP-Version CRLF
```

メソッド、Request-URI、HTTPのバージョンが書かれています。

### header-field

```
header-field = field-name ":" OWS field-value OWS
```

フィールド名と値の組が書かれています。
ボディが0バイトだとしてもヘッダとボディの区切りになる空行は必ず存在するらしく、空行に出会うまでがヘッダとして読み取るようです。

※ OWD は "optional white space" 。０個または１個の半角スペース・タブ文字

またHttpHeaderクラスには、isChunkedTransferメソッドとgetContentLengthメソッドがあり、ボディのContent-LengthとTransfer-Encodingをパースしています。

### Content-Length
ボディの長さ、バイト数が書かれています。

### Transfer-Encoding
ボディの長さがわからない時に使い、チャンクという分割送信方式の、データとデータのサイズが書かれています。

他者様の記事によるとTransfer-EncodingもContent-Lengthも存在しない場合があり、それはボディがない時のようです。
HttpRequest.javaに戻りますと、リクエストボティをisChunkedTransferメソッドとgetContentLengthメソッドを元にパースしています。getBodyTextメソッドでそのボディを取得しています。
Content-Lengthの時の処理はわかりやすいですが、Transfer-Encodingの時は、若干複雑です。

### Transfer-Encodingが設定されている時の処理
チャンクデータは、readBodyByChunkedTransferメソッドで取得しています。
やっていることとしては、チャンクデータを1行ずつなめて、データとサイズを取得しています。
サイズは16進数に変換して取得しているようです。

分割して送信してきているので、過去のデータと連携させる必要があるかと思っていましたが、どうやらそういったように処理は書かれていないようです。これが正しいのか次の機会に確認してみます。

## 2.レスポンスの中身を確認

```java
                if (header.isGetMethod()) {
                    File file = new File(".", header.getPath());
                    
                    if (file.exists() && file.isFile()) {
                        this.respondLocalFile(file, out);
                    } else {
                        this.respondNotFoundError(out);
                    }
                } else {
                    this.respondOk(out);
                }
```

レスポンスの中身は上記の処理で作っています。respondLocalFile、respondNotFoundError、respondOkはそれぞれ、指定のパスのファイル、404、200を返すようになっています。

HttpResponse.javaを見てみます。
こちらはコンストラクタでステータスを受け取り、addHeaderメソッドでヘッダーを作り、setBodyメソッドでボディを作り、writeToメソッドでレスポンスとして返しているようです。

全体を見てところ、
GETやPOSTの中身を判別しているわけではない感じですが、一般的には中身はAPIの実行時に使われるかと思われます。

このあたりは次回、自分で実装してみようかと思います。

# 参考
[簡単なHTTPサーバーを作る](https://qiita.com/opengl-8080/items/ca152658a0e52c786029)

[Java で簡易サーバを自作して HTTP を理解する](https://qiita.com/ksugimori/items/b128e3aed0aa7ed2c89c)

[新しいプログラミング言語の学び方 HTTPサーバーを作って学ぶ Java, Scala, Clojure](https://speakerdeck.com/todokr/xin-siihurokuraminkuyan-yu-falsexue-hifang-httpsahawozuo-tutexue-hu-java-scala-clojure)