🔖

HTTP史 (HTTP/0.9 ~ HTTP/1.0編)

2022/11/20に公開

HTTP史 (HTTP/0.9 ~ HTTP/1.0編)

Real World HTTPを一通り流して(一部実装して)みたのでまとめる。

そもそもHTTPとは

これを読んでくれている方々には釈迦に説法どころの騒ぎではないだろうが、 Hyper Text Transfer Protocol のこと。
その名の通り通信プロトコルの一種で、元々はWebブラウザとWebサーバー間の通信をルール化したもの。

HTTP/0.9

HTTPの原始の姿。根本的な部分はなんとなく共通しているが、現在の仕様と比べるとまだまだ色々と足りない。
シン・ゴジラでいうところの第二形態「蒲田くん」と言ったところか。(第一形態は尻尾しか見えないしスルー)

機能としては「指定したパスに存在するHTMLのドキュメントを取得する」だけで、送受信できるデータも「検索対象のパス」と「HTML」のみ。
HTMLドキュメントのみの通信しか想定されていなかったため、サーバーから返すデータフォーマットを伝える手段(Content-Typeみたいなのとか)もなかったらしい。

実装してみた

とりあえず「特定のパスをリクエストできる」と「指定されたパスのコンテンツを返す」機能要件だけを満たした最小限の実装。

サーバー

# 細かいところは省略
...

const PUBLIC_DIR: &'static str = "/tmp";

fn main() -> std::io::Result<()> {
    // TCPソケットを作成して8080ポートにバインド
    let server = TcpListener::bind("127.0.0.1:8080")?;

    let (client, addr) = server.accept()?;
    println!("connected: {:?}", addr);

    // クライアントからリクエストを読み込み
    let mut request = String::new();
    let mut reader = BufReader::new(&client);
    reader.read_line(&mut request)?;

    println!("request: {:?}", request);

    // 指定されたパスのファイルを読み込み (めちゃくちゃ任意のパスをトラバースできる)
    let mut file = File::open(format!("{}{}", PUBLIC_DIR, request.trim()))?;
    let mut response = String::new();
    file.read_to_string(&mut response)?;

    // 読み込んだファイルの内容をクライアントに返す
    println!("response: {:?}", response);
    let mut writer = BufWriter::new(&client);
    writer.write(response.as_bytes())?;
    writer.flush()?;

    client.shutdown(Shutdown::Both)?;

    Ok(())
}

クライアント

use std::io::prelude::*;
use std::io::{BufReader, BufWriter};
use std::net::{Shutdown, TcpStream};

const SERVER: &'static str = "127.0.0.1:8080";
const REQUEST_PATH: &'static str = "/test.html";

fn main() -> std::io::Result<()> {
    // サーバーに接続
    println!("Server: {:?}", SERVER);
    let stream = TcpStream::connect(SERVER)?;

    // リクエストされたファイルを読み込み
    println!("request: {:?}", REQUEST_PATH);
    let mut writer = BufWriter::new(&stream);
    writer.write(format!("{}\n", REQUEST_PATH).as_bytes())?;
    writer.flush()?;

    // リクエストしたファイルの内容を受取
    println!("read responses...");
    let mut response = String::new();
    let mut reader = BufReader::new(&stream);
    reader.read_line(&mut response)?;

    println!("response: {:?}", response);
    stream.shutdown(Shutdown::Both)?;

    Ok(())
}
# サーバー実行
$ cargo run --bin http_0_9_server
connected: 127.0.0.1:56374
request: "/test.html\n"
response: "<html>...</html>\n"

# クライアント実行
$ cargo run --bin http_0_9_client
Server: "127.0.0.1:8080"
request: "/test.html"
read responses...
response: "<html>...</html>\n"

動いた。

HTTP/1.0

HTTP/0.9から大幅にアップデートされた形。ここから急に現在の仕様に近づいてくる。
シン・ゴジラで例え他個人的なイメージは、第三形態「品川くん」。(まだまだ進化中の箇所も多いが、二足歩行にもなりぐっとゴジラ感が増したような気がしてるので)

