https://doc.rust-lang.org/1.30.0/book/second-edition/ch20-00-final-project-a-web-server.html
用Rust 來搭建一個多線程網絡服務器,主要覆蓋
- 瞭解點TCP和 HTTP.
- 在一個socket上監聽TCP connections
- 解析很小一部分 HTTP requests.
- 創建適當的 HTTP response.
- 用 thread pool 來提高服務器吞吐量.
注意:本項目是示例教程。
1. Building a Single-Threaded Web Server 搭建一個單線程web server
2.Turning Our Single-Threaded Server into a Multithreaded Server 將單線程web server 改成 多線程
3. Graceful Shutdown and Cleanup 優雅地關閉和清除
1.搭建一個單線程web server
TCP 和 HTTP 都是 request-response 協議。即一個客戶端client向服務器server發送請求request,服務器server
監聽請求request,並提供響應response給客戶端client。
TCP 是低層協議,描述了信息是如何從一個機器到另外一個機器的但並不指定內容;HTTP是高層協議,在TCP之上,定義了
請求和響應的內容。技術上講,HTTP可以和其它協議結合使用。但是大部分情況,HTTP和TCP一起使用。
1.1 首先建立一個新的項目,然後來修改代碼。
zzl@zzl-virtualbox2:~/rustprj$ cargo new hello --bin
Created binary (application) `hello` package
zzl@zzl-virtualbox2:~/rustprj$
修改src/main.rs 文件如下:
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!("Connection established!");
}
}
使用TcpListener綁定127.0.0.1:7878,監聽
TCP connections。
bind
函數有點像 new
函數,返回一個新的TcpListener
實例.
bind
函數返回 Result<T, E>
, 表明綁定有可能失敗。例如,連接80端口需要管理員特權,非管理員只能監聽1024之上
的端口。或者倆個實例綁到同一端口會衝突。 本教程以學習爲目的,所以不必擔心處理各種錯誤,只是簡單使用unwrap
來停止程序,如果有錯誤發生。
Tincoming
方式返回一個迭代器,包含一系列Stream(TcpStream). 一個stream代表一個client和server間的open connection。一個 connection代表request/response的全過程:client連接server,server生成響應,Server 關閉連接。 TcpStream將讀到client發送了什麼,然後允許將response寫給stream。
下面運行程序cargo run 。 並打開瀏覽器,在地址欄輸入127.0.0.1:7878
zzl@zzl-virtualbox2:~/rustprj/hello$ cargo run
Compiling hello v0.1.0 (/home/zzl/rustprj/hello)
Finished dev [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/hello`
Connection established!
Connection established!
Connection established!
Connection established!
1.2 讀取request
實現一個函數來讀取瀏覽器的request。創建一個新函數 new handle_connection
來處理。新函數從 TCP stream 讀取數據
並打印,這樣服務器端可以看到瀏覽器發送的數據。.
將src/main.rs 修改如下:
use std::io::prelude::*;
use std::net::TcpStream;
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();
handle_connection(stream);
}
}
fn handle_connection(mut stream: TcpStream) {
let mut buffer = [0; 512];
stream.read(&mut buffer).unwrap();
println!("Request: {}", String::from_utf8_lossy(&buffer[..]));
}
引入std::io::prelude
獲取某些 traits ,以便讀寫stream數據。在 for循環中,調用
handle_connection
函數,將stream
傳給它。.
在 handle_connection
中將stream
參數 mutable.
然後從stream讀數據. 首先定義一個buffer
存儲讀到的數據,. buffer 大小512 bytes足夠容納本節需要的數據。 其次,將它轉化成string並打印。String::from_utf8_lossy
從&[u8]
生成String
. The “lossy” 表示遇到無效的UTF-8序列,將用 �代替。
, 即 U+FFFD REPLACEMENT CHARACTER
.
下面運行程序並打開瀏覽器。
zzl@zzl-virtualbox2:~/rustprj/hello$ cargo run
Compiling hello v0.1.0 (/home/zzl/rustprj/hello)
Finished dev [unoptimized + debuginfo] target(s) in 0.89s
Running `target/debug/hello`
Request: GET / HTTP/1.1
Host: 127.0.0.1:7878
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36
Sec-Fetch-User: ?1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
1.3 HTTPrequest
HTTP 格式:
Method Request-URI HTTP-Version CRLF
headers CRLF
message-body
1.4 編寫響應 response
response 格式:
HTTP-Version Status-Code Reason-Phrase CRLF
headers CRLF
message-body
一個最簡單的響應,沒有headers,沒有body,例如 HTTP/1.1 200 OK\r\n\r\n
修改src/main.rs的連接處理函數代碼:
#![allow(unused_variables)]
fn main() {
use std::io::prelude::*;
use std::net::TcpStream;
fn handle_connection(mut stream: TcpStream) {
let mut buffer = [0; 512];
stream.read(&mut buffer).unwrap();
let response = "HTTP/1.1 200 OK\r\n\r\n";
stream.write(response.as_bytes()).unwrap();
stream.flush().unwrap();
}
}
在運行程序cargo run 和打開瀏覽器。瀏覽器是一個空白頁,沒有報錯。服務器終端也不打印數據輸出。
zzl@zzl-virtualbox2:~/rustprj/hello$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running `target/debug/hello`
1.5 返回一個HTML
編寫一個hello.html,放到src的父目錄。例如:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Hello!</title>
</head>
<body>
<h1>Hello!</h1>
<p>Hi from Rust</p>
</body>
</html>
在src/main.rs 增加 use std::fs::File;
修改其中的handle_connection函數如下:
#![allow(unused_variables)]
fn handle_connection(mut stream: TcpStream) {
let mut buffer = [0; 512];
stream.read(&mut buffer).unwrap();
let mut file = File::open("hello.html").unwrap();
let mut contents = String::new();
file.read_to_string(&mut contents).unwrap();
let response = format!("HTTP/1.1 200 OK\r\n\r\n{}", contents);
stream.write(response.as_bytes()).unwrap();
stream.flush().unwrap();
}
1.6 確認請求,並有選擇地響應。
使用if ...else. 修改handle_connection:
#![allow(unused_variables)]
fn handle_connection(mut stream: TcpStream) {
let mut buffer = [0; 512];
stream.read(&mut buffer).unwrap();
let get = b"GET / HTTP/1.1\r\n";
if buffer.starts_with(get) {
let mut file = File::open("hello.html").unwrap();
let mut contents = String::new();
file.read_to_string(&mut contents).unwrap();
let response = format!("HTTP/1.1 200 OK\r\n\r\n{}", contents);
stream.write(response.as_bytes()).unwrap();
stream.flush().unwrap();
} else {
// some other request
let status_line = "HTTP/1.1 404 NOT FOUND\r\n\r\n";
let mut file = File::open("404.html").unwrap();
let mut contents = String::new();
file.read_to_string(&mut contents).unwrap();
let response = format!("{}{}", status_line, contents);
stream.write(response.as_bytes()).unwrap();
stream.flush().unwrap();
}
}
}
然後添加一個404.html 到hello.html所在目錄。代碼如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Hello!</title>
</head>
<body>
<h1>Oops!</h1>
<p>Sorry, I don't know what you're asking for.</p>
</body>
</html>
這樣,再次運行程序。打開瀏覽器,當輸入的請求找不到,就會返回404.html。
1.7 重構代碼:
上面if...else...代碼有重複,可以簡化如下:
#![allow(unused_variables)]
fn handle_connection(mut stream: TcpStream) {
let mut buffer = [0; 512];
stream.read(&mut buffer).unwrap();
let get = b"GET / HTTP/1.1\r\n";
let (status_line, filename) = if buffer.starts_with(get) {
("HTTP/1.1 200 OK\r\n\r\n", "hello.html")
} else {
("HTTP/1.1 404 NOT FOUND\r\n\r\n", "404.html")
};
let mut file = File::open(filename).unwrap();
let mut contents = String::new();
file.read_to_string(&mut contents).unwrap();
let response = format!("{}{}", status_line, contents);
stream.write(response.as_bytes()).unwrap();
stream.flush().unwrap();
}
這樣,一個簡單的單線程 web server就完成了!
。。。。。。。待續