深入理解 TCP 和 HTTP 等底層網路協定,對於構建穩健的 Web 應用至關重要。這些協定是現代網路應用程式根本,從 RESTful API 到 gRPC 和 WebSockets,都建立在 HTTP 和 TCP 之上。本文將引導你使用 Rust 語言,逐步建構一個基礎的 Web 伺服器,並闡述 TCP/IP 模型的各層功能,以及如何在 Rust 中實作 TCP 伺服器和客戶端。我們將從最基本的 TCP 伺服器開始,逐步新增功能,最終實作伺服器與客戶端之間的雙向通訊。過程中,我們還會探討 Rust 的錯誤處理機制,包含 Result 型別和 unwrap() 方法的運用,並開始設計 HTTP 函式庫,作為後續建構完整 Web 伺服器的基礎。
從零開始撰寫基礎的Web伺服器
本章涵蓋以下主題:
- 在Rust中撰寫TCP伺服器
- 在Rust中撰寫HTTP伺服器
在本章中,您將深入瞭解使用Rust進行TCP和HTTP通訊。這些通訊協定通常透過更高層級的函式庫和框架被抽象化,開發者可以直接用來建立Web應用程式。為什麼學習這些底層協定如此重要?這是一個合理的問題。學習使用TCP和HTTP至關重要,因為它們構成了大多數網路通訊的基礎。流行的應用程式通訊協定和技術,如REST、gRPC和WebSockets,都使用HTTP和TCP作為傳輸層。設計和建立基本的TCP和HTTP伺服器將使您對設計、開發和故障排除更高層級的應用程式後端服務充滿信心。
網路模型
現代應用程式被構建為一組獨立的元件和服務,有些屬於前端,有些屬於後端,有些則是分散式軟體基礎設施的一部分。當我們有獨立的元件時,問題就出現了:這些元件如何相互通訊?客戶端(網頁瀏覽器或行動應用程式)如何與後端服務通訊?後端服務如何與軟體基礎設施(如資料函式庫)通訊?這就是網路模型的作用。
網路模型描述了訊息的傳送者和接收者之間的通訊方式。它解決了諸如訊息應該以何種格式傳送和接收、訊息如何分解成位元組進行實體資料傳輸、如果資料包未到達目的地應如何處理錯誤等問題。OSI模型是最流行的網路模型,它被定義為一個包含七個層級的全面框架。但就網際網路通訊而言,一個簡化的四層模型——TCP/IP模型——通常足以描述客戶端發出請求與處理該請求的伺服器之間的通訊。
TCP/IP模型
TCP/IP模型(如圖2.1所示)是一套簡化的網際網路通訊標準和協定。它被組織成四個抽象層:網路存取層、網路層、傳輸層和應用層,在每個層中可以使用不同的網路協定。該模型以其所根據的兩個主要協定命名:傳輸控制協定(TCP)和網際網路協定(IP)。需要注意的是,TCP/IP模型的四個層相互補充,以確保訊息從傳送程式成功傳送到接收程式。
圖2.1 TCP/IP網路模型
圖表翻譯: 此圖示描述了TCP/IP網路模型的四層結構,包括網路存取層、網路層、傳輸層和應用層,每一層都有其特定的功能和作用,共同確保了網際網路通訊的順暢進行。
各層的功能
- 應用層:應用層是抽象層次最高的。它理解訊息的語義。例如,網頁瀏覽器和網頁伺服器使用HTTP通訊,或者電子郵件客戶端和電子郵件伺服器使用SMTP(簡單郵件傳輸協定)通訊。還有其他諸如DNS(網域名稱服務)和FTP(檔案傳輸協定)等協定。所有這些都稱為應用層協定,因為它們處理特定的使用者應用程式,如網頁瀏覽、電子郵件或檔案傳輸。在本文中,我們將主要關注應用層上的HTTP協定。
// 以下是一個簡單的TCP伺服器的程式碼範例
use std::net::{TcpListener, TcpStream};
use std::io::{Read, Write};
fn main() -> std::io::Result<()> {
let listener = TcpListener::bind("127.0.0.1:8080")?;
println!("Server listening on port 8080");
for stream in listener.incoming() {
handle_connection(stream?);
}
Ok(())
}
fn handle_connection(mut stream: TcpStream) {
let mut buffer = [0; 512];
stream.read(&mut buffer).unwrap();
println!("Received: {}", String::from_utf8_lossy(&buffer));
let response = "HTTP/1.1 200 OK\r\n\r\nHello, world!";
stream.write(response.as_bytes()).unwrap();
stream.flush().unwrap();
}
內容解密:
此段程式碼實作了一個簡單的TCP伺服器。首先,它在本地的8080埠上監聽連線請求。當接收到客戶端的連線請求時,handle_connection函式被呼叫來處理這個連線。在這個函式中,伺服器首先讀取客戶端傳送來的資料,並將其列印到控制檯。然後,伺服器向客戶端傳送一個簡單的HTTP回應,回應內容為"Hello, world!"。最後,呼叫flush方法確保資料被傳送到客戶端。
重點整理
- 本章節介紹瞭如何使用Rust語言從零開始建立一個基本的Web伺服器。
- 重點講解了TCP/IP模型及其在網際網路通訊中的作用。
- 提供了簡單的TCP伺服器實作範例,並對程式碼進行了解析。
TCP/IP 網路模型與 Rust 中的 TCP 實作
TCP/IP 網路模型概述
TCP/IP 網路模型是一種四層架構,分別為應用層、傳輸層、網路層和網路存取層。每一層都有其特定的功能和協定。
各層的功能
- 應用層:處理具有特定語義的訊息,如 HTTP 請求。
- 傳輸層:提供可靠的端對端通訊,主要協定包括 TCP 和 UDP。TCP 是導向連線的協定,保證資料的可靠傳輸;UDP 是無連線的協定,速度較快但不保證資料到達。
- 網路層:負責使用 IP 位址和路由器在網路中定位和路由資料包。
- 網路存取層:負責透過實體鏈路(如網路卡)在主機之間傳輸資料。
在 Rust 中撰寫 TCP 伺服器
設計 TCP/IP 通訊流程
Rust 的標準函式庫透過 std::net 模組提供網路相關的功能,主要使用 TcpListener 和 TcpStream 這兩個資料結構。
TcpListener用於建立 TCP 伺服器,並繫結到特定的埠。TcpStream代表一個位元組流,可以用於讀取和寫入資料。
建立 TCP 伺服器範例
use std::net::TcpListener;
let listener = TcpListener::bind("127.0.0.1:3000").unwrap();
for stream in listener.incoming() {
let mut stream = stream.unwrap();
let mut buffer = [0; 1024];
stream.read(&mut buffer).unwrap();
println!("Received: {}", String::from_utf8_lossy(&buffer));
let message = "Hello, client!".as_bytes();
stream.write(message).unwrap();
}
內容解密:
- 使用
TcpListener::bind將伺服器繫結到127.0.0.1:3000。 listener.incoming()傳回一個迭代器,用於接收連線請求。- 對每個連線,讀取客戶端傳送的資料到
buffer中,並列印出來。 - 向客戶端傳送訊息 “Hello, client!"。
建立 TCP 客戶端範例
use std::net::TcpStream;
let mut stream = TcpStream::connect("127.0.0.1:3000").unwrap();
let message = "Hello, server!".as_bytes();
stream.write(message).unwrap();
let mut buffer = [0; 1024];
stream.read(&mut buffer).unwrap();
println!("Received: {}", String::from_utf8_lossy(&buffer));
內容解密:
- 使用
TcpStream::connect連線到伺服器127.0.0.1:3000。 - 向伺服器傳送訊息 “Hello, server!"。
- 讀取伺服器的回應並列印出來。
專案結構與 Cargo 工作空間
為了管理多個相關的 Rust 專案,可以使用 Cargo 工作空間。工作空間是一個容器專案,可以包含多個子專案。
建立工作空間和子專案
cargo new scenario1 && cd scenario1
cargo new tcpserver
cargo new tcpclient
cargo new httpserver
cargo new --lib http
在 scenario1/Cargo.toml 中宣告工作空間和成員:
[workspace]
members = [
"tcpserver",
"tcpclient",
"http",
"httpserver",
]
這種結構便於管理多個相關專案,並將它們儲存在同一個 Git 倉函式庫中。
建立TCP伺服器與客戶端通訊例項
在現代網路程式設計中,TCP(Transmission Control Protocol)是一種常見且可靠的資料傳輸協定。本章節將透過Rust語言實作一個簡單的TCP伺服器與客戶端,並逐步解析其實作細節。
迭代1:基本TCP伺服器實作
首先,我們來建立一個基本的TCP伺服器。在tcpserver資料夾中,修改src/main.rs檔案如下:
use std::net::TcpListener;
fn main() {
let connection_listener = TcpListener::bind("127.0.0.1:3000").unwrap();
println!("Running on port 3000");
for stream in connection_listener.incoming() {
let _stream = stream.unwrap();
println!("Connection established");
}
}
內容解密:
TcpListener::bind("127.0.0.1:3000").unwrap():將伺服器繫結到本地端的3000埠。unwrap()方法用於處理可能的錯誤,如果繫結失敗,程式將會panic。connection_listener.incoming():監聽來自客戶端的連線請求,傳回一個迭代器。stream.unwrap():取得連線的TcpStream物件,如果連線過程中發生錯誤,程式將會panic。
執行以下指令啟動伺服器:
cargo run -p tcpserver
伺服器啟動後,將會在終端機上顯示「Running on port 3000」。
建立TCP客戶端
接下來,我們需要建立一個TCP客戶端來連線到剛剛建立的TCP伺服器。在tcpclient/src/main.rs中加入以下程式碼:
use std::net::TcpStream;
fn main() {
let _stream = TcpStream::connect("localhost:3000").unwrap();
}
內容解密:
TcpStream::connect("localhost:3000").unwrap():客戶端嘗試連線到本地端3000埠的伺服器。同樣地,unwrap()用於處理可能的連線錯誤。
執行以下指令啟動客戶端:
cargo run -p tcpclient
當客戶端成功連線到伺服器時,伺服器的終端機上將會顯示「Connection established」。
迭代2:實作雙向通訊
為了讓伺服器能夠回應客戶端的訊息,我們需要修改tcpserver/src/main.rs如下:
use std::io::{Read, Write};
use std::net::TcpListener;
fn main() {
let connection_listener = TcpListener::bind("127.0.0.1:3000").unwrap();
println!("Running on port 3000");
for stream in connection_listener.incoming() {
let mut stream = stream.unwrap();
println!("Connection established");
let mut buffer = [0; 1024];
stream.read(&mut buffer).unwrap();
stream.write(&mut buffer).unwrap();
}
}
內容解密:
use std::io::{Read, Write};:引入Read和Writetraits,使得TcpStream能夠進行讀寫操作。let mut stream = stream.unwrap();:將stream宣告為可變,以便進行讀寫操作。stream.read(&mut buffer).unwrap();:從客戶端讀取資料到buffer中。stream.write(&mut buffer).unwrap();:將讀取到的資料回寫給客戶端。
接著,修改tcpclient/src/main.rs以傳送訊息並接收伺服器的回應:
use std::io::{Read, Write};
use std::net::TcpStream;
use std::str;
fn main() {
let mut stream = TcpStream::connect("localhost:3000").unwrap();
stream.write("Hello".as_bytes()).unwrap();
let mut buffer = [0; 5];
stream.read(&mut buffer).unwrap();
println!("Got response from server:{:?}", str::from_utf8(&buffer).unwrap());
}
內容解密:
stream.write("Hello".as_bytes()).unwrap();:向伺服器傳送「Hello」訊息。stream.read(&mut buffer).unwrap();:讀取伺服器的回應。str::from_utf8(&buffer).unwrap():將收到的位元組資料轉換為UTF-8字串。
執行客戶端後,將會在客戶端終端機上顯示從伺服器接收到的回應訊息。
Read和Write Traits
在Rust中,Read和Write traits定義了資料讀寫的行為。任何實作了這些traits的型別都可以進行相應的操作。例如,TcpStream、File等都實作了這些traits。
Readtrait允許從來源讀取位元組資料,需要實作read()方法。Writetrait代表可以寫入位元組資料的目標,需要實作write()和flush()方法。
透過使用這些traits,我們可以對不同的資料來源和目標進行統一的操作,使得程式碼更加靈活和通用。
此圖示顯示了TCP伺服器與客戶端的互動流程:
@startuml
note
無法自動轉換的 Plantuml 圖表
請手動檢查和調整
@enduml圖表翻譯: 此圖示呈現了TCP客戶端與伺服器之間的互動過程。首先,客戶端發起連線請求,伺服器確認連線後,客戶端傳送訊息「Hello」。伺服器接收到訊息後,將其回傳給客戶端,最後客戶端讀取並顯示收到的回應。整個過程展示了TCP雙向通訊的基本流程。
使用Rust撰寫TCP伺服器與客戶端
在前面的章節中,我們已經成功地在Rust中實作了TCP伺服器和客戶端之間的通訊。在本文中,我們將更深入地探討Rust中的錯誤處理機制,並介紹如何使用Result型別和unwrap()方法。
Result型別和unwrap()方法
在Rust中,慣用的做法是讓可能失敗的函式或方法傳回一個Result<T, E>型別。這意味著Result型別包裝了另一個資料型別T(在成功時),或者包裝了一個Error型別(在失敗時),然後將其傳回給呼叫函式。呼叫函式隨後檢查Result型別並解封裝它,以接收型別為T的值或型別為Error的錯誤,以便進一步處理。
unwrap()方法的使用
到目前為止,我們已經在多個地方使用了unwrap()方法來檢索標準函式庫方法傳回的Result物件中嵌入的值。unwrap()方法在操作成功時傳回型別為T的值,或者在出現錯誤時引發panic。在真實世界的應用程式中,這不是正確的方法,因為Rust中的Result型別用於可還原的失敗,而panic則用於不可還原的失敗。然而,為了簡化學習過程,我們在這裡使用了它。我們將在後面的章節中介紹正確的錯誤處理方法。
使用Rust撰寫HTTP伺服器
在本文中,我們將使用Rust構建一個能夠與HTTP訊息進行通訊的Web伺服器。由於Rust沒有內建的HTTP支援,因此我們將從頭開始編寫一個HTTP函式庫。透過這樣做,您將學習如何使用Rust開發現代Web應用程式所依賴的底層函式庫和伺服器。
HTTP伺服器的設計
我們的Web伺服器將具有四個元件:伺服器、路由器、處理程式和HTTP函式庫。每個元件都有特定的職責,符合單一責任原則(SRP)。伺服器監聽傳入的TCP位元組流。HTTP函式庫解釋位元組流並將其轉換為HTTP請求(訊息)。路由器接受HTTP請求並確定要呼叫的處理程式。處理程式處理HTTP請求並構建HTTP回應。HTTP回應訊息使用HTTP函式庫轉換回位元組流,然後將位元組流發送回客戶端。
解析HTTP請求訊息
在本文中,我們將構建一個HTTP函式庫。該函式庫將包含資料結構和方法,以執行以下操作:
- 將傳入的位元組流解釋並轉換為HTTP請求訊息
- 建構HTTP回應訊息並將其轉換為位元組流,以便透過網路傳輸
建立HTTP函式庫
首先,在http/src/lib.rs中新增以下程式碼:
pub mod httprequest;
這告訴編譯器我們正在HTTP函式庫中建立一個新的公開可存取的模組httprequest。
接下來,在http/src下建立兩個新檔案:httprequest.rs和httpresponse.rs,分別包含處理HTTP請求和回應的功能。
設計HTTP請求的Rust資料結構
當TCP連線上傳入位元組流時,我們將解析它並將其轉換為強型別的Rust資料結構,以便進一步處理。我們的HTTP伺服器程式可以隨後使用這些Rust資料結構,而不是TCP流。
Method Enum的實作
pub enum Method {
GET,
POST,
Uninitialized,
}
我們使用列舉資料結構,因為我們只想允許我們的實作中預定義的HTTP方法值。我們將只支援兩種HTTP方法:GET和POST請求。我們還增加了第三種型別Uninitialized,用於在執行程式中初始化資料結構期間使用。
內容解密:
上述程式碼定義了一個名為 Method 的列舉,用於表示 HTTP 請求的方法。這個列舉有三個變體:GET、POST 和 Uninitialized。其中,GET 和 POST 分別代表 HTTP 協定中的 GET 和 POST 方法,而 Uninitialized 則用於表示尚未初始化的方法狀態。這種設計使得我們可以明確地區分不同的 HTTP 方法,並且透過使用列舉而不是字串,可以提高程式碼的安全性和可讀性。