加载中...

基于 Rust 标准库 API 使用 200 行代码实现 Http 1.1 协议简易服务

基于 Rust 标准库 API 使用 200 行代码实现 Http 1.1 协议简易服务

1. 背景

早在之前学过一波 Rust,但是由于没用武之地,没过多久又荒废了,最近想捡起来下。刚好看见有群里小伙伴说学习 Http 网络协议太难怎么办?其实很多技术都是相通的,只要你理解了技术的本质就可以自己实现它,这里就基于 Rust 用 200+ 代码通过 TCP API 实现了一个 Http 1.1 协议的雏形(全程减少使用 unwrap 等非最佳实践写法),够初学 Http 协议的小伙伴用来借鉴思路完善自己的协议学习了。

Http 1.1 协议属于应用层协议,构建于 TCP/IP 协议之上,处于 TCP/IP 协议架构层的顶端,所以,它不用处理下层协议间诸如丢包补发、握手及数据的分段及重新组装等繁琐的细节,使开发人员可以专注于应用业务。但这也是他的缺陷,具体为什么说是缺陷,建议继续看看 Http2 和 Http3 协议的演进和解决的问题就知道了。

对应一次请求的报文典型如下:

bash
代码解读
复制代码
yan% curl http://www.yan.com/ -v * Trying 120.232.145.144:80... * Connected to www.baidu.com (120.232.145.144) port 80 > GET / HTTP/1.1 > Host: www.baidu.com > User-Agent: curl/8.4.0 > Accept: */* > < HTTP/1.1 200 OK < Accept-Ranges: bytes < Cache-Control: private, no-cache, no-store, proxy-revalidate, no-transform < Connection: keep-alive < Content-Length: 2381 < Content-Type: text/html < Date: Fri, 05 Apr 2024 01:25:41 GMT < Etag: "588604eb-94d" < Last-Modified: Mon, 23 Jan 2017 13:28:11 GMT < Pragma: no-cache < Server: bfe/1.0.8.18 < Set-Cookie: BDORZ=27315; max-age=86400; domain=.baidu.com; path=/ < 加微信:bitdev * Connection #0 to host www.baidu.com left intact

具体 Http 1.1 协议的细节可以参考官方协议规范指导,比较简单,这里主要是基于 Rust 实现而已。

2. 目标

源码位于 github.com/yanbober/st…。 我们要实现的是这个完整流程,如下: New Image

代码量很少且非常适合 Rust 语言学习时用来实践,尽量不实用三方库,项目工程采用最佳实践组织,代码结构如下:

bash
代码解读
复制代码
| - study-http-server-rust 工程目录 | | - http_server 抽象服务 crate | | | - src/lib.rs 模块对外 API | | | - src/server.rs HTTP 1.1 服务管理 | | | - src/server/protocol.rs 基于 TCP 的 HTTP 1.1 协议实现 | | - demo-app 业务模拟 crate | | | - src/main.rs 模拟服务承载的业务路由实现

期望实现后使用方法:

2.1 HTTP 1.1 服务端启动

  1. github 拉取代码
  2. 根目录运行cargo runcargo run --release
  3. 控制台看见Http Server Started at 127.0.0.1:4221日志表示服务启动成功

2.2 客户端访问服务端基于此服务实现的业务

代码解读
复制代码
说明:你可以使用浏览器或 Postman 等工具,这里推荐使用 curl 命令行工具进行调试。
bash
代码解读
复制代码
# 场景1:GET/POST 请求根服务进入欢迎词 curl http://127.0.0.1:4221 -v # 场景2:GET/POST 请求 /user-agent 返回客户端的 User-Agent 信息 curl http://127.0.0.1:4221/user-agent -v # 场景3:GET/POST 请求 /echo/YOU_DEFINED 返回请求链接的 YOU_DEFINED curl http://127.0.0.1:4221/echo/bitdev -v # 场景4:GET 请求 /files/readme.txt 返回服务端静态资源 readme.txt 内容 curl http://127.0.0.1:4221/files/readme.txt -v # 场景5:POST 请求 /files/upload.txt 将 POST Boday 内容写入到服务端静态资源 /files/upload.txt 里面 curl http://127.0.0.1:4221/files/aaa -v -d dsfsfsfsgsg

3 基于 Rust 实现

3.1 报文协议实现(protocol.rs)

为实现报文协议,我们需要先拆解构思结构,所以大致分为如下几个部分。

3.1.1 通用行为定义

这里我们定义 HttpMethod 和 ContentType,便于后面进行类型转换枚举,如下:

