赞
踩
- Rust没有自带HTTP支持,因此很多的方法及接口都需要开发者自行设计实现,不过对于Web Server,不同业务对其规模及性能的要求不尽相同,这样一想也情有可原;
- 对于Rust基础以及HTTP原理,需要读者有所认识;
- 本文的设计思路也可以自行设计扩展进而发展成更完整的方案;
目录
在自定的目录下,创建两个子项目目录
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<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
内的空格不能忽略,或者进行进一步优化;
以下为自建库的响应构建部分;
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
成员的所有权转移;测试整个相应,自定义响应实例中的请求体数据长度要保持一致;
此时转至
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<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设计思路;
每一个不曾起舞的日子,都是对生命的辜负。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。