99re热这里只有精品视频,7777色鬼xxxx欧美色妇,国产成人精品一区二三区在线观看,内射爽无广熟女亚洲,精品人妻av一区二区三区

Rust 構(gòu)建單線程 web server

2023-03-22 15:16 更新
ch20-01-single-threaded.md
commit 9c0fa2714859738ff73cbbb829592e4c037d7e46

首先讓我們創(chuàng)建一個可運行的單線程 web server,不過在開始之前,我們將快速了解一下構(gòu)建 web server 所涉及到的協(xié)議。這些協(xié)議的細(xì)節(jié)超出了本書的范疇,不過一個簡單的概括會提供我們所需的信息。

web server 中涉及到的兩個主要協(xié)議是 超文本傳輸協(xié)議Hypertext Transfer Protocol,HTTP)和 傳輸控制協(xié)議Transmission Control ProtocolTCP)。這兩者都是 請求-響應(yīng)request-response)協(xié)議,也就是說,有 客戶端client)來初始化請求,并有 服務(wù)端server)監(jiān)聽請求并向客戶端提供響應(yīng)。請求與響應(yīng)的內(nèi)容由協(xié)議本身定義。

TCP 是一個底層協(xié)議,它描述了信息如何從一個 server 到另一個的細(xì)節(jié),不過其并不指定信息是什么。HTTP 構(gòu)建于 TCP 之上,它定義了請求和響應(yīng)的內(nèi)容。為此,技術(shù)上講可將 HTTP 用于其他協(xié)議之上,不過對于絕大部分情況,HTTP 通過 TCP 傳輸。我們將要做的就是處理 TCP 和 HTTP 請求與響應(yīng)的原始字節(jié)數(shù)據(jù)。

監(jiān)聽 TCP 連接

所以我們的 web server 所需做的第一件事便是能夠監(jiān)聽 TCP 連接。標(biāo)準(zhǔn)庫提供了 std::net 模塊處理這些功能。讓我們一如既往新建一個項目:

$ cargo new hello
     Created binary (application) `hello` project
$ cd hello

并在 src/main.rs 輸入示例 20-1 中的代碼作為開始。這段代碼會在地址 127.0.0.1:7878 上監(jiān)聽傳入的 TCP 流。當(dāng)獲取到傳入的流,它會打印出 Connection established!

文件名: 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!");
    }
}

示例 20-1: 監(jiān)聽傳入的流并在接收到流時打印信息

TcpListener 用于監(jiān)聽 TCP 連接。我們選擇監(jiān)聽地址 127.0.0.1:7878。將這個地址拆開,冒號之前的部分是一個代表本機的 IP 地址(這個地址在每臺計算機上都相同,并不特指作者的計算機),而 7878 是端口。選擇這個端口出于兩個原因:通常 HTTP 接受這個端口而且 7878 在電話上打出來就是 "rust"(譯者注:九宮格鍵盤上的英文)。

在這個場景中 bind 函數(shù)類似于 new 函數(shù),在這里它返回一個新的 TcpListener 實例。這個函數(shù)叫做 bind 是因為,在網(wǎng)絡(luò)領(lǐng)域,連接到監(jiān)聽端口被稱為 “綁定到一個端口”(“binding to a port”)

bind 函數(shù)返回 Result<T, E>,這表明綁定可能會失敗,例如,連接 80 端口需要管理員權(quán)限(非管理員用戶只能監(jiān)聽大于 1024 的端口),所以如果不是管理員嘗試連接 80 端口,則會綁定失敗。另一個例子是如果運行兩個此程序的實例這樣會有兩個程序監(jiān)聽相同的端口,綁定會失敗。因為我們是出于學(xué)習(xí)目的來編寫一個基礎(chǔ)的 server,將不用關(guān)心處理這類錯誤,使用 unwrap 在出現(xiàn)這些情況時直接停止程序。