機能的には、メソッド・ヘッダー・ボディ・ステータスなどの仕様が追加され、基本的なファイルアクセスができるようなプロトコルになった。

ヘッダー

元々は電子メールのプロトコルから着想を得ているらしい。(実際ところどころ電子メールの面影が見られる)
{フィールド名}: {値} といった形式で実際の本文の前に付加されるあれ。

メソッド

リクエストに含めるGETとかPOSTとかのあれ。
HTTPはファイルシステムのような思想で作られているらしく、そんな雰囲気のメソッドが色々用意されている。

ステータス

レスポンスに含まれる200とか404とかのあれ。
サーバーからの応答がどんな応答なのかを示す3桁の数字。

100番台
処理中の情報のレスポンス。(応答をいくつかに区切って返す時とか?)

200番台
処理成功時のレスポンス。

300番台
サーバーからクライアントへの指示を記述したレスポンス。
リダイレクトさせたりとか、キャッシュを利用させたりとか。

400番台
クライアントからのリクエストが異常な場合のレスポンス。

500番台
サーバー内部でエラーが発生した場合のレスポンス。

ステータスコードの直後にはかんたんな説明文がつけられる。
200 OKとか404 Not Foundみたいな感じ。

ボディ

リクエストにもレスポンスにも、ボディとしてコンテンツを含めることができるようになった。

ヘッダーとボディの間を空行で区切るのは言うまでもない。(ここも電子メールから来ているらしい)

~~~: ~~~
~~~: ~~~
Content-Length: ~~~

ここから下がContent-Length bytes分の大きさのボディ

これも実装してみた

こっちも最小限の機能要件を満たした実装。対応コンテンツもとりあえずHTMLだけ。
クライアントの実装は文字列を追加で送るだけで、そこまで変わらないので省略。
(とりあえず動くコードを書きなぐってるのはごめんなさい)

# クライアントから送るリクエスト
{メソッド} {パス} HTTP/1.0
{ヘッダー名}: {値}
User-Agent: my-http-client
Accept: text/html

{リクエストボディ}

---

# サーバーからのレスポンス
HTTP/1.0 {ステータスコード} {メッセージ}
{ヘッダー名}: {値}
Content-Length: {レスポンスサイズ}
Content-Type: text/html; charset=utf-8

{レスポンスボディ}

サーバー

/// 細かいところは省略
...

fn handle_get(client: &mut TcpStream, path: &str, headers: &HashMap<String, String>, body: &String) -> std::io::Result<()> {
    let (status, contents) = match File::open(format!("{}{}", PUBLIC_DIR, path)) {
        Ok(mut file) => {
            let mut contents = String::new();
            file.read_to_string(&mut contents)?;

            ("200 OK", contents)
        },
        Err(e) => ("404 Not Found", String::new()),
    };
    println!("contents: {:?}", contents);

    writeln!(client, "HTTP/1.0 {}", status)?;
    writeln!(client, "Content-Length: {}", contents.len())?;
    writeln!(client, "Content-Type: text/html")?;
    writeln!(client)?;  // 空行を挟む
    writeln!(client, "{}", contents)?;

    Ok(())
}

fn handle_post(client: &mut TcpStream, path: &str, headers: &HashMap<String, String>, body: &String) -> std::io::Result<()> {
    let mut file = File::create(format!("{}{}", PUBLIC_DIR, path))?;
    file.write_all(body.as_bytes())?;

    // レスポンス返すところはだいたい一緒なので省略
    ...

    Ok(())
}

fn handle_put(client: &mut TcpStream, path: &str, headers: &HashMap<String, String>, body: &String) -> std::io::Result<()> {
    let mut file = OpenOptions::new().write(true).open(format!("{}{}", PUBLIC_DIR, path))?;
    file.write_all(body.as_bytes())?;
    file.flush()?;

    // レスポンス返すところはだいたい一緒なので省略
    ...

    Ok(())
}

