Rust Web 全栈开发(二):构建 HTTP Server

发布于:2025-07-08 ⋅ 阅读:(20) ⋅ 点赞:(0)

Rust Web 全栈开发(二):构建 HTTP Server

参考视频:https://www.bilibili.com/video/BV1RP4y1G7KF

Web Server 的消息流动图:

在这里插入图片描述

Server:监听 TCP 字节流

Router:接收 HTTP 请求,并决定调用哪个 Handler

Handler:处理 HTTP 请求,构建 HTTP 响应

HTTP Library:

  • 解释字节流,把它转换为 HTTP 请求
  • 把 HTTP 响应转换回字节流

构建步骤:

  1. 解析 HTTP 请求消息
  2. 构建 HTTP 响应消息
  3. 路由与 Handler
  4. 测试 Web Server

创建成员包/库:httpserver、http

在原项目下新建成员包 httpserver、成员库 http:

cargo new httpserver
cargo new --lib http

在这里插入图片描述

在工作区内运行 cargo new 会自动将新创建的包添加到工作区内 Cargo.toml 的 [workspace] 定义中的 members 键中,如下所示:

在这里插入图片描述

在 http 成员库的 src 目录下新建两个文件:httprequest.rs、httpresponse.rs。

此时,我们可以通过运行 cargo build 来构建工作区。项目目录下的文件应该是这样的:

├── Cargo.lock
├── Cargo.toml
├── httpserver
│   ├── Cargo.toml
│   └── src
│       └── main.rs
├── http
│   ├── Cargo.toml
│   └── src
│       └── lib.rs
│       └── httprequest.rs
│       └── httpresponse.rs
├── tcpclient
│   ├── Cargo.toml
│   └── src
│       └── main.rs
├── tcpserver
│   ├── Cargo.toml
│   └── src
│       └── main.rs
└── target

解析 HTTP 请求

HTTP 请求的构成

HTTP 请求报文由 3 部分组成:请求行、请求头、请求体。

在这里插入图片描述

构建 HttpRequest

3 个数据结构:

名称 类型 描述
HttpRequest struct 表示 HTTP 请求
Method enum 指定所允许的 HTTP 方法
Version enum 指定所允许的 HTTP 版本

以上 3 个数据结构都需要实现的 3 个 trait:

名称 描述
From<&str> 用于把传进来的字符串切片转换为 HttpRequest
Debug 打印调试信息
PartialEq 用于解析和自动化测试脚本里做比较

打开 http 成员库中的 httprequest.rs,编写代码:

use std::collections::HashMap;

#[derive(Debug, PartialEq)]
pub enum Method {
    Get,
    Post,
    Uninitialized,
}
impl From<&str> for Method {
    fn from(s: &str) -> Method {
        match s {
            "GET" => Method::Get,
            "POST" => Method::Post,
            _ => Method::Uninitialized,
        }
    }
}

#[derive(Debug, PartialEq)]
pub enum Version {
    V1_1,
    V2_0,
    Uninitialized,
}

impl From<&str> for Version {
    fn from(s: &str) -> Version {
        match s {
            "HTTP/1.1" => Version::V1_1,
            _ => Version::Uninitialized,
        }
    }
}

#[derive(Debug, PartialEq)]
pub enum Resource {
    Path(String),
}

#[derive(Debug)]
pub struct HttpRequest {
    pub method: Method,
    pub resource: Resource,
    pub version: Version,
    pub headers: HashMap<String, String>,
    pub body: String,
}

impl From<String> for HttpRequest {
    fn from(request: String) -> HttpRequest {
        let mut parsed_method = Method::Uninitialized;
        let mut parsed_resource = Resource::Path("".to_string());
        let mut parsed_version =  Version::V1_1;
        let mut parsed_headers = HashMap::new();
        let mut parsed_body = "";

        for line in request.lines() {
            if line.contains("HTTP") {
                let (method, resource, version) = process_request_line(line);
                parsed_method = method;
                parsed_resource = resource;
                parsed_version = version;
            } else if line.contains(":") {
                let (key, value) = process_header_line(line);
                parsed_headers.insert(key, value);
            } else if line.len() == 0 {

            } else {
                parsed_body = line;
            }
        }

        HttpRequest {
            method: parsed_method,
            resource: parsed_resource,
            version: parsed_version,
            headers: parsed_headers,
            body: parsed_body.to_string(),
        }
    }
}

