返回文章列表

Rust HTTP 伺服器實作與單元測試

本文示範如何使用 Rust 建立一個簡單的 HTTP 伺服器,包含處理 HTTP 請求、路由設定、單元測試撰寫,以及 JSON 資料處理等核心技術。文章涵蓋伺服器模組、路由模組和處理器模組的實作細節,並提供程式碼範例和測試方法,適合想了解 Rust 網路程式設計的開發者參考。

Web 開發 後端開發

Rust 的強大效能和安全性使其成為建構網路服務的理想選擇。本文逐步講解如何使用 Rust 建立 HTTP 伺服器,並探討 HTTP 請求處理、路由機制、單元測試,以及 JSON 資料的序列化和反序列化。程式碼範例涵蓋伺服器核心模組、路由模組、處理器模組以及 HTTP 請求與回應的處理邏輯,並示範如何使用外部 crate serdeserde_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);
    }
}

內容解密:

  1. 測試 HTTP 回應結構的建立:我們首先測試了 HttpResponse 結構的建立,分別針對狀態碼 200 和 404 的情況進行了測試。
  2. 驗證 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);
        }
    }
}

內容解密:

  1. Server 結構的建立Server 結構包含一個 socket_addr 欄位,用於指定伺服器的監聽位址。
  2. 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);
            }
        }
    }
}

內容解密:

  1. 路由模組首先檢查接收到的 HTTP 請求的方法是否為 GET。
  2. 如果是 GET 請求,則進一步檢查請求的路徑。
  3. 如果路徑以 /api 開頭,則將請求轉發給 WebServiceHandler 處理。
  4. 否則,將請求轉發給 StaticPageHandler 處理靜態頁面。
  5. 如果不是 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 請求
    }
}

內容解密:

  1. 定義了一個 Handler trait,包含 handleload_file 兩個方法。
  2. 實作了三個不同的處理器:StaticPageHandlerPageNotFoundHandlerWebServiceHandler
  3. 每個處理器都根據自己的邏輯實作了 handle 方法。

JSON 序列化和反序列化

為了處理 JSON 資料,我們使用了 serdeserde_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 向量
    }
}

內容解密:

  1. 使用 serde 的 derive 宏來自動實作 OrderStatus 結構的序列化和反序列化。
  2. 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根目錄下建立兩個子資料夾:datapublic。在public資料夾中,建立四個檔案:index.htmlhealth.html404.htmlstyles.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/health
  • localhost:3000/api/shipping/orders
  • localhost:3000/invalid-path