Rust Web入门(一):TCP 和 HTTP Server

news2024/11/16 23:44:03

本教程笔记来自 杨旭老师的 rust web 全栈教程,链接如下:

https://www.bilibili.com/video/BV1RP4y1G7KF?p=1&vd_source=8595fbbf160cc11a0cc07cadacf22951

学习 Rust Web 需要学习 rust 的前置知识可以学习杨旭老师的另一门教程

https://www.bilibili.com/video/BV1hp4y1k7SV/?spm_id_from=333.999.0.0&vd_source=8595fbbf160cc11a0cc07cadacf22951

或者查看作者的另一篇专栏:

https://blog.csdn.net/weixin_46463785/category_12155724.html?spm=1001.2014.3001.5482

下面正式开始 Rust Web 全栈开发教程的学习:

TCP Server

在编写 web 之前,我们首先尝试编写一个能够相互通信的服务器和客户端程序,之后基于这个架构来编写我们的前后端交互的 web 应用。我们的通信程序基于 TCP 协议,这里使用了 rust 的 std::net 这个模块:

首先我们创建一个工作空间 s1 ,然后创建两个项目,再将我们的两个项目都包含进来

cargo new s1 
cd s1
cargo new tcpserver
cargo new tcpclient

s1 / Cargo.toml

[workspace]
members = ["tcpserver", "tcpclient"]

服务端

之后进入 tcpserver 项目的 main.rs,我们需要导入 TcpListener 这个包,它绑定一个端口,之后监听这个端口的信息,使用 incoming 方法,它返回一个迭代器,返回一个 Result ,我们使用 unwrap 方法可以拿到我们的数据

use std::io::{Read, Write};
use std::net::TcpListener;
fn main() {
    let listener = TcpListener::bind("127.0.0.1:3000").unwrap();
    println!("Listening on port 3000");
    for stream in listener.incoming() {
        let mut _stream = stream.unwrap();
        println!("connection established.");
    }
}

现在我们可以在命令行根目录运行这个服务

cargo run -p tcpserver

客户端

之后我们编写我们的客户端,客户端需要在 127.0.0.1:3000 这个端口建立连接,我们使用

TcpStream::connect 这个函数。

use std::{
    io::{Read, Write},
    net::TcpStream,
    str,
};
fn main() {
    let mut _stream = TcpStream::connect("127.0.0.1:3000").unwrap();
}

现在我们可以启动我们的客户端,可以看到它建立了连接

cargo run -p tcpclient

收发数据

现在我们有了一个相互连接的客户端和服务端,之后我们希望他们相互可以发送消息,因为我们的服务器通信使用的 stream 它实现了 io 的 Write 和Read,所以我们可以使用相关的操作来接收和传递我们的消息。

在 tcpserver 里,我们使用新建一个 buff 缓存区来接收我们的数据,之后再使用 write 把它发送回去

use std::io::{Read, Write};
use std::net::TcpListener;
fn main() {
    let listener = TcpListener::bind("127.0.0.1:3000").unwrap();
    println!("Listening on port 3000");
    for stream in listener.incoming() {
        let mut _stream = stream.unwrap();
        println!("connection established.");
        let mut buffer = [0; 1024];
        _stream.read(&mut buffer).unwrap();
        _stream.write(&mut buffer).unwrap();
    }
}

在 client 中,我们也是同样的操作,我们先写入一个 “hello” 字符串,要注意,传输的时候要传递原始的字节,所以要使用 as_bytes 进行转化,之后我们接收服务器返回的消息,根据刚才的代码,我们会收到 hello 的信息,我们将它输出

use std::{
    io::{Read, Write},
    net::TcpStream,
    str,
};
fn main() {
    let mut _stream = TcpStream::connect("127.0.0.1:3000").unwrap();
    _stream.write("hello".as_bytes()).unwrap();
    let mut buf = [0; 5];
    _stream.read(&mut buf).unwrap();
    println!("{}", str::from_utf8(&buf).unwrap());
}

现在我们重新启动这两个程序,注意是先启动 server 再启动 client 可以看到在 client 打印出了 hello ,那么你的服务器就完成了。