fn process_header_line(s: &str) -> (String, String) {
    let mut header_items = s.split(":");
    let mut key = String::from("");
    let mut value = String::from("");

    if  let Some(k) = header_items.next() {
        key = k.to_string();
    }
    if let Some(v) = header_items.next() {
        value = v.to_string();
    }

    (key, value)
}

fn process_request_line(s: &str) -> (Method, Resource, Version) {
    let mut words = s.split_whitespace();
    let method = words.next().unwrap();
    let resource = words.next().unwrap();
    let version = words.next().unwrap();

    (
        method.into(),
        Resource::Path(resource.to_string()),
        version.into()
    )
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn test_method_into() {
        let method: Method = "GET".into();
        assert_eq!(method, Method::Get);
    }

    #[test]
    fn test_version_into() {
        let version: Version = "HTTP/1.1".into();
        assert_eq!(version, Version::V1_1);
    }

    #[test]
    fn test_read_http() {
        let s = String::from("GET /greeting HTTP/1.1\r\nHost: localhost:3000\r\nUser-Agent: curl/7.71.1\r\nAccept: */*\r\n\r\n");

        let mut headers_excepted = HashMap::new();
        headers_excepted.insert("Host".into(), " localhost".into());
        headers_excepted.insert("Accept".into(), " */*".into());
        headers_excepted.insert("User-Agent".into(), " curl/7.71.1".into());

        let request: HttpRequest = s.into();

        assert_eq!(request.method, Method::Get);
        assert_eq!(request.resource, Resource::Path("/greeting".to_string()));
        assert_eq!(request.version, Version::V1_1);
        assert_eq!(request.headers, headers_excepted);
    }
}

运行命令 cargo test -p http,测试 http 成员库。

3 个测试都通过了:

在这里插入图片描述

构建 HTTP 响应

HTTP 响应的构成

HTTP 响应报文由 3 部分组成:响应行、响应头、响应体。

在这里插入图片描述

构建 HttpResponse

HttpResponse 需要实现的方法或 trait:

名称 描述
Default trait 指定成员的默认值
From trait 将 HttpResponse 转化为 String
new() 使用默认值创建一个新的 HttpResponse 结构体
getter 方法 获取 HttpResponse 成员变量的值
send_response() 构建响应,将原始字节通过 TCP 传送

打开 http 成员库中的 httpresponse.rs,编写代码:

use std::collections::HashMap;
use std::io::{Result, Write};

#[derive(Debug, PartialEq, Clone)]
pub struct HttpResponse<'a> {
    version: &'a str,
    status_code: &'a str,
    status_text: &'a str,
    headers: Option<HashMap<&'a str, &'a str>>,
    body: Option<String>,
}

impl<'a> Default for HttpResponse<'a> {
    fn default() -> Self {
        Self {
            version: "HTTP/1.1".into(),
            status_code: "200".into(),
            status_text: "OK".into(),
            headers: None,
            body: None,
        }
    }
}

impl<'a> From<HttpResponse<'a>> for String {
    fn from(response: HttpResponse) -> String {
        let res = response.clone();
        format!(
            "{} {} {}\r\n{}Content-Length: {}\r\n\r\n{}",
            &res.version(),
            &res.status_code(),
            &res.status_text(),
            &res.headers(),
            &response.body.unwrap().len(),
            &res.body(),
        )
    }
}

impl<'a> HttpResponse<'a> {
    pub fn new(
        status_code: &'a str,
        headers: Option<HashMap<&'a str, &'a str>>,
        body: Option<String>,
    ) -> HttpResponse<'a> {
        let mut response: HttpResponse<'a> = HttpResponse::default();
        if status_code != "200" {
            response.status_code = status_code.into();
        }
        response.status_text = match response.status_code {
            // 消息
            "100" => "Continue".into(),
            "101" => "Switching Protocols".into(),
            "102" => "Processing".into(),
            // 成功
            "200" => "OK".into(),
            "201" => "Created".into(),
            "202" => "Accepted".into(),
            "203" => "Non-Authoritative Information".into(),
            "204" => "No Content".into(),
            "205" => "Reset Content".into(),
            "206" => "Partial Content".into(),
            "207" => "Multi-Status".into(),
            // 重定向
            "300" => "Multiple Choices".into(),
            "301" => "Moved Permanently".into(),
            "302" => "Move Temporarily".into(),
            "303" => "See Other".into(),
            "304" => "Not Modified".into(),
            "305" => "Use Proxy".into(),
            "306" => "Switch Proxy".into(),
            "307" => "Temporary Redirect".into(),
            // 请求错误
            "400" => "Bad Request".into(),
            "401" => "Unauthorized".into(),
            "402" => "Payment Required".into(),
            "403" => "Forbidden".into(),
            "404" => "Not Found".into(),
            "405" => "Method Not Allowed".into(),
            "406" => "Not Acceptable".into(),
            "407" => "Proxy Authentication Required".into(),
            "408" => "Request Timeout".into(),
            "409" => "Conflict".into(),
            "410" => "Gone".into(),
            "411" => "Length Required".into(),
            "412" => "Precondition Failed".into(),
            "413" => "Request Entity Too Large".into(),
            "414" => "Request-URI Too Long".into(),
            "415" => "Unsupported Media Type".into(),
            "416" => "Requested Range Not Satisfiable".into(),
            "417" => "Expectation Failed".into(),
            "421" => "Misdirected Request".into(),
            "422" => "Unprocessable Entity".into(),
            "423" => "Locked".into(),
            "424" => "Failed Dependency".into(),
            "425" => "Too Early".into(),
            "426" => "Upgrade Required".into(),
            "449" => "Retry With".into(),
            "451" => "Unavailable For Legal Reasons".into(),
            // 服务器错误
            "500" => "Internal Server Error".into(),
            "501" => "Not Implemented".into(),
            "502" => "Bad Gateway".into(),
            "503" => "Service Unavailable".into(),
            "504" => "Gateway Timeout".into(),
            "505" => "HTTP Version Not Supported".into(),
            "506" => "Variant Also Negotiates".into(),
            "507" => "Insufficient Storage".into(),
            "509" => "Bandwidth Limit Exceeded".into(),
            "510" => "Not Extended".into(),
            "600" => "Unparseable Response Headers".into(),
            _ => "Not Found".into(),
        };
        response.headers = match &headers {
            Some(_h) => headers,
            None => {
                let mut header = HashMap::new();
                header.insert("Content-Type", "text/html");
                Some(header)
            }
        };
        response.body = body;

