rust-server - 解説

実装の詳細

HTTPリクエストの構造

リクエストライン: GET /path HTTP/1.1
ヘッダー:        Host: example.com
                 Content-Type: application/json
                 Content-Length: 13
空行:
ボディ:          {"key":"value"}

パースの流れ

// 1. リクエストラインをパース
let line = "GET /api/users HTTP/1.1";
let parts: Vec<&str> = line.split_whitespace().collect();
// parts = ["GET", "/api/users", "HTTP/1.1"]

// 2. ヘッダーをパース(空行まで)
loop {
    let line = read_line();
    if line.is_empty() { break; }
    // "Content-Type: application/json" → ("content-type", "application/json")
}

// 3. Content-Length に基づいてボディを読む
let length = headers.get("content-length").parse()?;
let body = read_exact(length);

レスポンスの生成

// HTTP/1.1 200 OK
// Content-Type: text/html
// Content-Length: 13
//
// Hello, World!

fn send_response(stream: &mut TcpStream, response: &Response) {
    // ステータスライン
    write!(stream, "HTTP/1.1 {} {}\r\n", status, status_text);

    // ヘッダー
    for (key, value) in &headers {
        write!(stream, "{}: {}\r\n", key, value);
    }

    // 必須: Content-Length
    write!(stream, "Content-Length: {}\r\n", body.len());

    // 空行(ヘッダー終了を示す)
    write!(stream, "\r\n");

    // ボディ
    stream.write_all(&body);
}

よくある間違い

1. 改行コードの誤り

// 間違い: LF のみ
write!(stream, "HTTP/1.1 200 OK\n");

// 正しい: CRLF
write!(stream, "HTTP/1.1 200 OK\r\n");

2. Content-Length の省略

// 間違い: Content-Length なし
// → クライアントがボディの終わりを判断できない

// 正しい: 必ず指定
write!(stream, "Content-Length: {}\r\n", body.len());

3. バッファリング問題

// 間違い: flush 忘れ
stream.write_all(&response);
// → バッファに溜まったまま送信されない可能性

// 正しい: 明示的に flush
stream.write_all(&response)?;
stream.flush()?;

パフォーマンス最適化

スレッドプールの使用

use std::sync::mpsc;

struct ThreadPool {
    workers: Vec<Worker>,
    sender: mpsc::Sender<Job>,
}

// 固定数のワーカースレッドで処理
// 接続ごとにスレッド生成しない

静的ファイルのキャッシュ

use std::collections::HashMap;
use std::sync::RwLock;

lazy_static! {
    static ref FILE_CACHE: RwLock<HashMap<String, Vec<u8>>> = RwLock::new(HashMap::new());
}

fn serve_static(path: &str) -> Response {
    // キャッシュを確認
    if let Some(content) = FILE_CACHE.read().unwrap().get(path) {
        return Response::with_body(content.clone());
    }

    // ファイルを読み込んでキャッシュ
    let content = std::fs::read(path)?;
    FILE_CACHE.write().unwrap().insert(path.to_string(), content.clone());

    Response::with_body(content)
}

発展トピック

Keep-Alive

// 接続を維持して複数リクエストを処理
fn handle_connection(stream: TcpStream) {
    loop {
        let request = Request::parse(&stream)?;

        // Connection: close なら終了
        if request.headers.get("connection") == Some(&"close".to_string()) {
            break;
        }

        let response = process(request);
        response.headers.insert("Connection", "keep-alive");
        response.send(&stream)?;
    }
}

非同期I/O(tokio)

use tokio::net::TcpListener;
use tokio::io::{AsyncReadExt, AsyncWriteExt};

#[tokio::main]
async fn main() {
    let listener = TcpListener::bind("127.0.0.1:8080").await?;

    loop {
        let (socket, _) = listener.accept().await?;
        tokio::spawn(async move {
            handle_connection(socket).await;
        });
    }
}

ミドルウェアパターン

type Middleware = fn(Request, Handler) -> Response;

fn logging_middleware(req: Request, next: Handler) -> Response {
    println!("{} {}", req.method, req.path);
    let start = Instant::now();
    let response = next(req);
    println!("Completed in {:?}", start.elapsed());
    response
}

// チェイン実行
fn apply_middleware(req: Request, middlewares: &[Middleware], handler: Handler) -> Response {
    if middlewares.is_empty() {
        handler(req)
    } else {
        let (first, rest) = middlewares.split_first().unwrap();
        first(req, |r| apply_middleware(r, rest, handler))
    }
}