rust
代码解读
复制代码
//我们只实现 GET/POST,其他的感兴趣自己补充 pub enum HttpMethod { GET, POST } //&str 和 HttpMethod 相互转换实现 impl From<&str> for HttpMethod { fn from(value: &str) -> Self { match value.to_uppercase().as_str() { "GET" => HttpMethod::GET, "POST" => HttpMethod::POST, _ => { eprintln!("HttpMethod {} not support, please impl it, default to GET.", value); HttpMethod::GET } } } } // 只实现了三个常见的 ContentType,其他自己感兴趣补充 pub enum ContentType { Plain, Json, OctetStream, } //String 和 ContentType 相互转换实现 impl From<String> for ContentType { fn from(value: String) -> Self { match value.to_uppercase().as_str() { "text/plain" => ContentType::Plain, "text/json" => ContentType::Json, "application/octet-stream" => ContentType::OctetStream, _ => { eprintln!("ContentType {} not support, default to text/plain.", value); ContentType::Plain } } } }

3.1.2 http request 报文处理

如背景部分介绍,我们需要对 Http 1.1 协议按照报文格式定义结构,以便将 TCP 传输的报文转换为 Http 1.1 格式协议,大致如下(最核心的关键就是 try_from 方法将报文协议规范化):

rust
代码解读
复制代码
// HTTP/1.1 请求协议实现 pub struct HttpRequest { pub method: HttpMethod, pub path: String, pub headers: HashMap<String, String>, pub body: String, } //最核心的关键 impl TryFrom<String> for HttpRequest { type Error = HttpError; fn try_from(request: String) -> Result<Self, Self::Error> { //按照背景介绍中的协议规则,对上下两大块进行切分,即 head、body let parts: Vec<&str> = request.split("\r\n\r\n").collect(); if parts.len() < 2 { return Err(HttpError::InvalidFormat(request)); } let head_parts = parts[0]; let body_parts = parts[1]; //对 head 部分按照行切分,然后行里面再处理分段协议 let head_lines: Vec<&str> = head_parts.split("\r\n").collect(); let request_tokens: Vec<&str> = head_lines.first().unwrap_or(&"").split(" ").collect(); let mut headers_map: HashMap<String, String> = HashMap::new(); for kv_line in head_lines.iter().skip(1) { if let Some((k, v)) = kv_line.split_once(":") { headers_map.insert(k.trim().to_string(), v.trim().to_string()); } } //协议解析后组装成 HttpRequest 结构即可 Ok(HttpRequest { method: HttpMethod::from(request_tokens[0]), path: request_tokens[1].to_string(), headers: headers_map, body: body_parts.to_string(), }) } }

到此客户端的请求上来后我们就从 TCP 报文变为了 HttpRequest 结构。

3.1.3 http response 报文处理

类似 request 报文处理的思路,我们对 response 实现如下:

rust
代码解读
复制代码
// HTTP/1.1 响应协议实现 pub struct HttpResponse { pub status: u16, pub content: String, pub content_type: ContentType, pub content_length: usize, } impl HttpResponse { pub fn new(status: u16, content: &str, content_type: ContentType) -> Self { HttpResponse { status, content: content.to_string(), content_type, content_length: content.len() } } //将HttpResponse转为TCP报文后通过TcpStream返回客户端 pub fn response(res: HttpResponse, stream: &mut TcpStream) -> Result<String, HttpError> { let mut str_buf = String::new(); str_buf.push_str("HTTP/1.1 "); match res.status { 200 => str_buf.push_str("200 OK\r\n"), 201 => str_buf.push_str("201 OK\r\n"), 404 => str_buf.push_str("404 Not Found\r\n"), _ => return Err(HttpError::UnsupportStatus(res.status.to_string())), } match res.content_type { ContentType::Plain => str_buf.push_str("Content-Type: text/plain\r\n"), ContentType::Json => str_buf.push_str("Content-Type: text/json\r\n"), ContentType::OctetStream => { str_buf.push_str("Content-Type: application/octet-stream\r\n") } } str_buf.push_str(format!("Content-Length: {}\r\n\r\n", res.content_length).as_str()); str_buf.push_str(&res.content); stream.write_all(str_buf.as_bytes())?; Ok(str_buf) } }

到此 Http1.1 协议的收发报文处理雏形就 OK 实现了。

3.2 基于上面封装协议的 HttpServer 实现(server.rs)

这里我们采用模拟一个简单的 server 路由框架,具体通过回调函数实现。