        response
    }

    fn version(&self) -> &str {
        self.version
    }

    fn status_code(&self) -> &str {
        self.status_code
    }

    fn  status_text(&self) -> &str {
        self.status_text
    }

    fn headers(&self) -> String {
        let map = self.headers.clone().unwrap();
        let mut headers_string = "".into();
        for (key, value) in map.iter() {
            headers_string = format!("{headers_string}{}:{}\r\n", key, value);
        }

        headers_string
    }

    fn body(&self) -> &str {
        match &self.body {
            Some(b) => b.as_str(),
            None => "",
        }
    }

    pub fn send_response(&self, write_stream: &mut impl Write) -> Result<()> {
        let response = self.clone();
        let response_string: String = String::from(response);
        let _ = write!(write_stream, "{}", response_string);

        Ok(())
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn test_response_struct_creation_200() {
        let response_actual = HttpResponse::new(
            "200",
            None,
            Some("xxxx".into()),
        );
        let response_excepted = HttpResponse {
            version: "HTTP/1.1",
            status_code: "200",
            status_text: "OK",
            headers: {
                let mut h = HashMap::new();
                h.insert("Content-Type", "text/html");
                Some(h)
            },
            body: Some("xxxx".into()),
        };

        assert_eq!(response_actual, response_excepted);
    }

    #[test]
    fn test_response_struct_creation_404() {
        let response_actual = HttpResponse::new(
            "404",
            None,
            Some("xxxx".into()),
        );
        let response_excepted = HttpResponse {
            version: "HTTP/1.1",
            status_code: "404",
            status_text: "Not Found",
            headers: {
                let mut h = HashMap::new();
                h.insert("Content-Type", "text/html");
                Some(h)
            },
            body: Some("xxxx".into()),
        };

        assert_eq!(response_actual, response_excepted);
    }

    #[test]
    fn test_http_response_creation() {
        let response_excepted = HttpResponse {
            version: "HTTP/1.1",
            status_code: "404",
            status_text: "Not Found",
            headers: {
                let mut h = HashMap::new();
                h.insert("Content-Type", "text/html");
                Some(h)
            },
            body: Some("xxxx".into()),
        };

        let http_string: String = response_excepted.into();
        let actual_string = 
            "HTTP/1.1 404 Not Found\r\nContent-Type:text/html\r\nContent-Length: 4\r\n\r\nxxxx";

        assert_eq!(http_string, actual_string);
    }
}

运行命令 cargo test -p http,测试 http 成员库。

现在一共有 6 个测试,都通过了:

在这里插入图片描述

构建 server 模块

httpserver 需要引用 http 模块,打开 httpserver/Cargo.toml,在 [dependencies] 部分添加:

http = {path = "../http"}

在 httpserver/src 目录下新建 3 个文件:router.rs、server.rs、handler.rs。

在这里插入图片描述

它们是这样的关系:

  • main.rs 中的 main 函数调用 server
  • server 调用 router
  • router 调用 handler

打开 httpserver 成员库中的 server.rs,编写代码:

use super::router::Router;
use http::httprequest::HttpRequest;
use std::io::prelude::*;
use std::net::TcpListener;
use std::str;

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; 200];
            stream.read(&mut read_buffer).unwrap();
            let request: HttpRequest = String::from_utf8(read_buffer.to_vec()).unwrap().into();
            Router::route(request, &mut stream);
        }
    }
}

打开 httpserver 成员库中的 main.rs,编写代码:

mod server;
mod router;
mod handler;

use server::Server;

fn main() {
    let server = Server::new("localhost:3000");
    server.run();
}

目前,代码还不能运行,需要继续构建 router 和 handler 模块。

构建 router 模块

打开 httpserver 成员库中的 router.rs,编写代码:

use std::io::prelude::*;
use super::handler::{Handler, WebServerHandler, StaticPageHandler, PageNotFoundHandler};
use http::httprequest;
use http::httprequest::HttpRequest;
use http::httpresponse::HttpResponse;
pub struct Router;

impl Router {
    pub fn route(request: HttpRequest, stream: &mut impl Write) -> () {
        match request.method {
            httprequest::Method::Get => match &request.resource {
                httprequest::Resource::Path(s) => {
                    let route: Vec<&str> = s.split("/").collect();
                    match route[1] {
                        "api" => {
                            let response: HttpResponse = WebServerHandler::handle(&request);
                            let _ = response.send_response(stream);
                        }
                        _ => {
                            let response: HttpResponse = StaticPageHandler::handle(&request);
                            let _ = response.send_response(stream);
                        }
                    }
                }
            },
            _ => {
                let response: HttpResponse = PageNotFoundHandler::handle(&request);
                let _ = response.send_response(stream);
            }
        }
    }
}

构建 handler 模块

首先需要添加两个 crate:serde、serde_json,它们提供了 JSON 的处理和序列化/反序列化的能力。

