Rust 中單線程 Web 服務(wù)器的實(shí)現(xiàn)
Web 服務(wù)器中涉及的兩個(gè)主要協(xié)議是超文本傳輸協(xié)議(HTTP)和傳輸控制協(xié)議(TCP)。這兩種協(xié)議都是請(qǐng)求-響應(yīng)協(xié)議,這意味著客戶端發(fā)起請(qǐng)求,服務(wù)器偵聽(tīng)請(qǐng)求并向客戶端提供響應(yīng)。這些請(qǐng)求和響應(yīng)的內(nèi)容由協(xié)議定義。
TCP 是較低級(jí)別的協(xié)議,它描述了信息如何從一臺(tái)服務(wù)器傳遞到另一臺(tái)服務(wù)器的細(xì)節(jié),但沒(méi)有指定該信息是什么。HTTP 通過(guò)定義請(qǐng)求和響應(yīng)的內(nèi)容建立在 TCP 之上。在技術(shù)上可以將 HTTP 與其他協(xié)議一起使用,但在絕大多數(shù)情況下,HTTP 通過(guò) TCP 發(fā)送數(shù)據(jù)。
我們將處理 TCP 和 HTTP 請(qǐng)求和響應(yīng)的原始字節(jié)。
監(jiān)聽(tīng) TCP 連接
標(biāo)準(zhǔn)庫(kù)提供了一個(gè) std::net 模塊,可以讓我們監(jiān)聽(tīng) TCP 連接。
下面這段代碼將在本地地址 127.0.0.1:7878 上監(jiān)聽(tīng)傳入的 TCP 流。當(dāng)它收到一個(gè)傳入流時(shí),它將打印 Connection established!
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,我們可以監(jiān)聽(tīng)地址為 127.0.0.1:7878 的 TCP 連接。在地址中,冒號(hào)之前的部分是代表本地地址,7878 是端口。
bind 函數(shù)類似于 new 函數(shù),作用是監(jiān)聽(tīng)一個(gè)端口,它返回 Result<T, E>,這表明綁定有可能失敗。若成功,則得到一個(gè)新的 TcpListener 實(shí)例;若失敗,我們使用 unwrap 來(lái)停止程序。
TcpListener 上的 incoming 方法返回一個(gè)迭代器,該迭代器為我們提供一個(gè) TcpStream 類型的流。單個(gè)流表示客戶端和服務(wù)器之間的連接,在該過(guò)程中,客戶機(jī)連接到服務(wù)器,服務(wù)器生成響應(yīng),服務(wù)器關(guān)閉連接。因此,我們將從 TcpStream 中讀取以查看客戶端發(fā)送的內(nèi)容,然后將響應(yīng)寫入流以將數(shù)據(jù)發(fā)送回客戶端??偟膩?lái)說(shuō),這個(gè) for 循環(huán)將依次處理每個(gè)連接,并產(chǎn)生一系列流供我們處理。
目前,我們對(duì)流的處理包括:如果流有任何錯(cuò)誤,調(diào)用 unwrap 來(lái)終止程序;如果沒(méi)有任何錯(cuò)誤,程序?qū)⒋蛴∫粭l消息。
在終端中調(diào)用 cargo run,然后在瀏覽器中加載 127.0.0.1:7878。瀏覽器應(yīng)該顯示一個(gè)錯(cuò)誤消息,因?yàn)榉?wù)器當(dāng)前沒(méi)有發(fā)回任何數(shù)據(jù)。
但是終端上有瀏覽器連接到服務(wù)器時(shí)打印的幾條消息。
閱讀請(qǐng)求
實(shí)現(xiàn)一個(gè) handle_connection 函數(shù),從 TCP 流中讀取數(shù)據(jù)并打印出來(lái),這樣我們就可以看到從瀏覽器發(fā)送的數(shù)據(jù)。
use std::net::{TcpListener, TcpStream}; use std::io::{BufReader, prelude::*}; fn handle_connection(mut stream: TcpStream) { let buf_reader = BufReader::new(&stream); let http_request: Vec<_> = buf_reader .lines() .map(|result| result.unwrap()) .take_while(|line| !line.is_empty()) .collect(); println!("Request: {http_request:#?}"); } fn main() { let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); for stream in listener.incoming() { let stream = stream.unwrap(); handle_connection(stream); } }
在 handle_connection 函數(shù)中,我們創(chuàng)建了一個(gè)新的 BufReader 實(shí)例,該實(shí)例包裝了對(duì)流的引用。BufReader 通過(guò)為我們管理對(duì) std::io::Read trait 方法的調(diào)用來(lái)增加緩沖。
我們創(chuàng)建了一個(gè)名為 http_request 的變量來(lái)收集瀏覽器發(fā)送到服務(wù)器的請(qǐng)求行。我們通過(guò)添加 Vec<_> 類型注釋來(lái)表示希望將這些行收集到一個(gè)向量中。
BufReader 實(shí)現(xiàn)了 std::io::BufRead trait,它提供了 lines 方法。lines 方法返回一個(gè) Result<String, std::io::Error> 的迭代器,方法是在看到換行符時(shí)拆分?jǐn)?shù)據(jù)流。為了獲得每個(gè) String,我們使用 map 方法展開(kāi)每個(gè) Result。
瀏覽器通過(guò)在一行中發(fā)送兩個(gè)換行符來(lái)表示 HTTP 請(qǐng)求的結(jié)束,因此為了從流中獲得一個(gè)請(qǐng)求,我們一直讀取行,直到得到空字符串的行。一旦我們將這些行收集到 vector 中,我們將使用 #? 調(diào)試格式將它們打印出來(lái),這樣我們就可以查看 Web 瀏覽器發(fā)送給服務(wù)器的指令。
運(yùn)行程序并再次在 Web 瀏覽器中發(fā)出請(qǐng)求。我們?nèi)匀粫?huì)在瀏覽器中得到一個(gè)錯(cuò)誤頁(yè)面,但是我們的程序在終端中的輸出現(xiàn)在看起來(lái)像這樣:
Request: [ "GET / HTTP/1.1", "Host: 127.0.0.1:7878", "Connection: keep-alive", "sec-ch-ua: \"Google Chrome\";v=\"137\", \"Chromium\";v=\"137\", \"Not/A)Brand\";v=\"24\"", "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/137.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.7", "Sec-Fetch-Site: none", "Sec-Fetch-Mode: navigate", "Sec-Fetch-User: ?1", "Sec-Fetch-Dest: document", "Accept-Encoding: gzip, deflate, br, zstd", "Accept-Language: zh-CN,zh;q=0.9", ]
讓我們分解這個(gè)請(qǐng)求數(shù)據(jù)來(lái)理解瀏覽器對(duì)程序的要求。
仔細(xì)看看 HTTP 請(qǐng)求
HTTP 是一個(gè)基于文本的協(xié)議,請(qǐng)求采用以下格式:
Method Request-URI HTTP-Version CRLF headers CRLF message-body
第一行是請(qǐng)求行,包含有關(guān)客戶端請(qǐng)求內(nèi)容的信息。
請(qǐng)求行的第一部分表明正在使用的方法,例如 GET 或 POST,它描述了客戶端如何發(fā)出此請(qǐng)求。我們的客戶端使用 GET 請(qǐng)求,這意味著它正在請(qǐng)求信息。
請(qǐng)求行的下一部分是/,它指示客戶機(jī)請(qǐng)求的統(tǒng)一資源標(biāo)識(shí)符(URI)。URI 類似于 URL,但是 HTTP 規(guī)范使用術(shù)語(yǔ) URI。
請(qǐng)求行的最后一部分是客戶端使用的 HTTP 版本,然后請(qǐng)求行以 CRLF 序列 \r\n 結(jié)束,其中 \r 是回車,\n 是換行符。CRLF 序列將請(qǐng)求行與請(qǐng)求數(shù)據(jù)的其余部分分開(kāi)。
查看我們收到的請(qǐng)求行數(shù)據(jù),可以看到 GET 是方法,/ 是請(qǐng)求 URI, HTTP/1.1 是版本。
在請(qǐng)求行之后,從 Host: 開(kāi)始的其余行是請(qǐng)求頭。GET 請(qǐng)求沒(méi)有請(qǐng)求體。
現(xiàn)在我們知道了瀏覽器在請(qǐng)求什么,讓我們發(fā)回一些數(shù)據(jù)吧!
編寫響應(yīng)
我們將實(shí)現(xiàn)發(fā)送數(shù)據(jù)以響應(yīng)客戶機(jī)請(qǐng)求。HTTP 響應(yīng)的格式如下:
HTTP-Version Status-Code Reason-Phrase CRLF headers CRLF message-body
第一行是狀態(tài)行,其中包含響應(yīng)中使用的 HTTP 版本、總結(jié)請(qǐng)求結(jié)果的數(shù)字狀態(tài)碼,以及提供狀態(tài)碼文本描述的原因短語(yǔ)。在 CRLF 序列之后是任何響應(yīng)頭、另一個(gè) CRLF 序列和響應(yīng)體。
下面是一個(gè)使用 HTTP 1.1 版本的響應(yīng)示例,它的狀態(tài)碼是 200,一個(gè) OK 原因短語(yǔ),沒(méi)有響應(yīng)頭、響應(yīng)體:
HTTP/1.1 200 OK\r\n\r\n
狀態(tài)碼 200 是標(biāo)準(zhǔn)的成功響應(yīng)。讓我們將其寫入流,作為對(duì)成功請(qǐng)求的響應(yīng)。修改 handle_connection 函數(shù):
fn handle_connection(mut stream: TcpStream) { let buf_reader = BufReader::new(&stream); let http_request: Vec<_> = buf_reader .lines() .map(|result| result.unwrap()) .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(); }
as_bytes 方法將字符串?dāng)?shù)據(jù)轉(zhuǎn)換為字節(jié)。流上的 write_all 方法接受 &[u8],并將這些字節(jié)直接發(fā)送到連接。因?yàn)?write_all 操作可能失敗,所以我們像以前一樣對(duì)任何錯(cuò)誤結(jié)果使用 unwrap。
通過(guò)這些更改,讓我們運(yùn)行代碼并在瀏覽器中加載 127.0.0.1:7878。你應(yīng)該得到一個(gè)空白頁(yè)面,而不是一個(gè)錯(cuò)誤頁(yè)面。
返回真正的 HTML
讓我們實(shí)現(xiàn)不止返回一個(gè)空白頁(yè)的功能。在項(xiàng)目目錄的根目錄中創(chuàng)建新文件 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>
接著修改 handle_connection 函數(shù),讀取 HTML 文件,將其作為正文添加到響應(yīng)中,然后發(fā)送。
fn handle_connection(mut stream: TcpStream) { let buf_reader = BufReader::new(&stream); let http_request: Vec<_> = buf_reader .lines() .map(|result| result.unwrap()) .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(); }
我們使用 format! 將 hello.html 的內(nèi)容添加到響應(yīng)體中。為了確保有效的 HTTP 響應(yīng),我們添加了 Content-Length,該報(bào)頭設(shè)置為響應(yīng)體的大小,在本例中為 hello.html 的大小。
運(yùn)行這段代碼,并在瀏覽器中加載 127.0.0.1:7878。我們看到瀏覽器接收并渲染了 hello.html。
目前,我們忽略了 http_request 中的請(qǐng)求數(shù)據(jù),只是無(wú)條件地發(fā)回 HTML 文件的內(nèi)容。
我們希望根據(jù)請(qǐng)求定制響應(yīng),只響應(yīng)格式良好的請(qǐng)求。
驗(yàn)證請(qǐng)求并選擇性地響應(yīng)
讓我們添加一些功能,在返回 HTML 文件之前檢查瀏覽器是否正在請(qǐng)求 /,如果瀏覽器請(qǐng)求任何其他內(nèi)容,則返回一個(gè)錯(cuò)誤。
我們需要修改 handle_connection 函數(shù),檢查收到的請(qǐng)求的內(nèi)容,并添加 if 和 else 塊以區(qū)別對(duì)待請(qǐng)求。
// --snip-- fn handle_connection(mut stream: TcpStream) { let buf_reader = BufReader::new(&stream); let request_line = buf_reader.lines().next().unwrap().unwrap(); if request_line == "GET / HTTP/1.1" { 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(); } else { // some other request } }
我們將只查看 HTTP 請(qǐng)求的第一行,因此我們將調(diào)用 next 來(lái)從迭代器中獲取第一項(xiàng),而不是將整個(gè)請(qǐng)求讀入 vector。第一次 unwrap 處理 Option,如果迭代器沒(méi)有項(xiàng),則停止程序。第二個(gè) unwrap 處理 Result,取出請(qǐng)求內(nèi)容。
接下來(lái),我們檢查 request_line,看看它是否等于對(duì) / 路徑的 GET 請(qǐng)求的請(qǐng)求行。如果是,if 塊返回 hello.html 文件的內(nèi)容。
現(xiàn)在運(yùn)行此代碼并請(qǐng)求 127.0.0.1:7878,還是成功的。
如果發(fā)出任何其他請(qǐng)求,例如 127.0.0.1:7878/other,你將得到一個(gè)連接錯(cuò)誤。
現(xiàn)在,讓我們完善 else 塊中的代碼,返回一個(gè)狀態(tài)碼為 404、原因短語(yǔ)為 NOT FOUND 的響應(yīng),響應(yīng)體是 404.html 文件。
// --snip-- } else { let status_line = "HTTP/1.1 404 NOT FOUND"; let contents = fs::read_to_string("404.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(); }
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>
通過(guò)這些更改,再次運(yùn)行服務(wù)器。請(qǐng)求 127.0.0.1:7878 應(yīng)該返回 hello.html 的內(nèi)容。而任何其他請(qǐng)求,如 127.0.0.1:7878/other,應(yīng)該返回 404.html 中的錯(cuò)誤 HTML。
代碼重構(gòu)
目前,if 和 els e塊有很多重復(fù):它們都在讀取文件并將文件的內(nèi)容寫入流,唯一的區(qū)別是狀態(tài)行和文件名。
讓我們將這些差異提取到單獨(dú)的 if 和 else 行中,將狀態(tài)行和文件名的值分配給變量,然后使用這些變量來(lái)讀取文件并寫入響應(yīng)。
fn handle_connection(mut stream: TcpStream) { // --snip-- let (status_line, filename) = if request_line == "GET / HTTP/1.1" { ("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 length = contents.len(); let response = format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"); stream.write_all(response.as_bytes()).unwrap(); }
總結(jié)
我們只用 32 行 Rust 代碼就實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的單線程 Web 服務(wù)器,用hello.html 響應(yīng)一個(gè)請(qǐng)求,用 404.html 響應(yīng)所有其他請(qǐng)求。
目前,我們的服務(wù)器在單線程中運(yùn)行,這意味著它一次只能處理一個(gè)請(qǐng)求。在下一個(gè)項(xiàng)目中,我們先通過(guò)模擬一些慢速請(qǐng)求來(lái)檢查這是如何造成問(wèn)題的。然后我們將修復(fù)它,以便我們的服務(wù)器可以同時(shí)處理多個(gè)請(qǐng)求。
- 通過(guò)rust實(shí)現(xiàn)自己的web登錄圖片驗(yàn)證碼功能
- Rust如何使用Sauron實(shí)現(xiàn)Web界面交互
- rust中間件actix_web在項(xiàng)目中的使用實(shí)戰(zhàn)
- RustDesk?Server服務(wù)器搭建教程含api服務(wù)器和webclient服務(wù)器
- rust?創(chuàng)建多線程web?server的詳細(xì)過(guò)程
- Rust多線程Web服務(wù)器搭建過(guò)程
- 在Rust?web服務(wù)中使用Redis的方法
- Rust開(kāi)發(fā)WebAssembly在Html和Vue中的應(yīng)用小結(jié)(推薦)
相關(guān)文章
Rust中實(shí)例化動(dòng)態(tài)對(duì)象的示例詳解
這篇文章主要為大家詳細(xì)介紹了Rust中實(shí)例化動(dòng)態(tài)對(duì)象的多種方法,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2025-02-02rust交叉編譯問(wèn)題及報(bào)錯(cuò)解析
這篇文章主要為大家介紹了rust交叉編譯問(wèn)題及報(bào)錯(cuò)解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-07-07rust程序靜態(tài)編譯的兩種方法實(shí)例小結(jié)
這篇文章主要介紹了rust程序靜態(tài)編譯的兩種方法總結(jié),本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2025-05-05Rust中的Drop特性之解讀自動(dòng)化資源清理的魔法
Rust通過(guò)Drop特性實(shí)現(xiàn)了自動(dòng)清理機(jī)制,確保資源在對(duì)象超出作用域時(shí)自動(dòng)釋放,避免了手動(dòng)管理資源時(shí)可能出現(xiàn)的內(nèi)存泄漏或雙重釋放問(wèn)題,智能指針如Box、Rc和RefCell都依賴于Drop來(lái)管理資源,提供了靈活且安全的資源管理方案2025-02-02