TcpListener 的 incoming 方法返回一個迭代器,它提供了一系列的流(更準(zhǔn)確的說是 TcpStream 類型的流)。stream)代表一個客戶端和服務(wù)端之間打開的連接。連接connection)代表客戶端連接服務(wù)端、服務(wù)端生成響應(yīng)以及服務(wù)端關(guān)閉連接的全部請求 / 響應(yīng)過程。為此,TcpStream 允許我們讀取它來查看客戶端發(fā)送了什么,并可以編寫響應(yīng)??傮w來說,這個 for 循環(huán)會依次處理每個連接并產(chǎn)生一系列的流供我們處理。

目前為止,處理流的過程包含 unwrap 調(diào)用,如果出現(xiàn)任何錯誤會終止程序,如果沒有任何錯誤,則打印出信息。下一個示例我們將為成功的情況增加更多功能。當(dāng)客戶端連接到服務(wù)端時 incoming 方法返回錯誤是可能的,因為我們實際上沒有遍歷連接,而是遍歷 連接嘗試connection attempts)。連接可能會因為很多原因不能成功,大部分是操作系統(tǒng)相關(guān)的。例如,很多系統(tǒng)限制同時打開的連接數(shù);新連接嘗試產(chǎn)生錯誤,直到一些打開的連接關(guān)閉為止。

讓我們試試這段代碼!首先在終端執(zhí)行 cargo run,接著在瀏覽器中加載 127.0.0.1:7878。瀏覽器會顯示出看起來類似于“連接重置”(“Connection reset”)的錯誤信息,因為 server 目前并沒響應(yīng)任何數(shù)據(jù)。但是如果我們觀察終端,會發(fā)現(xiàn)當(dāng)瀏覽器連接 server 時會打印出一系列的信息!

     Running `target/debug/hello`
Connection established!
Connection established!
Connection established!

有時會看到對于一次瀏覽器請求會打印出多條信息;這可能是因為瀏覽器在請求頁面的同時還請求了其他資源,比如出現(xiàn)在瀏覽器 tab 標(biāo)簽中的 favicon.ico。

這也可能是因為瀏覽器嘗試多次連接 server,因為 server 沒有響應(yīng)任何數(shù)據(jù)。當(dāng) stream 在循環(huán)的結(jié)尾離開作用域并被丟棄,其連接將被關(guān)閉,作為 drop 實現(xiàn)的一部分。瀏覽器有時通過重連來處理關(guān)閉的連接,因為這些問題可能是暫時的?,F(xiàn)在重要的是我們成功的處理了 TCP 連接!

記得當(dāng)運行完特定版本的代碼后使用 ctrl-C 來停止程序。并在做出最新的代碼修改之后執(zhí)行 cargo run 重啟服務(wù)。

讀取請求

讓我們實現(xiàn)讀取來自瀏覽器請求的功能!為了分離獲取連接和接下來對連接的操作的相關(guān)內(nèi)容,我們將開始一個新函數(shù)來處理連接。在這個新的 handle_connection 函數(shù)中,我們從 TCP 流中讀取數(shù)據(jù)并打印出來以便觀察瀏覽器發(fā)送過來的數(shù)據(jù)。將代碼修改為如示例 20-2 所示:

文件名: src/main.rs

use std::io::prelude::*;
use std::net::TcpListener;
use std::net::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 mut buffer = [0; 1024];

    stream.read(&mut buffer).unwrap();

    println!("Request: {}", String::from_utf8_lossy(&buffer[..]));
}

示例 20-2: 讀取 TcpStream 并打印數(shù)據(jù)

這里將 std::io::prelude 引入作用域來獲取讀寫流所需的特定 trait。在 main 函數(shù)的 for 循環(huán)中,相比獲取到連接時打印信息,現(xiàn)在調(diào)用新的 handle_connection 函數(shù)并向其傳遞 stream。

