Rust Web(二)—— 自建HTTP Server

  • Rust没有自带HTTP支持,因此很多的方法及接口都需要开发者自行设计实现,不过对于Web Server,不同业务对其规模及性能的要求不尽相同,这样一想也情有可原;
  • 对于Rust基础以及HTTP原理,需要读者有所认识;
  • 本文的设计思路也可以自行设计扩展进而发展成更完整的方案;

目录

Rust Web(二)—— 自建HTTP Server

一、项目创建

二、解析HTTP请求

测试示例

全部实现

测试

三、构建HTTP响应

测试

四、构建Server模块

模块准备

功能实现

五、构建 Router & Handler 模块

实现代码

六、完整测试

运行


Rust Web(二)—— 自建HTTP Server

一、项目创建


  • 在自定的目录下,创建两个子项目目录

    • httpserver

    • http

      • http 为·lib 库,故命令中添加 --lib

  • 在根项目的 Cargo.toml文件中添加这两个子项目


  • 进入 http 子项目,在 src/lib.rs 内写入公共模块 pub mod httprequest;

  • 在同级 src 目录下新建:

    • httprequest.rs

    • httpresponse.rs

二、解析HTTP请求


  • httprequest.rs 中,先尝试实现 枚举 Method,并进行一次测试

#[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,
        }
    }
}
​
#[cfg(test)]
mod tests {
    use super::*;
​
    #[test]
    fn test_method_into() {
        let m: Method = "GET".into();
        assert_eq!(m, Method::Get);
    }
}

测试示例

全部实现

  • 依照HTTP协议原理以及Rust本身的特性,先实现 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 {
    V11,
    V20,
    Uninitialized,
}
​
impl From<&str> for Version {
    fn from(s: &str) -> Version {
        match s {
            "HTTP/1.1" => Version::V11,
            "HTTP/2.0" => Version::V20,
            _ => Version::Uninitialized,
        }
    }
}
​
#[derive(Debug, PartialEq)]
pub enum Resource {
    Path(String),
}
​
#[derive(Debug)]
pub struct HttpRequest {
    pub method: Method,
    pub version: Version,
    pub resource: Resource,
    pub headers: HashMap<String, String>,
    pub msg_body: String,
}
​
impl From<String> for HttpRequest {
    fn from(req: String) -> Self {
        let mut parsed_method = Method::Uninitialized;
        let mut parsed_version = Version::V11;
        let mut parsed_resource = Resource::Path("".to_string());
        let mut parsed_headers = HashMap::new();
        let mut parsed_msg_body = "";
​
        for line in req.lines() {
            if line.contains("HTTP") {
                let (method, resource, version) = process_req_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 {
                // No operation
            } else {
                parsed_msg_body = line;
            }
        }
         HttpRequest {
                method: parsed_method,
                resource: parsed_resource,
                version: parsed_version,
                headers: parsed_headers,
                msg_body: parsed_msg_body.to_string(),
            }
    }
}
​
fn process_req_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()
    )
}
​
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)
}
​
#[cfg(test)]
mod tests {
    use super::*;
​
    #[test]
    fn test_method_into() {
        let m: Method = "GET".into();
        assert_eq!(m, Method::Get);
    }
​
    #[test]
    fn test_version_into() {
        let v: Version = "HTTP/1.1".into();
        assert_eq!(v, Version::V11);
    }
​
    #[test]
    fn test_read_http() {
        let s: String = String::from("GET /index HTTP/1.1\r\n\
        Host: localhost\r\n\
        User-Agent: Curl/7.64.1\r\n\
        Accept: */*\r\n\r\n");
        let mut headers_expected = HashMap::new();
        headers_expected.insert("Host".into(), " localhost".into());
        headers_expected.insert("User-Agent".into(), " Curl/7.64.1".into());
        headers_expected.insert("Accept".into(), " */*".into());
        let req: HttpRequest = s.into();
​
        assert_eq!(Method::Get, req.method);
        assert_eq!(Resource::Path("/index".to_string()), req.resource);
        assert_eq!(Version::V11, req.version);
        assert_eq!(headers_expected, req.headers);
    }
}

测试

  • 测试结果

  • 编写过程中以下问题值得注意

    • 测试请求中的大小写要严格区分;

    • 由于请求头部仅以冒号分割,因此值 value 内的空格不能忽略,或者进行进一步优化;

三、构建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(res: HttpResponse) -> String {
        let res1 = res.clone();
        format!(
            "{} {} {}\r\n{}Content-Length: {}\r\n\r\n{}",
            &res1.version(),
            &res1.status_code(),
            &res1.status_text(),
            &res1.headers(),
            &res.body.unwrap().len(),
            &res1.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(); // mut
        if status_code != "200" {
            response.status_code = status_code.into();
        };
        response.headers = match &headers {
            Some(_h) => headers,
            None => {
                let mut h = HashMap::new();
                h.insert("Content-Type", "text/html");
                Some(h)
            }
        };
        response.status_text = match response.status_code {
            "200" => "OK".into(),
            "400" => "Bad Request".into(),
            "404" => "Not Found".into(),
            "500" => "Internal Server Error".into(),
            _ => "Not Found".into(), //
        };
​
        response.body = body;
        response
    }
​
    pub fn send_response(&self, write_stream: &mut impl Write) -> Result<()> {
        let res = self.clone();
        let response_string: String = String::from(res); // from trait
        let _ = write!(write_stream, "{}", response_string);
​
        Ok(())
    }
​
    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: HashMap<&str, &str> = self.headers.clone().unwrap();
        let mut header_string: String = "".into();
        for (k, v) in map.iter() {
            header_string = format!("{}{}:{}\r\n", header_string, k, v);
        }
        header_string
    }
