Rust Web 全栈开发(二):构建 HTTP Server
- [Rust Web 全栈开发(二):构建 HTTP Server](#Rust Web 全栈开发(二):构建 HTTP Server)
-
- 创建成员包/库:httpserver、http
- [解析 HTTP 请求](#解析 HTTP 请求)
-
- [HTTP 请求的构成](#HTTP 请求的构成)
- [构建 HttpRequest](#构建 HttpRequest)
- [构建 HTTP 响应](#构建 HTTP 响应)
-
- [HTTP 响应的构成](#HTTP 响应的构成)
- [构建 HttpResponse](#构建 HttpResponse)
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,编写代码:
rust
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,编写代码:
rust
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 个测试,都通过了:

lib
打开 http 成员库中的 lib.rs,编写代码:
rust
pub mod httprequest;
pub mod httpresponse;