Dev Study
← Rust で小さなブラウザを作る コース

9. TCP で送って受け取る — 名前解決、接続、タイムアウト、閉じるまで読む

fetch は 5 行の手順です。名を IP アドレスにし()、 でつなぎ、リクエストの文字列を書き、サーバーが閉じるまで読み、前のレッスンの parse に渡す。Rust の ::net だけでできます。

名前解決は (host, ).to_socket_addrs() です。OS のリゾルバ(/etc/hosts や DNS)に任せ、返ってきた最初のアドレスを使います。接続は :: で、5 秒待っても返事が無ければあきらめます。読み取りにも set_read_timeout で 10 秒を入れてあります。タイムアウトを入れないと、相手が黙ったときにプログラムが永久に止まります。

送るのは write_all(build_request(url).as_bytes())。読むのは で、これは相手が接続を閉じるまで返ってきません。前のレッスンで を送ったので、サーバーは応答を書き終えると閉じ、read_to_end が全部を返します。受け取ったバイト列は で文字列にします。 でないバイトがあっても落ちず、置換文字になります。

このコードはサイトの実行では動きません(外に出られないため)。手元で試すには、HTML を置いたフォルダで 8000 を動かし、別の端末で step03_http_fetch.rs をコンパイルして ./step03_http_fetch http://localhost:8000/index.html と打ちます。Python の簡易サーバーは HTTP/1.0 で 付きの応答を返すので、分解の 2 つ目の道が通ります。http://example.com/ も試せます。こちらは で返ってきます。1 つ注意があり、macOS では localhost が IPv6 の ::1 に先に解決されることがあります。サーバーを --bind 127.0.0.1 で立てると接続を拒否されるので、既定(全アドレス)で立てるか、 を http://127.0.0.1:8000/ にしてください(fetch は名前解決の結果の先頭 1 つしか試しません)。

本物のブラウザでは: ()はを開きません。 では URLLoaderFactory::CreateURLLoader() で要求を投げ、別プロセスの(services/network)が URLRequest → URLRequestHttpJob → ::Transaction → HttpNetworkTransaction の順で処理します(docs/life-of-a-url-request.md)。ネットワークサービスが落ちても新しいプロセスで再起動されます。Firefox は content プロセスの HttpChannelChild から親の HttpChannelParent へ渡し、ソケットは socket プロセスに分けられます。 は Chromium が net/socket/ssl_client_socket_impl.cc、Firefox が NSS(security/manager/ssl)です。この回の fetch() は『同じプロセスで、TLS 無しで、1 回だけ』の最短版です。

開発のどこに活かすか: クライアントのライブラリが隠している 5 つの手順(名前解決・接続・送信・受信・分解)のどこで失敗したかが、エラーメッセージの読み方として身につきます。「接続タイムアウト」と「読み取りタイムアウト」が別物であること、Keep-Alive のときは「閉じるまで読む」が使えないこと、は障害対応でそのまま出てきます。

点線の付いた語はタップすると説明が出ます。用語集を開く →

① fetch の 5 段と、失敗の出どころ

どこで止まったかで、HttpError::Network のメッセージが変わる
to_socket_addrs名前解決に失敗 → ホスト名の誤り / DNS
IP アドレス
connect_timeout(5 秒)接続に失敗 → ポート閉鎖 / FW / 相手が落ちている
TcpStream
write_all送信に失敗 → 途中で切られた
read_to_end(10 秒の読み取りタイムアウト)受信に失敗 → 相手が黙った / Connection: close を付けていない
バイト列 → 文字列
HttpResponse::parse前のレッスンの分解

本物のクライアントはこの上に、リダイレクト・TLS・接続の使い回し・リトライを重ねています。

サンプルコード(フレームワーク環境が必要なため表示のみ)

// src/http.rs の通信の部分(build_request と fetch)
pub fn build_request(url: &Url) -> String {
    format!(
        "GET {target} HTTP/1.1\r\n\
         Host: {host}\r\n\
         User-Agent: tinybrowser/0.1\r\n\
         Accept: text/html\r\n\
         Connection: close\r\n\
         \r\n",
        target = url.request_target(),
        host = url.host_header(),
    )
}

pub fn fetch(url: &Url) -> Result<HttpResponse, HttpError> {
    // 1. 名前解決: "example.com:80" → IP アドレス(OS のリゾルバに任せる)
    let addr = (url.host.as_str(), url.port)
        .to_socket_addrs()
        .map_err(|e| HttpError::Network(format!("名前解決に失敗 {}: {e}", url.host)))?
        .next()
        .ok_or_else(|| HttpError::Network(format!("名前解決の結果が空: {}", url.host)))?;

    // 2. TCP 接続(3 ウェイハンドシェイクはここで起きる)
    let mut stream = TcpStream::connect_timeout(&addr, Duration::from_secs(5))
        .map_err(|e| HttpError::Network(format!("接続に失敗 {addr}: {e}")))?;
    stream
        .set_read_timeout(Some(Duration::from_secs(10)))
        .map_err(|e| HttpError::Network(e.to_string()))?;

    // 3. リクエストを送る
    stream
        .write_all(build_request(url).as_bytes())
        .map_err(|e| HttpError::Network(format!("送信に失敗: {e}")))?;

    // 4. サーバーが閉じるまで読む(Connection: close を送ったので、閉じたら終わり)
    let mut raw = Vec::new();
    stream
        .read_to_end(&mut raw)
        .map_err(|e| HttpError::Network(format!("受信に失敗: {e}")))?;

    // 5. 分解する
    HttpResponse::parse(&String::from_utf8_lossy(&raw))
}

手元で動かす(配布物)

図やコードに出てくる用語(タップで説明)

サンプルコードで身につける(3本) →

学んだ概念を、素朴な例から実務寄りの例まで3つの具体例で確認します。

公式ドキュメントで詳しく ↗