#TheBook #Rust https://doc.rust-lang.org/book/ch20-00-final-project-a-web-server.html > [!attention] > サンプルコードの内容がかなり異なっていたので英語版を参考にする。 # シングルスレッドのWebサーバを構築する - [[HTTP]]と[[TCP]] ## [[TCP]]接続のリッスン ```console cargo new --bin hello-server ``` ```rust use std::net::TcpListener; fn main() { let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); for stream in listener.incoming() { let stream = stream.unwrap(); println!("接続が確立しました"); } } ``` `localhost:7878`にアクセスして、ログにメッセージが表示されることを確認。 ## リクエストを読み取る ```rust use std::{ io::{prelude::*, BufReader}, net::{TcpListener, TcpStream}, }; fn main() { let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); for stream in listener.incoming() { let stream = stream.unwrap(); handle_connection(stream); } } fn handle_connection(mut stream: TcpStream) { let buf_reader = BufReader::new(&mut stream); let http_request: Vec<_> = buf_reader .lines() .map(|result| result.unwrap()) // 改行が2つ続くとHTTPリクエストは終了なため、空行までを取得 .take_while(|line| !line.is_empty()) .collect(); eprintln!("Request: {:#?}", http_request); } ``` `localhost:7878`にアクセスすると、ログに以下が出力される。 ``` Request: [ "GET / HTTP/1.1", "Host: localhost:7878", "Connection: keep-alive", "Cache-Control: max-age=0", "sec-ch-ua: \"Not_A Brand\";v=\"99\", \"Google Chrome\";v=\"109\", \"Chromium\";v=\"109\"", "sec-ch-ua-mobile: ?0", "sec-ch-ua-platform: \"Windows\"", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", "Sec-Fetch-Site: none", "Sec-Fetch-Mode: navigate", "Sec-Fetch-User: ?1", "Sec-Fetch-Dest: document", "Accept-Encoding: gzip, deflate, br", "Accept-Language: ja,en-US;q=0.9,en;q=0.8", ] ``` ## レスポンスを書く レスポンスを作成してstreamから書き込む処理を追加。 ```rust fn handle_connection(mut stream: TcpStream) { let buf_reader = BufReader::new(&mut stream); let http_request: Vec<_> = buf_reader .lines() .map(|result| result.unwrap()) // 改行が2つ続くとHTTPリクエストは終了なため、空行までを取得 .take_while(|line| !line.is_empty()) .collect(); let response = "HTTP/1.1 200 OK\r\n\r\n"; stream.write_all(response.as_bytes()).unwrap(); } ``` レスポンス形式は以下なので ``` HTTPバージョン ステータスコード 理由 \r\n ヘッダ \r\n ボディ ``` `"HTTP/1.1 200 OK\r\n\r\n"`は以下の意味になる。 - HTTPバージョンは`1.1` - [[HTTPレスポンスステータスコード|ステータスコード]]は`200` - 理由は`OK` - ヘッダはなし - ボディはなし 期待通り動いている。 ![[Pasted image 20230121173532.png]] ## 実際の[[HTML]]を返す プロジェクトルートに`hello.html`を配置して以下のコードを実装。 ```rust fn handle_connection(mut stream: TcpStream) { let buf_reader = BufReader::new(&mut stream); let http_request: Vec<_> = buf_reader .lines() .map(|result| result.unwrap()) // 改行が2つ続くとHTTPリクエストは終了なため、空行までを取得 .take_while(|line| !line.is_empty()) .collect(); let status_line = "HTTP/1.1 200 OK"; let contents = fs::read_to_string("hello.html").unwrap(); let length = contents.len(); let response = format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"); stream.write_all(response.as_bytes()).unwrap(); } ``` ## リクエストの検証と選択的な応答 サンプルコードはあまりに冗長だったので、関数に分離した。 ```rust fn handle_connection(mut stream: TcpStream) { let buf_reader = BufReader::new(&mut stream); let request_line = buf_reader.lines().next().unwrap().unwrap(); let response = if request_line == "GET / HTTP/1.1" { create_response("HTTP/1.1 200 OK", "hello.html") } else { create_response("HTTP/1.1 404 NOT FOUND", "404.html") }; stream.write_all(response.as_bytes()).unwrap(); } fn create_response(status_line: &str, html_file: &str) -> String { let contents = fs::read_to_string(html_file).unwrap_or_else(|_| panic!("{html_file}が見つかりません")); let length = contents.len(); format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}") } ``` はじめ404が発生する理由が謎だったけど、`/favicon.ico`にアクセスしている2発目が原因だった。 根本原因は`404.html`をプロジェクトルートに配置していなかったことなので、それも追加。`localhost:7878/hoge`で`404.html`のコンテンツが表示されることも併せて確認。 ## ちょっとしたリファクタリング やはり公式でもここには触れていたようだ。公式では[[タプル (Rust)|タプル]]へ必要なパラメータを代入し、処理は`handle_connection`に引き続き書くスタイルだった。関数かするのが微妙な場合はそれもアリだな。 # シングルスレッドサーバーをマルチスレッドサーバーに変える #todo