HTTP Server

请添加图片描述

这是一个 web server 的消息流动图,当我们收到一个来自客户端浏览器的请求的时候,我们的服务器将请求放到路由器上,路由器决定我们调用哪个方法来提供一个响应返回客户端。

在 rust 中没有内置的HTTP 支持,所以我们需要的是监听 TCP 字节流,将它进行处理,分析出 HTTP 请求的信息,然后通过路由器决定调用的方法,所以这个项目的架构和之前的 TCP Server 服务器一致。我们创建一个 http 项目,然后在其中新建一个 httprequest.rs 和 一个 httpresponse.rs 作为我们服务器的收发工具

解析 HTTP

首先我们需要在之前的服务器的基础上增加解析 HTTP 请求的内容,我们需要三个数据结构来处理我们的请求, Method 和 Version 枚举来处理我们的收到请求的方法和版本,HttpRequest 结构体来处理我们一个请求的全部内容,他们需要实现三个Trait:

  • From<&str> 用于把传进来的字符串切片转化成处理完毕的信息,
  • Debug用于打印调试信息,
  • PartialEq 用于解析和自动化测试脚本里做比较

我们先编写枚举类,可以编写一个测试来实验一下我们的类是不是编写成功

use std::collections::HashMap;

#[derive(Debug, PartialEq)]
pub enum Method {
    Get,
    Post,
    Uninitialized,
}

impl From<&str> for Method {
    fn from(s: &str) -> Self {
        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,
        }
    }
}


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

}

之后我们编写我们的 HttpRequest 的解析,按照 HTTP 的规范,分别获取方法、版本、路径、请求头和参数,其中请求头中的参数我们按照 key-value 对的方式来存储,所以我们使用了 HashMap 这样的结构

//请求路径是一个String ,我们通过枚举来包装它
#[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 msd_body: String,
}

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

        for line in req.lines() {
            //判定是不是一个 HTTP 请求的开始
            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(":") {
                //头部是 key:value 形式的,所以可以这样判定是不是进入到头部
                let (key, value) = process_header_line(line);
                parsed_headers.insert(key, value);
            } else {
                //剩余部分是参数,用于post方法
                parsed_msg_body = line;
            }
        }
		//封装成一个 HttpRequest
        HttpRequest {
            method: parsed_method,
            version: parsed_version,
            resource: parsed_resource,
            headers: parsed_headers,
            msd_body: parsed_msg_body.to_string(),
        }
    }
}

fn process_req_line(s: &str) -> (Method, Resource, Version) {
    //用空白字符分割成数组,之后一个一个遍历就依次是我们需要的内容
    let mut words = s.split_whitespace();
    let methods = words.next().unwrap();
    let resource = words.next().unwrap();
    let version = words.next().unwrap();
    (
        methods.into(),
        Resource::Path(resource.to_string()),
        version.into(),
    )
}

//取出一个一个的 key - value 对
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.trim().to_string();
    }
    if let Some(v) = header_items.next() {
        value = v.trim().to_string();
    }
    (key, value)
}

然后我们再编写一个测试,如果顺利通过说明我们的代码正常

 #[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 expected_header: HashMap<String, String> = HashMap::new();
        expected_header.insert("Host".into(), "localhost".into());
        expected_header.insert("Accept".into(), "*/*".into());
        expected_header.insert("User-Agent".into(), "curl/7.71.1".into());

        println!("{:?}", expected_header);
        let req: HttpRequest = s.into();
        assert_eq!(Method::Get, req.method);
        assert_eq!(Version::V1_1, req.version);
        assert_eq!(Resource::Path("/greeting".to_string()), req.resource);
        assert_eq!(expected_header, req.headers);
    }

HTTP 响应

对于一个收到的 HTTP 请求,我们的服务器需要做出应答,所以和解析 HTTP 一样,我们需要编写一个封装 HTTP 应答的类,我们先定义我们的结构,它包含版本,状态码,状态信息,头部和应答体,通过实现 Default Trait ,我们可以给我们的应答体一个默认值:

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,
        }
    }
}