fn handle_delete(client: &mut TcpStream, path: &str, headers: &HashMap<String, String>, body: &String) -> std::io::Result<()> {
    let path = format!("{}{}", PUBLIC_DIR, path);
    let status = match File::open(&path) {
        Ok(_file) => {
            std::fs::remove_file(&path)?;
            "200 OK"
        },
        Err(_e) => "404 Not Found",
    };

    // レスポンス返すところはだいたい一緒なので省略
    ...

    Ok(())
}

fn main() -> std::io::Result<()> {
    // 最初もHTTP/0.9とだいたい一緒なので省略
    ...

    let mut headers = HashMap::new();
    parse_headers(&header_lines, &mut headers);

    println!("Headers");
    for (key, value) in headers.iter() {
        println!("Key: {:?}, Value: {:?}", key, value);
    }

    let content_length = if let Some(len) = headers.get("Content-Length") {
        len.parse().unwrap()
    } else {
        0
    };
    let mut body = String::from_utf8(vec![0; content_length]).unwrap();
    println!("{:?}", body.len());
    reader.read_exact(unsafe { body.as_bytes_mut() })?;
    println!("{:?}", body);

    match method {
        "GET" => handle_get(&mut client, path, &headers, &body)?,
        "POST" => handle_post(&mut client, path, &headers, &body)?,
        "PUT" => handle_put(&mut client, path, &headers, &body)?,
        "DELETE" => handle_delete(&mut client, path, &headers, &body)?,
        _ => {},
    }

    client.flush()?;
    client.shutdown(std::net::Shutdown::Both)?;

    Ok(())
}

curlでテスト。

### サーバーを起動
$ cargo run --bin http_1_0_server

### GETメソッド
$ curl -v --http1.0 http://localhost:8080/test.html
*   Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /test.html HTTP/1.0
> Host: localhost:8080
> User-Agent: curl/7.81.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Content-Length: 18
< Content-Type: text/html
<
* Excess found in a read: excess = 1, size = 18, maxdownload = 18, bytecount = 0
<html>...</html>

* Closing connection 0

### PUTメソッド
$ curl -v --http1.0 -X POST -d '<html>test2</html>' http://localhost:8080/test2.html
Note: Unnecessary use of -X or --request, POST is already inferred.
*   Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> POST /test2.html HTTP/1.0
> Host: localhost:8080
> User-Agent: curl/7.81.0
> Accept: */*
> Content-Length: 18
> Content-Type: application/x-www-form-urlencoded
>
* Mark bundle as not supporting multiuse
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Content-Length: 0
< Content-Type: text/html
* Closing connection 0

$ curl --http1.0 http://localhost:8080/test2.html
<html>test2</html>

### PUTメソッド
$ curl -v --http1.0 -X PUT -d '<html>test2 updated</html>' http://localhost:8080/test2.html
*   Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> PUT /test2.html HTTP/1.0
> Host: localhost:8080
> User-Agent: curl/7.81.0
> Accept: */*
> Content-Length: 26
> Content-Type: application/x-www-form-urlencoded
>
* Mark bundle as not supporting multiuse
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Content-Length: 0
< Content-Type: text/html
<
* Closing connection 0

$ curl --http1.0 http://localhost:8080/test2.html
<html>test2 updated</html>

### DELETEメソッド
$ curl -v --http1.0 -X DELETE http://localhost:8080/test2.html
*   Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> DELETE /test2.html HTTP/1.0
> Host: localhost:8080
> User-Agent: curl/7.81.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Content-Length: 0
< Content-Type: text/html
<
* Closing connection 0

$ curl --http1.0 http://localhost:8080/test2.html
(ファイルが削除できているので返ってこない)

動いた。
他にも、フォームの送信とかクッキーの取り扱いとかもあるけどそのへんは割愛。

最後に

実際自分で書いてみてもシンプルでそこまで難しくないし、汎用性の高さはさすがだな〜という雑感。
HTTP/1.1からHTTP/3とかまではまた別途まとめる予定。

Discussion