在 handle_connection 中,stream 參數(shù)是可變的。這是因為 TcpStream 實例在內(nèi)部記錄了所返回的數(shù)據(jù)。它可能讀取了多于我們請求的數(shù)據(jù)并保存它們以備下一次請求數(shù)據(jù)。因此它需要是 mut 的因為其內(nèi)部狀態(tài)可能會改變;通常我們認(rèn)為 “讀取” 不需要可變性,不過在這個例子中則需要 mut 關(guān)鍵字。

接下來,需要實際讀取流。這里分兩步進(jìn)行:首先,在棧上聲明一個 buffer 來存放讀取到的數(shù)據(jù)。這里創(chuàng)建了一個 1024 字節(jié)的緩沖區(qū),它足以存放基本請求的數(shù)據(jù)并滿足本章的目的需要。如果希望處理任意大小的請求,緩沖區(qū)管理將更為復(fù)雜,不過現(xiàn)在一切從簡。接著將緩沖區(qū)傳遞給 stream.read ,它會從 TcpStream 中讀取字節(jié)并放入緩沖區(qū)中。

接下來將緩沖區(qū)中的字節(jié)轉(zhuǎn)換為字符串并打印出來。String::from_utf8_lossy 函數(shù)獲取一個 &[u8] 并產(chǎn)生一個 String。函數(shù)名的 “l(fā)ossy” 部分來源于當(dāng)其遇到無效的 UTF-8 序列時的行為:它使用 ?,U+FFFD REPLACEMENT CHARACTER,來代替無效序列。你可能會在緩沖區(qū)的剩余部分看到這些替代字符,因為他們沒有被請求數(shù)據(jù)填滿。

讓我們試一試!啟動程序并再次在瀏覽器中發(fā)起請求。注意瀏覽器中仍然會出現(xiàn)錯誤頁面,不過終端中程序的輸出現(xiàn)在看起來像這樣:

$ cargo run
   Compiling hello v0.1.0 (file:///projects/hello)
    Finished dev [unoptimized + debuginfo] target(s) in 0.42s
     Running `target/debug/hello`
Request: GET / HTTP/1.1
Host: 127.0.0.1:7878
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:52.0) Gecko/20100101
Firefox/52.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Upgrade-Insecure-Requests: 1
????????????????????????????????????

根據(jù)使用的瀏覽器不同可能會出現(xiàn)稍微不同的數(shù)據(jù)?,F(xiàn)在我們打印出了請求數(shù)據(jù),可以通過觀察 Request: GET 之后的路徑來解釋為何會從瀏覽器得到多個連接。如果重復(fù)的連接都是請求 /,就知道了瀏覽器嘗試重復(fù)獲取 / 因為它沒有從程序得到響應(yīng)。

拆開請求數(shù)據(jù)來理解瀏覽器向程序請求了什么。

仔細(xì)觀察 HTTP 請求

HTTP 是一個基于文本的協(xié)議,同時一個請求有如下格式:

Method Request-URI HTTP-Version CRLF
headers CRLF
message-body

第一行叫做 請求行request line),它存放了客戶端請求了什么的信息。請求行的第一部分是所使用的 method,比如 GET 或 POST,這描述了客戶端如何進(jìn)行請求。這里客戶端使用了 GET 請求。

請求行接下來的部分是 /,它代表客戶端請求的 統(tǒng)一資源標(biāo)識符Uniform Resource IdentifierURI) —— URI 大體上類似,但也不完全類似于 URL(統(tǒng)一資源定位符,Uniform Resource Locators)。URI 和 URL 之間的區(qū)別對于本章的目的來說并不重要,不過 HTTP 規(guī)范使用術(shù)語 URI,所以這里可以簡單的將 URL 理解為 URI。