之后我们给他提供一个 new 方法来初始化一个 HttpResponse,根据传入的状态码来判定状态信息,并且为空的 headers 填充一个默认的 Content-Type 字段来保证程序的健壮性:

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.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
    }
}

之后我们实现一些 get 方法来获取我们的数据,我们将数据都返回为 字符串或者字符串切片类型,目的是为了之后可以更加方便的将他们整合到一起

impl<'a> HttpResponse<'a> {
    //...
 	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 => "",
        }
    }
}

有了这些方法我们就可以将我们的数据整合为一个字符串了,之后再将这个字符串发送出去即可,为了整合我们的字符串,我们为 String 实现 From 这个 Trait,传入一个 HttpResponse 返回一个组装好的 HTTP 响应字符串:

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

有了这个 Trait 我们就可以实现我们的返回方法,将我们封装的 HttpResponse 变成一个标准的 HTTP 响应发送出去,它接收一个 stream ,也就是上文中 TCP 服务器中我们使用的 stream,因为我们需要用这个 stream 发送数据,所以我们限定它需要实现 Write 这个 Trait:

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

        Ok(())
    }
}

最后我们为我们的 HttpResponse 编写测试测试,整个 DEMO 的完整代码如下:

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();
        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);
        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("xxxx".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("xxxx".into()),
        };
        assert_eq!(response_actual, response_expected);
    }

    #[test]
    fn test_response_struct_creation_404() {
        let response_actual = HttpResponse::new("404", None, Some("xxxx".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("xxxx".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("xxxx".into()),
        };
        let http_string: String = response_expected.into();
        let actual_string =
            "HTTP/1.1 404 Not Found\r\nContent-Type:text/html\r\nContent-Length: 4\r\n\r\nxxxx"
                .to_string();
        assert_eq!(http_string, actual_string);
    }
}

HTTP 服务器的编写

好了现在我们拥有了可以解析和发送 HTTP 请求响应的工具了,此时我们可以编写我们的服务器了,我们需要先将之前编写的 http 类引用进来,再安装我们需要的包,首先,我们在之前的 http 项目下新建一个 lib.rs 导出我们编写的模块

pub mod httprequest;
pub mod httpresponse;

之后我们新建一个 httpserver 项目,在 Cargo.toml 中导入我们的包,包括我们刚刚编写的 http 以及处理json 和 序列化的库

[package]
name = "httpserver"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
http = { path = "../http" }
serde = { version = "1.0.131", features = ["derive"] }
serde_json = "1.0.72"

之后我们就可以开始编写我们的服务器了,首先我们在 src 文件夹下新建 server.rs 文件用于服务器的运行,它接收一个服务器的地址之后在指定的地址启动我们的服务器,循环接收数据,结构和之前的 TCP server 一致,之后调用我们刚刚编写的 HttpRequest 进行解析:

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

在 main.rs 中,我们调用我们刚刚编写的方法在本地启动我们的服务器

mod handler;
mod router;
mod server;
use server::Server;
fn main() {
    println!("Hello, world!");
    let server = Server::new("localhost:3000");
    server.run();
}

现在我们的服务器可以运行了,之后我们要对我们接收到的 HttpRequest 进行处理,我们编写一个 router.rs 和 handler.rs 处理我们的请求,首先编写我们的 handler.rs ,我们定义一个 Handler ,它提供一个 handle 方法返回 http 响应,同时包含一个 load_file 函数加载我们需要的数据:

  • env!(“”) 可以拿到编译时的环境变量,env!(“CARGO_MANIFEST_DIR”)是调用环境变量拿到根目录
  • env::var(“”) 可以拿到构建时的环境变量,注意这些变量需要编写在文件中 .env 读取,我们暂时不编写我们的路径,那么就让这个路径等于我们的 default_path ,如果后期我们的项目发布了,可以通过配置 .env 文件来修改我们需要的路径
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> {
        //取到文件存放的根目录
        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()
    }
}