​
    pub fn body(&self) -> &str {
        match &self.body {
            Some(b) => b.as_str(),
            None => "",
        }
    }
}
​
#[cfg(test)]
mod tests {
    use super::*;
​
    #[test]
    fn test_response_struct_creation_200() {
        let response_actual = HttpResponse::new(
            "200",
            None,
            Some("nothing for now".into()),
        );
        let response_expected = 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("nothing for now".into()),
        };
​
        assert_eq!(response_actual, response_expected);
    }
​
    #[test]
    fn test_response_struct_creation_404() {
        let response_actual = HttpResponse::new(
            "404",
            None,
            Some("nothing for now".into()),
        );
        let response_expected = 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("nothing for now".into()),
        };
​
        assert_eq!(response_actual, response_expected);
    }
​
    #[test]
    fn test_http_response_creation() {
        let response_expected = 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("nothing for now".into()),
        };
        let http_string: String = response_expected.into();
        let actual_string: String =
            "HTTP/1.1 404 Not Found\r\n\
            Content-Type:text/html\r\n\
            Content-Length: 15\r\n\r\n\
            nothing for now".into(); // 此处注意Content-Length值
​
        assert_eq!(http_string, actual_string);
    }
}

测试

  • 测试结果

  • 其中需要留意的点位

    • 在实现 Stringtrait 时,不能从 &res1.body 获取长度,以避免内部 body 成员的所有权转移;

    • 测试整个相应,自定义响应实例中的请求体数据长度要保持一致;

四、构建Server模块


模块准备

  • 此时转至 httpserver 子项目内,将前文所涉及的 http 子项目导入 Cargo.toml 文件;

  • 并在 httpserver/src 下再创建三文件

    • server.rs

    • router.rs

    • handler.rs

功能实现

  • 大概的调用逻辑

    • main - 调用 -> server - 调用 -> router - 调用 -> handler

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 req: HttpRequest = String::from_utf8( read_buffer.to_vec()).unwrap().into();
            Router::route(req, &mut stream);
        }
    }
}
  • 实现至当前阶段还不能直接运行;

五、构建 Router & Handler 模块


  • 这两个模块联合起来处理接收到的请求,其中

    • 判定请求的合法性,适当返回错误反馈;

    • 解析后台的数据部分,进行相应的序列化和反序列化;

  • 不同的请求状况交由不同类型的句柄 Handler 来处理,同名可重写的方法通过 Trait 来定义;

  • 其中的 handler.rs 需要引入两个crate

    • serde (本文使用的是1.0.140版本)

    • serde_json (本文使用的是1.0.82版本)

实现代码

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);
            }
        }
    }
}

handler.rs

use http::{httprequest::HttpRequest, httpresponse::HttpResponse};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::env;
use std::fs;
use std::ops::Index;
​
pub trait Handler {
    fn handle(req: &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 StaticPageHandler;
pub struct PageNotFoundHandler;
pub struct WebServiceHandler;
​
#[derive(Serialize, Deserialize)]
pub struct OrderStatus {
    order_id: i32,
    order_date: String,
    order_status: String,
}
​
impl Handler for PageNotFoundHandler {
    fn handle(_req: &HttpRequest) -> HttpResponse {
        HttpResponse::new("404", None, Self::load_file("404.html"))
    }
}
​
impl Handler for StaticPageHandler {
    fn handle(req: &HttpRequest) -> HttpResponse {
        let http::httprequest::Resource::Path(s) = &req.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 WebServiceHandler {
    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 WebServiceHandler {
    fn handle(req: &HttpRequest) -> HttpResponse {
        let http::httprequest::Resource::Path(s) = &req.resource;
        let route: Vec<&str> = s.split("/").collect();
        // localhost:2333/api/air/orders
        match route[2] {
            "air" 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"))
        }
    }
}

六、完整测试


  • httpserver 项目中分别添加

    • data/orders.json

    • public/index.html

    • public/404.html

    • public/health.html

    • styles.css

  • 测试文件内容

<!-- 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>

<!-- 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>

/* styles.css */
​
h1 {
     color: red;
     margin-left: 25px;
}

// orders.json
​
[
  {
    "order_id": 1,
    "order_date": "20 June 2022",
    "order_status": "Delivered"
  },
  {
    "order_id": 2,
    "order_date": "27 October 2022",
    "order_status": "Pending"
  }
]

运行

  • 效果如下

访问 index.html

访问 health.html

访问 orders.json

访问一个错误地址

  • 至此,HTTP的基本功能实现就到此为止;

  • 可以基于此框架做性能优化以及扩展自己所需要的功能;

  • 通过本次HTTP《简易》设计,可以更深刻地体会一些后端设计思想、Rust本身的特点以及基于HTTP协议的Server设计思路;

每一个不曾起舞的日子,都是对生命的辜负。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_廿_尘

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值