最后一部分是客戶端使用的HTTP版本,然后請求行以 CRLF序列 (CRLF代表回車和換行,carriage return line feed,這是打字機時代的術(shù)語?。┙Y(jié)束。CRLF序列也可以寫成\r\n,其中\r是回車符,\n是換行符。 CRLF序列將請求行與其余請求數(shù)據(jù)分開。 請注意,打印CRLF時,我們會看到一個新行,而不是\r\n。

觀察目前運行程序所接收到的數(shù)據(jù)的請求行,可以看到 GET 是 method,/ 是請求 URI,而 HTTP/1.1 是版本。

從 Host: 開始的其余的行是 headers;GET 請求沒有 body。

如果你希望的話,嘗試用不同的瀏覽器發(fā)送請求,或請求不同的地址,比如 127.0.0.1:7878/test,來觀察請求數(shù)據(jù)如何變化。

現(xiàn)在我們知道了瀏覽器請求了什么。讓我們返回一些數(shù)據(jù)!

編寫響應(yīng)

我們將實現(xiàn)在客戶端請求的響應(yīng)中發(fā)送數(shù)據(jù)的功能。響應(yīng)有如下格式:

HTTP-Version Status-Code Reason-Phrase CRLF
headers CRLF
message-body

第一行叫做 狀態(tài)行status line),它包含響應(yīng)的 HTTP 版本、一個數(shù)字狀態(tài)碼用以總結(jié)請求的結(jié)果和一個描述之前狀態(tài)碼的文本原因短語。CRLF 序列之后是任意 header,另一個 CRLF 序列,和響應(yīng)的 body。

這里是一個使用 HTTP 1.1 版本的響應(yīng)例子,其狀態(tài)碼為 200,原因短語為 OK,沒有 header,也沒有 body:

HTTP/1.1 200 OK\r\n\r\n

狀態(tài)碼 200 是一個標(biāo)準(zhǔn)的成功響應(yīng)。這些文本是一個微型的成功 HTTP 響應(yīng)。讓我們將這些文本寫入流作為成功請求的響應(yīng)!在 handle_connection 函數(shù)中,我們需要去掉打印請求數(shù)據(jù)的 println!,并替換為示例 20-3 中的代碼:

文件名: src/main.rs

fn handle_connection(mut stream: TcpStream) {
    let mut buffer = [0; 1024];

    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();
}

示例 20-3: 將一個微型成功 HTTP 響應(yīng)寫入流

新代碼中的第一行定義了變量 response 來存放將要返回的成功響應(yīng)的數(shù)據(jù)。接著,在 response 上調(diào)用 as_bytes,因為 stream 的 write 方法獲取一個 &[u8] 并直接將這些字節(jié)發(fā)送給連接。

因為 write 操作可能會失敗,所以像之前那樣對任何錯誤結(jié)果使用 unwrap。同理,在真實世界的應(yīng)用中這里需要添加錯誤處理。最后,flush 會等待并阻塞程序執(zhí)行直到所有字節(jié)都被寫入連接中;TcpStream 包含一個內(nèi)部緩沖區(qū)來最小化對底層操作系統(tǒng)的調(diào)用。

有了這些修改,運行我們的代碼并進(jìn)行請求!我們不再向終端打印任何數(shù)據(jù),所以不會再看到除了 Cargo 以外的任何輸出。不過當(dāng)在瀏覽器中加載 127.0.0.1:7878 時,會得到一個空頁面而不是錯誤。太棒了!我們剛剛手寫了一個 HTTP 請求與響應(yīng)。

返回真正的 HTML

讓我們實現(xiàn)不只是返回空頁面的功能。在項目根目錄創(chuàng)建一個新文件,hello.html —— 也就是說,不是在 src 目錄。在此可以放入任何你期望的 HTML;列表 20-4 展示了一個可能的文本:

文件名: hello.html

<!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>

示例 20-4: 一個簡單的 HTML 文件用來作為響應(yīng)

這是一個極小化的 HTML5 文檔,它有一個標(biāo)題和一小段文本。為了在 server 接受請求時返回它,需要如示例 20-5 所示修改 handle_connection 來讀取 HTML 文件,將其加入到響應(yīng)的 body 中,并發(fā)送:

文件名: src/main.rs

use std::fs;
// --snip--

fn handle_connection(mut stream: TcpStream) {
    let mut buffer = [0; 1024];
    stream.read(&mut buffer).unwrap();

    let contents = fs::read_to_string("hello.html").unwrap();

    let response = format!(
        "HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n{}",
        contents.len(),
        contents
    );

    stream.write(response.as_bytes()).unwrap();
    stream.flush().unwrap();
}

示例 20-5: 將 hello.html 的內(nèi)容作為響應(yīng) body 發(fā)送

在開頭增加了一行來將標(biāo)準(zhǔn)庫中的 File 引入作用域。打開和讀取文件的代碼應(yīng)該看起來很熟悉,因為第十二章 I/O 項目的示例 12-4 中讀取文件內(nèi)容時出現(xiàn)過類似的代碼。

接下來,使用 format! 將文件內(nèi)容加入到將要寫入流的成功響應(yīng)的 body 中。

使用 cargo run 運行程序,在瀏覽器加載 127.0.0.1:7878,你應(yīng)該會看到渲染出來的 HTML 文件!

目前忽略了 buffer 中的請求數(shù)據(jù)并無條件的發(fā)送了 HTML 文件的內(nèi)容。這意味著如果嘗試在瀏覽器中請求 127.0.0.1:7878/something-else 也會得到同樣的 HTML 響應(yīng)。如此其作用是非常有限的,也不是大部分 server 所做的;讓我們檢查請求并只對格式良好(well-formed)的請求 / 發(fā)送 HTML 文件。

驗證請求并有選擇的進(jìn)行響應(yīng)

目前我們的 web server 不管客戶端請求什么都會返回相同的 HTML 文件。讓我們增加在返回 HTML 文件前檢查瀏覽器是否請求 /,并在其請求任何其他內(nèi)容時返回錯誤的功能。為此需要如示例 20-6 那樣修改 handle_connection。新代碼接收到的請求的內(nèi)容與已知的 / 請求的一部分做比較,并增加了 if 和 else 塊來區(qū)別處理請求:

文件名: src/main.rs

// --snip--

fn handle_connection(mut stream: TcpStream) {
    let mut buffer = [0; 1024];
    stream.read(&mut buffer).unwrap();

    let get = b"GET / HTTP/1.1\r\n";

    if buffer.starts_with(get) {
        let contents = fs::read_to_string("hello.html").unwrap();

        let response = format!(
            "HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n{}",
            contents.len(),
            contents
        );

        stream.write(response.as_bytes()).unwrap();
        stream.flush().unwrap();
    } else {
        // some other request
    }
}

示例 20-6: 匹配請求并區(qū)別處理 / 請求與其他請求

首先,將與 / 請求相關(guān)的數(shù)據(jù)硬編碼進(jìn)變量 get。因為我們將原始字節(jié)讀取進(jìn)了緩沖區(qū),所以在 get 的數(shù)據(jù)開頭增加 b"" 字節(jié)字符串語法將其轉(zhuǎn)換為字節(jié)字符串。接著檢查 buffer 是否以 get 中的字節(jié)開頭。如果是,這就是一個格式良好的 / 請求,也就是 if 塊中期望處理的成功情況,并會返回 HTML 文件內(nèi)容的代碼。

如果 buffer  以 get 中的字節(jié)開頭,就說明接收的是其他請求。之后會在 else 塊中增加代碼來響應(yīng)所有其他請求。

現(xiàn)在如果運行代碼并請求 127.0.0.1:7878,就會得到 hello.html 中的 HTML。如果進(jìn)行任何其他請求,比如 127.0.0.1:7878/something-else,則會得到像運行示例 20-1 和 20-2 中代碼那樣的連接錯誤。

