- 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);
}
}
测试
- 测试结果
- 其中需要留意的点位
- 在实现
String
的trait
时,不能从&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设计思路;