Rust Web 全栈开发(二):构建 HTTP Server
- Rust Web 全栈开发(二):构建 HTTP Server
- 创建成员包/库:httpserver、http
- 解析 HTTP 请求
- HTTP 请求的构成
- 构建 HttpRequest
- 构建 HTTP 响应
- HTTP 响应的构成
- 构建 HttpResponse
- 构建 server 模块
- 构建 router 模块
- 构建 handler 模块
Rust Web 全栈开发(二):构建 HTTP Server
参考视频:https://www.bilibili.com/video/BV1RP4y1G7KF
Web Server 的消息流动图:

Server:监听 TCP 字节流
Router:接收 HTTP 请求,并决定调用哪个 Handler
Handler:处理 HTTP 请求,构建 HTTP 响应
HTTP Library:
- 解释字节流,把它转换为 HTTP 请求
- 把 HTTP 响应转换回字节流
构建步骤:
- 解析 HTTP 请求消息
- 构建 HTTP 响应消息
- 路由与 Handler
- 测试 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!
各个页面的显示效果如下所示:



