Dev Study
← 解説「TCP で送って受け取る — 名前解決、接続、タイムアウト、閉じるまで読む」に戻る

サンプルコードで身につける: TCP で送って受け取る — 名前解決、接続、タイムアウト、閉じるまで読む

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

1fetch: 名前解決 → 接続 → 送信 → 閉じるまで読む

で実際に を送る fetch で、(, ).to_socket_addrs() でし、(5 秒)で接続、set_read_timeout(10 秒)を付けてから write_all で送り、 でサーバーが閉じるまで読みます。ネットワークを使うのでこのページでは実行せず、手元で別のターミナルに 8000 を立ててから fetch.rs && ./fetch すると、「3472 バイト受信」の後に HTTP/1.0 200 OK、Server: SimpleHTTP/...、: 3316 などのが出ます(Python のサーバーは HTTP/1.0 で答え、バイト数はディレクトリ一覧の中身で変わります)。localhost の名前解決はによって IPv6 の [::1] が先に返るので、サーバーを 127.0.0.1 だけに bind すると refused になります。

use std::io::{Read, Write};
use std::net::{TcpStream, ToSocketAddrs};
use std::time::Duration;

struct Url {
    host: String,
    port: u16,
    path: String,
}

/// http://host[:port]/path だけ読む最小版
fn parse(input: &str) -> Option<Url> {
    let (_, rest) = input.split_once("://")?;
    let (authority, path) = rest.split_once('/').unwrap_or((rest, ""));
    let (host, port) = match authority.rsplit_once(':') {
        Some((h, p)) => (h, p.parse().ok()?),
        None => (authority, 80),
    };
    Some(Url { host: host.to_string(), port, path: format!("/{path}") })
}

fn host_header(url: &Url) -> String {
    if url.port == 80 { url.host.clone() } else { format!("{}:{}", url.host, url.port) }
}

/// URL に GET を 1 回送り、生のレスポンス(バイト列)を返す
fn fetch(url: &Url) -> Result<Vec<u8>, String> {
    // 1. 名前解決: ("localhost", 8000) -> 127.0.0.1:8000 のような SocketAddr。OS のリゾルバに任せる
    let addr = (url.host.as_str(), url.port)
        .to_socket_addrs()
        .map_err(|e| format!("名前解決に失敗 {}: {e}", url.host))?
        .next()
        .ok_or_else(|| format!("名前解決の結果が空: {}", url.host))?;

    // 2. TCP 接続。相手が応答しないとき無限に待たないよう、上限を付ける
    let mut stream = TcpStream::connect_timeout(&addr, Duration::from_secs(5))
        .map_err(|e| format!("接続に失敗 {addr}: {e}"))?;
    stream.set_read_timeout(Some(Duration::from_secs(10))).map_err(|e| e.to_string())?;

    // 3. リクエストを送る。Connection: close で「この 1 往復で閉じる」と宣言
    let request = format!(
        "GET {} HTTP/1.1\r\nHost: {}\r\nConnection: close\r\n\r\n",
        url.path,
        host_header(url)
    );
    stream.write_all(request.as_bytes()).map_err(|e| format!("送信に失敗: {e}"))?;

    // 4. サーバーが接続を閉じる(EOF)まで読む。close を宣言したので、閉じたら終わり
    let mut raw = Vec::new();
    stream.read_to_end(&mut raw).map_err(|e| format!("受信に失敗: {e}"))?;
    Ok(raw)
}

fn main() {
    // 別のターミナルで python3 -m http.server 8000 を起動してから実行する
    let url = parse("http://localhost:8000/").expect("URL が読めない");
    match fetch(&url) {
        Ok(raw) => {
            let text = String::from_utf8_lossy(&raw);
            println!("{} バイト受信", raw.len());
            for line in text.lines().take_while(|l| !l.is_empty()) {
                println!("  {line}");
            }
        }
        Err(e) => println!("失敗: {e}"),
    }
}

2ネットワーク無しで試す: Read を型引数にする

読む先を TcpStream に固定せず R: Read の型引数にすると、本番は TcpStream、テストでは手で作ったバイト列(&[u8] はそのまま Read を実装している)を同じ関数に渡せます。Drip は 1 回の read で最大 4 バイトしか返さない「途切れ途切れ」のストリームで、read が 1 回で全部返すとは限らないこと、read_to_end が 0 が返るまで呼び続けてくれることを見せます。実行すると 43 バイトの偽レスポンスから と body "hello" が取れ、Drip でも 43 バイト揃う(read は 12 回)、不正な は で U+FFFD に置き換わって落ちない、と出ます。

Rust

3ローカルで往復を見る: TcpListener を同じプロセスに立てる

TcpListener::bind("127.0.0.1:0") で OS に空きポートを割り当てさせ、別スレッドでサーバー役を 1 接続だけ動かし、クライアント役が connect_timeout → write_all → read_to_end で往復する最小の実験です。サーバー側の conn が drop された瞬間に接続が閉じ、それがクライアントの read_to_end の終わりになります。を使うので手元で動かす想定で、実行するとサーバーが受け取ったリクエスト(GET / \r\nHost: 127.0.0.1:<ポート>\r\nConnection: close\r\n\r\n)と、クライアントが受け取った 70 バイトのレスポンスが \r\n を見える形で出ます(ポート番号は実行ごとに変わります)。

use std::io::{self, Read, Write};
use std::net::{TcpListener, TcpStream, ToSocketAddrs};
use std::thread;
use std::time::Duration;

fn main() -> io::Result<()> {
    // 1. ポート 0 で待ち受けると、OS が空いているポートを割り当てる(テストで衝突しない)
    let listener = TcpListener::bind("127.0.0.1:0")?;
    let port = listener.local_addr()?.port();

    // 2. サーバー役は別スレッド。1 接続だけ受け、固定のレスポンスを返して閉じる
    let server = thread::spawn(move || -> io::Result<String> {
        let (mut conn, _) = listener.accept()?;
        let mut buf = [0u8; 1024];
        let n = conn.read(&mut buf)?; // 短いリクエストなので 1 回で足りる前提(本物は空行まで読む)
        let received = String::from_utf8_lossy(&buf[..n]).to_string();
        conn.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 12\r\nConnection: close\r\n\r\n<p>hello</p>")?;
        Ok(received) // conn はここで drop され、接続が閉じる -> クライアントの read_to_end が終わる
    });

    // 3. クライアント役: 名前解決 -> connect_timeout -> 送信 -> read_to_end
    let addr = ("127.0.0.1", port).to_socket_addrs()?.next().unwrap();
    let mut stream = TcpStream::connect_timeout(&addr, Duration::from_secs(5))?;
    stream.set_read_timeout(Some(Duration::from_secs(10)))?;
    let request = format!("GET / HTTP/1.1\r\nHost: 127.0.0.1:{port}\r\nConnection: close\r\n\r\n");
    stream.write_all(request.as_bytes())?;
    let mut raw = Vec::new();
    stream.read_to_end(&mut raw)?;

    let received = server.join().expect("サーバースレッドが panic した")?;
    println!("--- サーバーが受け取ったリクエスト ---");
    println!("{}", received.escape_debug());
    println!("--- クライアントが受け取ったレスポンス({} バイト)---", raw.len());
    println!("{}", String::from_utf8_lossy(&raw).escape_debug());
    Ok(())
}

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