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

目录

Rust Web(二)—— 自建HTTP Server

一、项目创建

二、解析HTTP请求

测试示例

全部实现

测试

三、构建HTTP响应

测试

四、构建Server模块

模块准备

功能实现

五、构建 Router & Handler 模块

实现代码

六、完整测试

运行


Rust Web(二)—— 自建HTTP Server

一、项目创建


  • 在自定的目录下,创建两个子项目目录
  • httpserver
  • http
  • http 为·lib 库,故命令中添加 --lib

rustdesktop自建服务器 windows rust如何创建服务器_后端

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

rustdesktop自建服务器 windows rust如何创建服务器_http_02


  • 进入 http 子项目,在 src/lib.rs 内写入公共模块 pub mod httprequest;
  • 在同级 src 目录下新建:
  • httprequest.rs
  • httpresponse.rs

rustdesktop自建服务器 windows rust如何创建服务器_tcp/ip_03

rustdesktop自建服务器 windows rust如何创建服务器_HTTP_04

二、解析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);
    }
}

测试示例

rustdesktop自建服务器 windows rust如何创建服务器_rust_05

全部实现

  • 依照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);
    }
}

测试

  • 测试结果

rustdesktop自建服务器 windows rust如何创建服务器_HTTP_06

  • 编写过程中以下问题值得注意
  • 测试请求中的大小写要严格区分;
  • 由于请求头部仅以冒号分割,因此值 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);
    }
}

测试

  • 测试结果

rustdesktop自建服务器 windows rust如何创建服务器_HTTP_07

  • 其中需要留意的点位
  • 在实现 Stringtrait 时,不能从 &res1.body 获取长度,以避免内部 body 成员的所有权转移;
  • 测试整个相应,自定义响应实例中的请求体数据长度要保持一致;

四、构建Server模块


模块准备

  • 此时转至 httpserver 子项目内,将前文所涉及的 http 子项目导入 Cargo.toml 文件;
  • 并在 httpserver/src 下再创建三文件
  • server.rs
  • router.rs
  • handler.rs

rustdesktop自建服务器 windows rust如何创建服务器_tcp/ip_08

rustdesktop自建服务器 windows rust如何创建服务器_http_09

功能实现

  • 大概的调用逻辑
  • 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

rustdesktop自建服务器 windows rust如何创建服务器_后端_10

  • 测试文件内容
<!-- 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

rustdesktop自建服务器 windows rust如何创建服务器_http_11

访问 health.html

rustdesktop自建服务器 windows rust如何创建服务器_HTTP_12

访问 orders.json

rustdesktop自建服务器 windows rust如何创建服务器_tcp/ip_13

访问一个错误地址

rustdesktop自建服务器 windows rust如何创建服务器_http_14

  • 至此,HTTP的基本功能实现就到此为止;
  • 可以基于此框架做性能优化以及扩展自己所需要的功能;
  • 通过本次HTTP《简易》设计,可以更深刻地体会一些后端设计思想、Rust本身的特点以及基于HTTP协议的Server设计思路;