rust
代码解读
复制代码
//定义给业务实现的回调函数类型,以便业务能基于 Http 协议处理自己的路由业务实现 pub type HandleHttp = fn(&HttpRequest) -> Result<HttpResponse, HttpError>; // 启动 Http 服务,基于 TCP 实现 pub fn start_http_server(address: &str, port: u16, handle_http: HandleHttp) { //TCP bind,请求后不终止链接,也就是“多路复用”socket,实际标准协议这里很复杂 let listener = TcpListener::bind(format!("{}:{}", address, port)); match listener { Ok(listener) => { println!("Http Server Started at {}:{}", address, port); for stream in listener.incoming() { match stream { Ok(mut stream) => { let handle_http = handle_http.clone(); // 多线程并发处理,可以同时处理多个 http 请求 let _ = thread::spawn(move || { //处理一个业务http请求 handle_stream_connect_default(&mut stream, handle_http); }); } Err(e) => { eprintln!("Accept new connection error:{}", e); } } } } Err(e) => { eprintln!("Start Http Server Error:{}", e); } } } // 处理一个 http 连接请求,兜底容错处理 fn handle_stream_connect_default(tcp_stream: &mut TcpStream, handle_http: HandleHttp) { let result = handle_stream_connect(tcp_stream, handle_http); if result.is_err() { eprintln!("handle_stream_connect error:{}", result.err().unwrap()); } } // 处理一个 http 连接请求 fn handle_stream_connect( tcp_stream: &mut TcpStream, handle_http: HandleHttp ) -> Result<(), HttpError> { println!( "Accepted a new connection from {}.", tcp_stream.peer_addr()? ); // 读取整个 http 请求内容放入 buf let mut buf = [0; 1024]; tcp_stream.read(&mut buf)?; let null_index = buf.iter().position(|&c| c == b''\0'').unwrap_or(buf.len()); let raw_string: String = String::from_utf8(buf[0..null_index].to_vec())?; // 把整个用户请求体按照 http 协议约定解析成 HttpRequest 对象 let request = HttpRequest::try_from(raw_string)?; //Http Server 对外给用户自定义的路由实现层 let response = handle_http(&request)?; // 把 HttpResponse 对象按照 http 协议约定包装成返回信息返回 HttpResponse::response(response, tcp_stream)?; Ok(()) }

可以看到,上面最核心的是 handle_stream_connect 方法中调用了 handle_http 回调函数,入参是 HttpRequest,返回是 HttpResponse,一入一出后通过 HttpResponse::response 发了出去请求。

至此服务雏形已经有了,我们对外暴漏下 API,如下(lib.rs):

rust
代码解读
复制代码
pub mod server; pub use server::protocol::{HttpError, HttpRequest, HttpResponse}; pub fn start(address: &str, port: u16, handle_http: server::HandleHttp) { server::start_http_server(address, port, handle_http); }

业务方就可以 Happy 的使用我们的 HttpServer 了。

3.3 业务使用 HttpServer 实现自己的路由业务(main.rs)

这里就像我们引入一个其他 HttpServer 一样,start 服务后实现自己的业务分发即可,如下样例:

rust
代码解读
复制代码
fn main() { //启动服务,传入回调函数进行业务处理 http_server::start("127.0.0.1", 4221, router_to_handle_request); } // 模拟基于 http server 的业务路由处理实现 fn router_to_handle_request(request: &HttpRequest) -> Result<HttpResponse, HttpError> { //如果请求是 http://127.0.0.1:4221/files/xxxx 则进入此路由 if request.path.starts_with("/files/") { let file_name = request.path.strip_prefix("/files/").unwrap_or_default(); let static_res_dir = "./static_res_dir"; let file_path = Path::new(static_res_dir).join(file_name); match request.method { HttpMethod::GET => { //如果是 get 请求则返回服务器静态资源目录下 http://127.0.0.1:4221/files/xxxx 中 xxxx 名的文件内容 if !Path::new(static_res_dir).exists() { return Ok(HttpResponse::new(404, "dir not found", ContentType::Json)); } if !file_path.exists() { return Ok(HttpResponse::new(404, "not found", ContentType::Json)); } else { let file = std::fs::File::open(file_path); match file { Ok(mut file) => { let mut content = String::new(); file.read_to_string(&mut content)?; return Ok(HttpResponse::new(200, content.as_str(), ContentType::OctetStream)); } Err(_) => { return Ok(HttpResponse::new(404, "not found", ContentType::Json)); } } } } HttpMethod::POST => { //如果是 POST 请求则向服务器静态资源目录下创建写入 http://127.0.0.1:4221/files/xxxx 中 xxxx 文件 let mut f = std::fs::File::create(file_path)?; f.write_all(request.body.as_bytes())?; return Ok(HttpResponse::new(201, "created", ContentType::Plain)); } } } else if request.path.starts_with("/echo/") { //如果请求是 http://127.0.0.1:4221/echo/aaaa 则返回 aaaa let end_str = request.path.strip_prefix("/echo/").unwrap_or_default(); return Ok(HttpResponse::new(200, end_str, ContentType::Json)); } else if request.path == "/" { return Ok(HttpResponse::new(200, "加微信:bitdev 进行交流.", ContentType::Plain)); } else if request.path == "/user-agent" { let default = &"unknown".to_string(); let ua: &str = request.headers.get("User-Agent").unwrap_or(default); return Ok(HttpResponse::new(200, ua, ContentType::Json)); } return Ok(HttpResponse::new(404, "unsupport", ContentType::Json)); }

到此我们一个简单实现 Http 1.1 协议的服务和业务实现都完成了。

4 验证

如下是我们的验证效果(上面终端模拟用户请求,下面终端显示服务端请求日志): New Image

加微信:bitdev 进行交流~