🦔

[備忘録15]作って学ぶブラウザのしくみ / HTTPを実装する/ ストリームの構築(リクエスト、レスポンス)

2025/01/03に公開

TCPは、データをストリームとして扱う。ストリームとは連続したデータの流れを示す。データは小さいパケットに文亜kつされますが、受信側では連続したデータとして復元される。

TCPでは3ウェイハンドシェイクでコネクションを確立します。これにより高い信頼性を確保します。
noliライブラリは、TcpStream構造体とデータを書き込みするAPIを提供している。connectメソッドでコネクションを確立。
connectメソッドは成功すればTcpStream構造体を返すが、失敗すればエラーを返す。

HttpClient構造体に以下の実装を追加する

let mut stream = match TcpStrewam::connect(socket_addr) {
            Ok(stream) => stream,
            Err(_) => {
                return Err(Error::Network("Failed to connect to server".to_string()))
            }
        }

リクエストラインの構築

メソッド名、パス名、httpバージョンをWSで繋げる。

let mut request = String::from("GET /");
        request.push_str(&path);
        request.push_str(" HTTP/1.1\r\n");

ヘッダの構築

Host, Accept, Connectionヘッダを追加。

  • Hostヘッダ
    • リクエスト先のホスト、ポト番号を指定する(必須)
  • Acceptヘッダ
    • クライアントが受け入れ可能な応答のコンテンツタイプを指定(省略可)
    • 今回はHTMLドキュメントのみを受け入れ可能にするので**"text/html"**という値を指定します。
  • Conectionヘッダ
    • クライアントとサーバ間の接続に関する情報を指定する(省略可)
    • 今回はリクエスト処理後に毎回接続を切断したいので、closeの値を指定する。この値がないとTCPレスポンスをずっと待ち続けて実行がストップしてしまう。
      以下のようなコードを追加する
        /** ヘッダの構築 */
        request.push_str("Host: ");
        request.push_str(&host);
        request.push_str("\r\n");
        request.push_str("Accept: text/html\n");
        request.push_str("Connection: close\n");
        request.push_str("\r\n");

リクエストの送信

サーバへのリクエストの送信はTcpStream構造体にあるwriteメソッドで行う。
上記で作成したHTTPリクエストの文字列を引数に渡す。
writeメソッドは、何バイト送ったかを戻り値として返す。_bytes_writtenという変数に保存するが、Rustでは_で始まる変数は使う予定のない変数。
_で始まらないかつ定義した変数が使用されていないとコンパイラは[unused variable]という警告を出す。

as_bytes()の動作は以下のようになる


fn main() {
    let bytes = "bors".as_bytes();
    println!("bytes = {:?}", bytes);
    
    let bytes2 = "日本".as_bytes();
    println!("bytes2 = {:?}", bytes2);
    
}

出力は以下のようになる(日本語の場合はマルチバイトになる)

bytes = [98, 111, 114, 115]
bytes2 = [230, 151, 165, 230, 156, 172]

以下のようなコードを追加する


### レスポンスの受信
サーバへのリクエストの送信はTcpStream構造体にあるreadメソッドで行う。

        /** レスポンスの受信 */
        let mut received = Vec::new();
        loop {
            let mut buf = [0u8; 4096];//バッファのサイズを指定している。
            let bytes_read = match stream.read(&mut buf) {
                Ok(bytes_read) => bytes_read,
                Err(e) => {
                    return Err(Error::Network(format!("Failed to read response: {:#?}",e)))
                };
                if byte_read === 0 {
                    break;
                }
                received.extend_form_slice(&buf[..bytes_read]); //vectorのメソッドであるextend_from_sliceを使って引数をreceivedに追加している。..butes_readという表現は配列のスライスを作成している
            }
        }

HTTPレスポンスの構築

レスポンスのデータはUTF-8のバイト列なので、from_utf8関数を利用してstr型の文字列に変換する。
この文字列からHttpResponse構造体を構築しメソッドの戻り値にする。

 /** HTTPレスポンスの構築 */
        match core::str::from_utf8(&received) {
            Ok(response) => {
                Ok(HttpResponse::new(response.to_string())),
            }
            Err(e) => {
                Err(Error::Network(format!("Failed to parse response: {:#?}",e)))
            }
        }

HttpResponse構造体の作成

HttpResponse構造体はどのプラットフォームでも共通で使用できるので、saba_coreディレクトリ以下に定義する。新しくhttp.rsファイルをsaba_coreディレクトリ以下に追加する。

touch saba_core/src/http.rs
#[derive(Debug, Clone)]
pub struct Header {
    pub name: String,
    pub value: String,
}

impl Header {
    pub fn new(name: String, value: String) -> Self {
        Self {name, value}
    }
}

Discussion