我们将我们需要提供的资源文件放在 public 文件夹下方,方便进行统一的管理,之后我们创建 404.html,index.html , health.html 和 styles.css 供我们测试使用。

我们将我们的响应分为三种,找不到请求内容的使用 PageNotFoundHandler ,加载静态资源的使用 StaticPageHandler ,加载接口数据的使用 WebServiceHandler ,他们都需要实现 Handler 这个 trait

pub struct StaticPageHandler;
pub struct PageNotFoundHandler;
pub struct WebServiceHandler;
  • 对于 PageNotFoundHandler 我们直接返回404状态 ,并且提供一个 404.html 作为返回
impl Handler for PageNotFoundHandler {
    fn handle(_req: &HttpRequest) -> HttpResponse {
        HttpResponse::new("404", None, Self::load_file("404.html"))
    }
}
  • 对于 StaticPageHandler ,我们先分割请求路径获得我们需要资源的地址,如果是页面路径则加载对应的页面;如果是请求静态文件,对于不同的文件类型,我们需要加上不一样的头部
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-Tvpe", "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")),
            },
        }
    }
}

对于 WebServiceHandler 我们需要提供一个 load_json 方法来处理我们的 json 文件,我们编写一个 orders.json 文件放在 data 文件夹下提供我们测试使用,这里我们只是用 json 文件做一个简单的测试,后续这部分将会替换成通过数据库查询得到的数据,成为一个完整的 api 后台

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

为了解析 json 我们需要我们的 serde_json 这个包,同时创建一个结构体来定义我们收到的 json 的每一项的内容,因为它对应的是 json 里的数据,所以我们要给他提供序列化和反序列化的方法

#[derive(Serialize, Deserialize)]
pub struct OrderStatus {
   order_id: i32
   order_date: String,
   order_status: String,
}
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
   }
}

这里我们假设我们的路径是 127.0.0.1:3000 / api / shipping / orders,通过分割字符串来解析出我们的路径判定 / shipping / orders 是不是匹配,如果路径正确,我们就将我们的 json 数据取出放到我们的 body 中返回,同时要修改我们的头部的来表示这是一组 json 数据,至于 / api 路径的处理,我们放在我们的 router 方法中:

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

        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("2oo", Some(headers), body)
            }
            _ => HttpResponse::new("404", None, Self::load_file("404.html")),
        }
    }
}

至此我们的 handler.rs 编写完毕,我们对静态资源,api接口和找不到路径三种情况进行判定,提供了返回,完整的代码如下:

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> {
        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-Tvpe", "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();

        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("2oo", Some(headers), body)
            }
            _ => HttpResponse::new("404", None, Self::load_file("404.html")),
        }
    }
}

最后我们编写我们的路由 router.rs , 它就很简单,分割我们拿到的 http 路径,根据路径和方法的不同分别调用不同的 handler 即可,这里我们先只处理 GET 请求,POST 的处理将会后续更新

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

我们将我们的路由处理方法放在之前编写的 server.rs 中,现在每当你收到一个 HTTP 的请求,服务器就会解析你的路径,通过不同的方式进行处理后返回给你数据:

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

你可以在命令行中启动我们的项目,然后进行如下的测试

  • 在浏览器访问 http://localhost:3000/ 可以看到 index.html 的内容(有红色字体)
  • 在浏览器访问 http://localhost:3000/health 可以看到 health.html 的 内容
  • 在浏览器访问 http://localhost:3000/api/shipping/orders 可以看到一组订单数据
  • 在浏览器访问 http://localhost:3000/hhhhhhhhh 将会弹出 404.html 页面

如果你的测试完全符合预期,那么你的服务器编写成功了,否则你可以查看这个 git 仓库

https://github.com/FrancisYLfan/rust_web_server_from_yang

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/354779.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【原创】java+swing+mysql图书管理系统设计与实现

图书管理系统是一个比较常见的系统&#xff0c;今天我们主要介绍如何使用javaswiingmysql去开发一个cs架构的图书管理系统&#xff0c;方便学生进行图书借阅。 功能分析&#xff1a; 宿舍报修管理系统的使用角色&#xff0c;一般分为管理员和学生&#xff0c;管理员主要进行学…

