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 段と、失敗の出どころ
本物のクライアントはこの上に、リダイレクト・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 の 1 ファイル版(step03_http_fetch.rs)手元で: rustc --edition 2021 step03_http_fetch.rs && ./step03_http_fetch http://localhost:8000/
- 試し用のページ(sample.html)index.html として置いて python3 -m http.server 8000
図やコードに出てくる用語(タップで説明)
学んだ概念を、素朴な例から実務寄りの例まで3つの具体例で確認します。
公式ドキュメントで詳しく ↗