打开 httpserver/Cargo.toml,在 [dependencies] 部分添加:

serde = {version = "1.0.131", features = ["derive"]}
serde_json = "1.0.72"

打开 httpserver 成员库中的 handler.rs,编写代码:

use std::collections::HashMap;
use std::env;
use std::fs;
use serde::{Deserialize, Serialize};
use http::httprequest;
use http::httprequest::HttpRequest;
use http::httpresponse::HttpResponse;

pub trait Handler {
    fn handle(request: &HttpRequest) -> HttpResponse;

    fn load_file(file_name: &str) -> Option<String> {
        let default_path = format!("{}/public", env!("CARGO_MANIFEST_DIR"));
        let public_path = env::var("PUBLIC_PATH").unwrap_or(default_path);
        let full_path = format!("{}/{}", public_path, file_name);

        let contents = fs::read_to_string(full_path);
        contents.ok()
    }
}

pub struct WebServerHandler;
pub struct StaticPageHandler;
pub struct PageNotFoundHandler;

#[derive(Serialize, Deserialize)]
pub struct OrderStatus {
    order_id: i32,
    order_date: String,
    order_status: String,
}

impl WebServerHandler {
    fn load_json() -> Vec<OrderStatus> {
        let default_path = format!("{}/data", env!("CARGO_MANIFEST_DIR"));
        let data_path = env::var("DATA_PATH").unwrap_or(default_path);
        let full_path = format!("{}/{}", data_path, "orders.json");

        let json_contents = fs::read_to_string(full_path);
        let orders: Vec<OrderStatus> =
            serde_json::from_str(json_contents.unwrap().as_str()).unwrap();

        orders
    }
}

impl Handler for WebServerHandler {
    fn handle(request: &HttpRequest) -> HttpResponse {
        let httprequest::Resource::Path(s) = &request.resource;
        let route: Vec<&str> = s.split("/").collect();

        match route[2] {
            // localhost:3000/api/shipping/orders
            "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"))
        }
    }
}

impl Handler for StaticPageHandler {
    fn handle(request: &HttpRequest) -> HttpResponse {
        let httprequest::Resource::Path(s) = &request.resource;
        let route: Vec<&str> = s.split("/").collect();

        match route[1] {
            "" => HttpResponse::new("200", None, Self::load_file("index.html")),
            "health" => HttpResponse::new("200", None, Self::load_file("health.html")),
            path => match Self::load_file(path) {
                Some(contents) => {
                    let mut map: HashMap<&str, &str> = HashMap::new();
                    if path.ends_with(".css") {
                        map.insert("Content-Type", "text/css");
                    } else if path.ends_with(".js") {
                        map.insert("Content-Type", "text/javascript");
                    } else {
                        map.insert("Content-Type", "text/html");
                    }

                    HttpResponse::new("200", Some(map), Some(contents))
                }
                None => HttpResponse::new("404", None, Self::load_file("404.html")),
            }
        }
    }
}
impl Handler for PageNotFoundHandler {
    fn handle(request: &HttpRequest) -> HttpResponse {
        HttpResponse::new("404", None, Self::load_file("404.html"))
    }
}

最后贴上各个 HTML 文件和 json 文件。

在 httpserver 目录下新建两个文件夹:data 和 public。

httpserver/data/orders.json:

[
  {
    "order_id": 1,
    "order_date": "21 Jan 2020",
    "order_status": "Delivered"
  },
  {
    "order_id": 2,
    "order_date": "2 Feb 2020",
    "order_status": "Pending"
  }
]

httpserver/public/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>

httpserver/public/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>

httpserver/public/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>

httpserver/public/styles.css:

h1 {
    color: red;
    margin-left: 25px;
}

现在,整个 httpserver 都建立好了。

在终端运行命令 cargo run -p httpserver,终端首先输出:

Running on localhost:3000

尝试在浏览器输入 URL,每次连接建立后,终端还会输出 Connection established!

各个页面的显示效果如下所示:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到