学习OpenGL图形2D/3D编程

环境&#xff1a;WindowsVisual Studio 2019最流行的几个库&#xff1a;GLUT&#xff0c;SDL&#xff0c;SFML和GLFWGLFWGLAD库查看显卡OPENGL支持情况VS2019glfwgladopenGL3.3顶点着色器片段着色器VAO-VBO-(EBO)->渲染VAO-VBO-EBO->texture纹理矩阵matrix对图形transfor…

jmx prometheus引起的一次cpu飙高

用户接入了jmx agent进行prometheus监控后&#xff0c;在某个时间点出现cpu飙高 排查思路&#xff1a; 1、top&#xff0c;找到java进程ID 2、top -Hp 进程ID&#xff0c;找到java进程下占用高CPU的线程ID 3、jstack 进程ID&#xff0c;找到那个高CPU的线程ID的堆栈。 4、分析堆…

jenkins基础部署

一、jenkins是什么1.Jenkins的前身是Hudson&#xff0c;采用JAVA编写的持续集成开源工具。Hudson由Sun公司在2004年启动&#xff0c;第一个版本于2005年在java.net发布。2007年开始Hudson逐渐取代CruiseControl和其他的开源构建工具的江湖地位。在2008年的JavaOne大会上在开发者…

【Vue3源码】第二章 effect功能的完善下

【Vue3源码】第二章 effect功能的完善下 前言 上一章节我们实现了effect函数的runner 和 scheduler&#xff0c;这一章我们继续完善effect函数的功能&#xff0c;stop和onstop。 1、实现effect的stop功能 顾名思义&#xff0c;stop就是让effect停下来的函数。那么怎么才能让…

系统分享|分享几个Windows系统镜像下载网站

&#x1f4e3;今日作品&#xff1a;如何关闭Microsoft start方法介绍&#x1f466;创作者&#xff1a;Jum朱⏰预计花费&#xff1a;10分钟&#x1f4d6;个人主页&#xff1a;Jum朱博客的个人主页系统之家传送门&#xff1a;https://www.xitongzhijia.net/这个是老牌一直还在运营…

聊聊RocketMQ 的功能特性

这是RocketMQ的第三篇文章&#xff0c;前两篇文章我们说了一下rocketmq的入门安装和开发配置&#xff0c;以及他的一些名词解释&#xff0c;RocketMQ入门第一次&#xff0c;RocketMQ&#xff08;二&#xff09; 领域名词。今天我们来说说的他的一些功能特性。明确区分这些功能特…

[AI生成图片] 效果最好的Midjourney 的介绍和使用

Midjourney介绍&#xff1a; 是一个文本生成图片的扩散模型&#xff0c;能够根据输入的任何文本生成令人难以置信的图像&#xff0c;让数十亿人在几秒钟内创造惊人的艺术。为方便用户控制和快速生成图片&#xff0c;打开后在页面底部输入文本内容&#xff0c;稍等一小会&#…

基于easyexcel的MySQL百万级别数据的excel导出功能

前言最近我做过一个MySQL百万级别数据的excel导出功能&#xff0c;已经正常上线使用了。这个功能挺有意思的&#xff0c;里面需要注意的细节还真不少&#xff0c;现在拿出来跟大家分享一下&#xff0c;希望对你会有所帮助。原始需求&#xff1a;用户在UI界面上点击全部导出按钮…

全网多种方式解决The requested resource [/] is not available的错误

文章目录1. 复现错误2. 分析错误3. 解决错误3.1 本地项目3.2 线上项目4. 此错误的其他解决方法5. 补充说明1. 复现错误 曾记得&#xff0c;当初使用idea来写Java web项目时&#xff0c;常常因为Tomcat配置导致如下错误&#xff1a; 即The requested resource [/] is not avail…

自动化完成1000个用户的登录并获取token并生成tokens.txt文件

