Rust 的強大效能和安全性使其成為建構網路服務的理想選擇。本文逐步講解如何使用 Rust 建立 HTTP 伺服器,並探討 HTTP 請求處理、路由機制、單元測試,以及 JSON 資料的序列化和反序列化。程式碼範例涵蓋伺服器核心模組、路由模組、處理器模組以及 HTTP 請求與回應的處理邏輯,並示範如何使用外部 crate serde 和 serde_json 進行 JSON 資料處理。透過本文,讀者可以快速上手 Rust 網路程式設計,並建構穩固高效的 HTTP 伺服器。
HTTP 伺服器實作與測試
在前面的章節中,我們完成了 HTTP 函式庫的編寫。現在,我們需要實作主要的 main() 函式、伺服器模組、路由器和處理器。
編寫單元測試
在開始實作伺服器之前,我們先來看看如何為 HTTP 回應結構編寫單元測試。測試程式碼將被加入到 httpresponse.rs 檔案的末尾。
測試 HTTP 回應結構的建立
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_response_struct_creation_200() {
let response_actual = HttpResponse::new(
"200",
None,
Some("Item was shipped on 21st Dec 2020".into()),
);
let response_expected = HttpResponse {
version: "HTTP/1.1".to_string(),
status_code: "200".to_string(),
status_text: "OK".to_string(),
headers: {
let mut h = HashMap::new();
h.insert("Content-Type", "text/html");
Some(h)
},
body: Some("Item was shipped on 21st Dec 2020".into()),
};
assert_eq!(response_actual, response_expected);
}
#[test]
fn test_response_struct_creation_404() {
let response_actual = HttpResponse::new(
"404",
None,
Some("Item was shipped on 21st Dec 2020".into()),
);
let response_expected = HttpResponse {
version: "HTTP/1.1".to_string(),
status_code: "404".to_string(),
status_text: "Not Found".to_string(),
headers: {
let mut h = HashMap::new();
h.insert("Content-Type", "text/html");
Some(h)
},
body: Some("Item was shipped on 21st Dec 2020".into()),
};
assert_eq!(response_actual, response_expected);
}
#[test]
fn test_http_response_creation() {
let response_expected = HttpResponse {
version: "HTTP/1.1".to_string(),
status_code: "404".to_string(),
status_text: "Not Found".to_string(),
headers: {
let mut h = HashMap::new();
h.insert("Content-Type", "text/html");
Some(h)
},
body: Some("Item was shipped on 21st Dec 2020".into()),
};
let http_string: String = response_expected.into();
let response_actual = "HTTP/1.1 404 Not Found\r\nContent-Type: text/html\r\nContent-Length: 33\r\n\r\nItem was shipped on 21st Dec 2020";
assert_eq!(http_string, response_actual);
}
}
內容解密:
- 測試 HTTP 回應結構的建立:我們首先測試了
HttpResponse結構的建立,分別針對狀態碼 200 和 404 的情況進行了測試。 - 驗證 HTTP 回應訊息的格式:最後一個測試驗證了
HttpResponse結構是否能正確地序列化成符合 HTTP 協定的字串。
編寫 main() 函式和伺服器模組
接下來,我們需要編寫 main() 函式和伺服器模組。main() 函式位於 httpserver/src/main.rs。
mod handler;
mod server;
mod router;
use server::Server;
fn main() {
let server = Server::new("localhost:3000");
server.run();
}
伺服器模組的實作位於 httpserver/src/server.rs。
use super::router::Router;
use http::httprequest::HttpRequest;
use std::io::prelude::*;
use std::net::TcpListener;
pub struct Server<'a> {
socket_addr: &'a str,
}
impl<'a> Server<'a> {
pub fn new(socket_addr: &'a str) -> Self {
Server { socket_addr }
}
pub fn run(&self) {
let connection_listener = TcpListener::bind(self.socket_addr).unwrap();
println!("Running on {}", self.socket_addr);
for stream in connection_listener.incoming() {
let mut stream = stream.unwrap();
println!("Connection established");
let mut read_buffer = [0; 90];
stream.read(&mut read_buffer).unwrap();
let req: HttpRequest = String::from_utf8(read_buffer.to_vec()).unwrap().into();
Router::route(req, &mut stream);
}
}
}
內容解密:
Server結構的建立:Server結構包含一個socket_addr欄位,用於指定伺服器的監聽位址。run方法的實作:run方法負責啟動伺服器並監聽傳入的連線請求。對於每個連線請求,它會讀取 HTTP 請求並將其路由到適當的處理器。
圖表翻譯:
@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultTextAlignment center
skinparam rectangleBackgroundColor #F5F5F5
skinparam rectangleBorderColor #333333
skinparam arrowColor #333333
title 圖表翻譯:
rectangle "HTTP 請求" as node1
rectangle "處理請求" as node2
rectangle "路由請求" as node3
rectangle "產生回應" as node4
rectangle "HTTP 回應" as node5
node1 --> node2
node2 --> node3
node3 --> node4
node4 --> node5
@enduml
圖表翻譯: 此圖示呈現了客戶端與伺服器之間的互動流程。客戶端傳送 HTTP 請求給伺服器,伺服器接收請求後交由路由器進行路由,路由器根據請求內容將其轉發給適當的處理器。處理器處理完請求後產生回應,回應被送回伺服器並最終傳回給客戶端。
HTTP 伺服器中的路由與處理器模組實作
在建立一個完整的 HTTP 伺服器時,路由(Router)與處理器(Handler)模組扮演著至關重要的角色。本篇文章將探討如何在 Rust 中實作這兩個模組,並提供詳細的程式碼解析。
路由模組的設計與實作
路由模組的主要功能是根據接收到的 HTTP 請求,決定應該將請求轉發給哪個處理器進行處理。以下是路由模組的實作程式碼:
// httpserver/src/router.rs
use super::handler::{Handler, PageNotFoundHandler, StaticPageHandler, WebServiceHandler};
use http::{httprequest, httprequest::HttpRequest, httpresponse::HttpResponse};
use std::io::prelude::*;
pub struct Router;
impl Router {
pub fn route(req: HttpRequest, stream: &mut impl Write) -> () {
match req.method {
httprequest::Method::Get => match &req.resource {
httprequest::Resource::Path(s) => {
let route: Vec<&str> = s.split("/").collect();
match route[1] {
"api" => {
let resp: HttpResponse = WebServiceHandler::handle(&req);
let _ = resp.send_response(stream);
}
_ => {
let resp: HttpResponse = StaticPageHandler::handle(&req);
let _ = resp.send_response(stream);
}
}
}
},
_ => {
let resp: HttpResponse = PageNotFoundHandler::handle(&req);
let _ = resp.send_response(stream);
}
}
}
}
內容解密:
- 路由模組首先檢查接收到的 HTTP 請求的方法是否為 GET。
- 如果是 GET 請求,則進一步檢查請求的路徑。
- 如果路徑以
/api開頭,則將請求轉發給WebServiceHandler處理。 - 否則,將請求轉發給
StaticPageHandler處理靜態頁面。 - 如果不是 GET 請求,則傳回 404 錯誤頁面。
處理器模組的設計與實作
處理器模組負責處理由路由模組轉發過來的請求,並傳回相應的 HTTP 回應。以下是處理器模組的實作程式碼:
// httpserver/src/handler.rs
use http::{httprequest::HttpRequest, httpresponse::HttpResponse};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::env;
use std::fs;
pub trait Handler {
fn handle(req: &HttpRequest) -> HttpResponse;
fn load_file(file_name: &str) -> Option<String> {
// 載入檔案的實作細節
}
}
// 實作 Handler trait 的不同處理器
pub struct StaticPageHandler;
pub struct PageNotFoundHandler;
pub struct WebServiceHandler;
impl Handler for StaticPageHandler {
fn handle(req: &HttpRequest) -> HttpResponse {
// 處理靜態頁面的請求
}
}
impl Handler for PageNotFoundHandler {
fn handle(req: &HttpRequest) -> HttpResponse {
// 傳回 404 錯誤頁面
}
}
impl Handler for WebServiceHandler {
fn handle(req: &HttpRequest) -> HttpResponse {
// 處理 Web Service 請求
}
}
內容解密:
- 定義了一個
Handlertrait,包含handle和load_file兩個方法。 - 實作了三個不同的處理器:
StaticPageHandler、PageNotFoundHandler和WebServiceHandler。 - 每個處理器都根據自己的邏輯實作了
handle方法。
JSON 序列化和反序列化
為了處理 JSON 資料,我們使用了 serde 和 serde_json 這兩個外部 crate。以下是相關的程式碼:
# Cargo.toml
[dependencies]
http = { path = "../http" }
serde = { version = "1.0.117", features = ["derive"] }
serde_json = "1.0.59"
// httpserver/src/handler.rs
#[derive(Serialize, Deserialize)]
pub struct OrderStatus {
order_id: i32,
order_date: String,
order_status: String,
}
impl WebServiceHandler {
fn load_json() -> Vec<OrderStatus> {
// 載入 JSON 檔案並反序列化為 OrderStatus 向量
}
}
內容解密:
- 使用
serde的 derive 宏來自動實作OrderStatus結構的序列化和反序列化。 - 在
WebServiceHandler中實作了load_json方法,用於載入 JSON 檔案並反序列化為OrderStatus向量。
網路伺服器與HTTP函式庫的實作
簡介
本章節主要介紹如何使用Rust語言建立一個簡單的HTTP伺服器和相關的HTTP函式庫。我們將實作一個能夠提供靜態網頁和JSON資料的網路伺服器。
HTTP請求處理
首先,我們需要定義一個處理HTTP請求的函式。以下是一個範例實作:
fn handle(req: &HttpRequest) -> HttpResponse {
let http::httprequest::Resource::Path(s) = &req.resource;
// 解析URI
let route: Vec<&str> = s.split("/").collect();
// 如果路由是/api/shipping/orders,回傳JSON資料
match route[2] {
"shipping" if route.len() > 2 && route[3] == "orders" => {
let body = Some(serde_json::to_string(&Self::load_json()).unwrap());
let mut headers: HashMap<&str, &str> = HashMap::new();
headers.insert("Content-Type", "application/json");
HttpResponse::new("200", Some(headers), body)
}
_ => HttpResponse::new("404", None, Self::load_file("404.html")),
}
}
內容解密:
- 此函式用於處理HTTP請求,首先解析請求的URI。
- 根據URI的不同,決定回傳JSON資料或是404錯誤頁面。
- 當URI為
/api/shipping/orders時,載入orders.json檔案並序列化為JSON格式回傳。 - 其他URI則回傳404錯誤頁面。
測試網路伺服器
為了測試我們的網路伺服器,我們需要建立一些測試檔案。首先,在httpserver根目錄下建立兩個子資料夾:data和public。在public資料夾中,建立四個檔案:index.html、health.html、404.html和styles.css。在data資料夾中,建立一個orders.json檔案。
以下是這些檔案的範例內容:
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="stylesheet" href="styles.css">
<title>Index!</title>
</head>
<body>
<h1>Hello, welcome to home page</h1>
<p>This is the index page for the web site</p>
</body>
</html>
styles.css
h1 {
color: red;
margin-left: 25px;
}
health.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Health!</title>
</head>
<body>
<h1>Hello welcome to health page!</h1>
<p>This site is perfectly fine</p>
</body>
</html>
404.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Not Found!</title>
</head>
<body>
<h1>404 Error</h1>
<p>Sorry the requested page does not exist</p>
</body>
</html>
orders.json
[
{
"order_id": 1,
"order_date": "21 Jan 2020",
"order_status": "Delivered"
},
{
"order_id": 2,
"order_date": "2 Feb 2020",
"order_status": "Pending"
}
]
執行網路伺服器
執行以下指令啟動網路伺服器:
cargo run -p httpserver
然後,使用瀏覽器或curl工具測試以下網址:
localhost:3000/localhost:3000/healthlocalhost:3000/api/shipping/orderslocalhost:3000/invalid-path