- Rust没有自带HTTP支持,因此很多的方法及接口都需要开发者自行设计实现,不过对于Web Server,不同业务对其规模及性能的要求不尽相同,这样一想也情有可原;
- 对于Rust基础以及HTTP原理,需要读者有所认识;
- 本文的设计思路也可以自行设计扩展进而发展成更完整的方案;
目录
Rust Web(二)—— 自建HTTP Server
一、项目创建
二、解析HTTP请求
测试示例
全部实现
测试
三、构建HTTP响应
测试
四、构建Server模块
模块准备
功能实现
五、构建 Router & Handler 模块
实现代码
六、完整测试
运行
在自定的目录下,创建两个子项目目录
httpserver
http
http
为·lib
库,故命令中添加--lib
在根项目的
Cargo.toml
文件中添加这两个子项目
进入
http
子项目,在src/lib.rs
内写入公共模块pub mod httprequest;
在同级
src
目录下新建:
httprequest.rs
httpresponse.rs
在
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,
pub msg_body: String,
}
impl From 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
内的空格不能忽略,或者进行进一步优化;
以下为自建库的响应构建部分;
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>,
body: Option,
}
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> 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>,
body: Option
) -> 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
成员的所有权转移;测试整个相应,自定义响应实例中的请求体数据长度要保持一致;
此时转至
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);
}
}
}
实现至当前阶段还不能直接运行;
这两个模块联合起来处理接收到的请求,其中
判定请求的合法性,适当返回错误反馈;
解析后台的数据部分,进行相应的序列化和反序列化;
不同的请求状况交由不同类型的句柄
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 {
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 {
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 = 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
Hello,welcome to home page
This is the index page for the web site
Health!
Hello,welcome to health page!
This site is perfectly fine
Not Found!
404 Error
Sorry! The requested page does not exist
/* 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设计思路;
每一个不曾起舞的日子,都是对生命的辜负。