自动化完成1000个用户的登录并获取token并生成tokens.txt文件 写作背景 在我学习使用redis实现秒杀功能的过程中&#xff0c;在编写完秒杀代码后&#xff0c;需要使用Jmeter实际测试1000个用户进行秒杀&#xff0c;由于秒杀功能需要在用户登录完成后才能实现&#xff0c;用户是…

DFS深度优先搜索—Java版

递归三要素 递归的定义 递归的拆解 递归的出口 什么时候使用DFS&#xff1f; 深度回溯问题&#xff08;DFS与回溯区别不大&#xff09; 二叉树问题 组合、排列问题 找方案问题&#xff08;解空间是一棵树或者图&#xff0c;需要自行构造图/树&#xff09; 图的搜索问题…

Smokeping的主从模式部署

Smokeping 支持 Standalone&#xff08;单机&#xff09;模式和 Master/Slave&#xff08;主从&#xff09;模式。 之前老苏折腾过单机模式&#xff0c;这次应网友 Roxmie 的要求&#xff0c;研究了一下主从模式的部署 文章传送门&#xff1a; 网络性能监控工具Smokeping 因为…

Go基础-环境安装

文章目录1 Go?Golang?2 下载Go3 windows安装4 测试是否成功1 Go?Golang? Go也称为Golang&#xff0c;是Google开发的一个开源的编译型的静态语言。 Golang的主要关注点是高可用、高并发和高扩展性&#xff0c;Go语言定位是系统级编程语言&#xff0c;对web程序具有很好的支…

SAP数据导入工具(LSMW) 超级详细教程(批量导入内部订单)

目录 第一步&#xff1a;记录批导步骤编辑数据源对应字段 第二步&#xff1a;维护数据源 第三步&#xff1a;维护数据源对应字段&#xff08;重要&#xff09; 第四步&#xff1a;维护数据源关系。 第五步&#xff1a;维护数据源与导入字段的对应关系。 第六步&#xff0…

K_A12_006 基于STM32等单片机驱动BH1750模块 串口与OLED0.96双显示

K_A12_006 基于STM32等单片机驱动BH1750模块 串口与OLED0.96双显示一、资源说明二、基本参数参数引脚说明三、驱动说明时序对应程序:四、部分代码说明1、接线引脚定义1.1、STC89C52RCBH1750模块1.2、STM32F103C8T6BH1750模块五、基础知识学习与相关资料下载六、视频效果展示与程…

《蓝桥杯每日一题》递归·AcWing 1497. 树的遍历

1.题目描述一个二叉树&#xff0c;树中每个节点的权值互不相同。现在给出它的后序遍历和中序遍历&#xff0c;请你输出它的层序遍历。输入格式第一行包含整数 N&#xff0c;表示二叉树的节点数。第二行包含 N个整数&#xff0c;表示二叉树的后序遍历。第三行包含 N 个整数&…

设计模式之迭代器模式与命令模式详解和应用

目录1 迭代器模式1.1 目标1.2 内容定位1.3 迭代器模式1.4 迭代器模式的应用场景1.5 手写字定义的送代器1.6 迭代器模式在源码中的体现1.7 迭代器模式的优缺点2 命令模式2.1 定义2.2 命令模式的应用场景2.3 命令模式在业务场景中的应用2.4 命令模式在源码中的体现2.5 命令模式的…

UVa 211 The Domino Effect 多米诺效应 暴力搜索

题目链接&#xff1a;UVa 211 The Domino Effect 题目描述&#xff1a; 一张多米诺骨牌拥有两个数值&#xff0c;一共有二十八张不同的多米诺骨牌&#xff0c;这二十八张多米诺骨牌的点数如下图所示&#xff1a; 上图的BoneBoneBone代表编号&#xff0c;而PipsPipsPips代表两个…

Springboot扩展点系列之终结篇:Bean的生命周期

前言关于Springboot扩展点系列已经输出了13篇文章&#xff0c;分别梳理出了各个扩展点的功能特性、实现方式和工作原理&#xff0c;为什么要花这么多时间来梳理这些内容&#xff1f;根本原因就是这篇文章&#xff1a;Spring bean的生命周期。你了解Spring bean生命周期&#xf…