用Rust 來搭建一個多線程網絡服務器 詳細教程 Building a multiple thread web server

 https://doc.rust-lang.org/1.30.0/book/second-edition/ch20-00-final-project-a-web-server.html

用Rust 來搭建一個多線程網絡服務器,主要覆蓋

  1. 瞭解點TCP和 HTTP.  
  2. 在一個socket上監聽TCP connections  
  3. 解析很小一部分 HTTP requests.
  4. 創建適當的 HTTP response.
  5. 用 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就完成了!

 

。。。。。。。待續

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章