現(xiàn)在向示例 20-7 的 else 塊增加代碼來返回一個帶有 404 狀態(tài)碼的響應(yīng),這代表了所請求的內(nèi)容沒有找到。接著也會返回一個 HTML 向瀏覽器終端用戶表明此意:

文件名: src/main.rs

    // --snip--
    } else {
        let status_line = "HTTP/1.1 404 NOT FOUND";
        let contents = fs::read_to_string("404.html").unwrap();

        let response = format!(
            "{}\r\nContent-Length: {}\r\n\r\n{}",
            status_line,
            contents.len(),
            contents
        );

        stream.write(response.as_bytes()).unwrap();
        stream.flush().unwrap();
    }

示例 20-7: 對于任何不是 / 的請求返回 404 狀態(tài)碼的響應(yīng)和錯誤頁面

這里,響應(yīng)的狀態(tài)行有狀態(tài)碼 404 和原因短語 NOT FOUND。仍然沒有返回任何 header,而其 body 將是 404.html 文件中的 HTML。需要在 hello.html 同級目錄創(chuàng)建 404.html 文件作為錯誤頁面;這一次也可以隨意使用任何 HTML 或使用示例 20-8 中的示例 HTML:

文件名: 404.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>

示例 20-8: 任何 404 響應(yīng)所返回錯誤頁面內(nèi)容樣例

有了這些修改,再次運行 server。請求 127.0.0.1:7878 應(yīng)該會返回 hello.html 的內(nèi)容,而對于任何其他請求,比如 127.0.0.1:7878/foo,應(yīng)該會返回 404.html 中的錯誤 HTML!

少量代碼重構(gòu)

目前 if 和 else 塊中的代碼有很多的重復(fù):他們都讀取文件并將其內(nèi)容寫入流。唯一的區(qū)別是狀態(tài)行和文件名。為了使代碼更為簡明,將這些區(qū)別分別提取到一行 if 和 else 中,對狀態(tài)行和文件名變量賦值;然后在讀取文件和寫入響應(yīng)的代碼中無條件的使用這些變量。重構(gòu)后取代了大段 if 和 else 塊代碼后的結(jié)果如示例 20-9 所示:

文件名: src/main.rs

// --snip--

fn handle_connection(mut stream: TcpStream) {
    // --snip--

    let (status_line, filename) = if buffer.starts_with(get) {
        ("HTTP/1.1 200 OK", "hello.html")
    } else {
        ("HTTP/1.1 404 NOT FOUND", "404.html")
    };

    let contents = fs::read_to_string(filename).unwrap();

    let response = format!(
        "{}\r\nContent-Length: {}\r\n\r\n{}",
        status_line,
        contents.len(),
        contents
    );

    stream.write(response.as_bytes()).unwrap();
    stream.flush().unwrap();
}

示例 20-9: 重構(gòu)使得 if 和 else 塊中只包含兩個情況所不同的代碼

現(xiàn)在 if 和 else 塊所做的唯一的事就是在一個元組中返回合適的狀態(tài)行和文件名的值;接著使用第十八章講到的使用模式的 let 語句通過解構(gòu)元組的兩部分為 filename 和 header 賦值。

之前讀取文件和寫入響應(yīng)的冗余代碼現(xiàn)在位于 if 和 else 塊之外,并會使用變量 status_line 和 filename。這樣更易于觀察這兩種情況真正有何不同,還意味著如果需要改變?nèi)绾巫x取文件或?qū)懭腠憫?yīng)時只需要更新一處的代碼。示例 20-9 中代碼的行為與示例 20-8 完全一樣。

好極了!我們有了一個 40 行左右 Rust 代碼的小而簡單的 server,它對一個請求返回頁面內(nèi)容而對所有其他請求返回 404 響應(yīng)。

目前 server 運行于單線程中,它一次只能處理一個請求。讓我們模擬一些慢請求來看看這如何會成為一個問題,并進(jìn)行修復(fù)以便 server 可以一次處理多個請求。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號