#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