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

news2025/1/13 5:56:44

1. 背景

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

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

下面是 Http 1.1 协议报文的示意图(网络配图):
在这里插入图片描述
对应一次请求的报文典型如下:

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. 目标

源码位于 https://github.com/yanbober/study-http-server-rust.git。
我们要实现的是这个完整流程,如下:
在这里插入图片描述

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

| - 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 命令行工具进行调试。
# 场景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,便于后面进行类型转换枚举,如下:

//我们只实现 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 方法将报文协议规范化):

// 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 实现如下:

// 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 路由框架,具体通过回调函数实现。

//定义给业务实现的回调函数类型,以便业务能基于 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):

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 服务后实现自己的业务分发即可,如下样例:

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 验证

如下是我们的验证效果(上面终端模拟用户请求,下面终端显示服务端请求日志):
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1570019.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

buu刷题(2)

[护网杯 2018]easy_tornado web buuctf [护网杯 2018]easy_tornado1_[护网杯 2018]easy_tornado 1-CSDN博客 render是渲染HTML页面用到的函数 这应该是一个模板注入漏洞 访问/fllllllllllllag&#xff0c;自动跳到了这个页面&#xff0c;可以看到 url 上有个msgError, 尝试将…

Node.js------模块化

◆ 能够说出模块化的好处◆ 能够知道 CommonJS 规定了哪些内容◆ 能够说出 Node.js 中模块的三大分类各自是什么◆ 能够使用 npm 管理包◆ 能够了解什么是规范的包结构◆ 能够了解模块的加载机制 一.模块化的基本概念 1.模块化 模块化是指解决一个复杂问题时&#xff0c…

大商创多用户商城系统 多处SQL注入漏洞复现

0x01 产品简介 大商创多用户商城系统是一个功能强大、灵活多变的新零售电商系统服务商。该系统支持平台自营和商家入驻,实现多元化经营模式,能够全面整合供应商、生产商、经销商和消费者等产业链资源,提高产品多样性,加快资金流动速度,并有助于减少不必要的成本输出。 0…

交换机特性解析

​1. 端口数量和类型: RJ-45端口: 最常见的端口类型,用于连接网线。 铜缆类型: 超五类、六类、七类等,影响传输速率和距离。 PoE功能: 支持为连接的设备供电,如IP电话、无线AP等。 光纤端口: 用于连接光纤,支持更长的传输距离和更高的速率。 光纤类型: 单模、多模等,影响传…

【核弹级安全事件】XZ Utils库中发现秘密后门,影响主要Linux发行版,软件供应链安全大事件

Red Hat 发布了一份“紧急安全警报”&#xff0c;警告称两款流行的数据压缩库XZ Utils&#xff08;先前称为LZMA Utils&#xff09;的两个版本已被植入恶意代码后门&#xff0c;这些代码旨在允许未授权的远程访问。 此次软件供应链攻击被追踪为CVE-2024-3094&#xff0c;其CVS…

计算机服务器中了halo勒索病毒怎么办,halo勒索病毒解密流程步骤

随着网络技术的不断应用&#xff0c;企业的生产运营得到了快速发展&#xff0c;越来越多的企业开始利用服务器数据库存储企业的重要信息文件&#xff0c;数据库为企业的生产运营提供了极大便利&#xff0c;但网络技术的不断发展也为企业的数据安全带来严重威胁。近日&#xff0…

IP-guard WebServer 任意文件读取漏洞复现

0x01 产品简介 IP-guard是由溢信科技股份有限公司开发的一款终端安全管理软件,旨在帮助企业保护终端设备安全、数据安全、管理网络使用和简化IT系统管理。 0x02 漏洞概述 由于IP-guard WebServer /ipg/static/appr/lib/flexpaper/php/view.php接口处未对用户输入的数据进行严…

C语言------冒泡法排序

一.前情提要 1.介绍 冒泡法排序法&#xff1a; 1)冒泡排序&#xff08;Bubble Sort&#xff09;是一种简单的排序算法&#xff0c;它重复地遍历要排序的列表&#xff0c;一次比较相邻的两个元素&#xff0c;并且如果它们的顺序错误就将它们交换过来。重复这个过程直到没有需…

聚酰亚胺PI材料难于粘接,用什么胶水粘接?那么让我们先一步步的从认识它开始(十八): 聚酰亚胺PI泡沫有哪些应用领域

聚酰亚胺PI泡沫有哪些应用领域 聚酰亚胺&#xff08;PI&#xff09;泡沫由于其一系列优异的特性&#xff0c;在许多高性能应用领域中都有广泛的应用&#xff0c;包括但不限于&#xff1a; 航空航天领域&#xff1a;聚酰亚胺PI泡沫由于其出色的耐高温、隔热和阻燃性能&#xff0…

C语言 循环控制——计数控制的循环

目录 循环语句 for语句 小结 循环结构有什么用&#xff1f; 国外某男子攻打自己的女友&#xff0c;并导致女友受伤&#xff0c;法官除判处他监禁和提供金钱补偿外&#xff0c;还处罚他抄写5000遍道歉辞&#xff1a; “Boys do not hit girls.” 循环的控制方法 循环语句 f…

Java NIO是New IO还是Non-blocking IO

文章目录 前言NIO到底叫啥通过对比理解NIO传统IO网络编程NIO引入的新概念NIO网络编程两者区别NIO的事件驱动 总结 前言 很多小伙伴对Java NIO的一些概念和编程不是很理解&#xff0c;希望通过本文对Java NIO与传统IO的对比&#xff0c;可以帮助大家更好地理解和掌握Java NIO。…

k8s存储卷 PV与PVC 理论学习

介绍 存储的管理是一个与计算实例的管理完全不同的问题。PersistentVolume 子系统为用户和管理员提供了一组 API&#xff0c;将存储如何制备的细节从其如何被使用中抽象出来。为了实现这点&#xff0c;我们引入了两个新的 API 资源&#xff1a;PersistentVolume 和 Persistent…

intellij idea 使用git撤销(取消)commit

git撤销(取消) 未 push的 commit Git&#xff0c;选择分支后&#xff0c;右键 Undo Commit &#xff0c;会把这个 commit 撤销。 git撤销(取消) 已经 push 的 commit 备份分支内容&#xff1a; 选中分支&#xff0c; 新建 分支&#xff0c;避免后续因为操作不当&#xff0c;导…

网络原理 - HTTP / HTTPS(4)——构造http请求

目录 一、postman 的下载安装以及简单介绍 1、下载安装 2、postman的介绍 二、通过 Java socket 构造 HTTP 请求 构造http请求的方式有两种&#xff1a;&#xff08;1&#xff09;通过代码构造&#xff08;有一点难度&#xff09; &#xff08;2&#xff09;通过第三…

【C++】探索C++中的类与对象(上)

​​ &#x1f331;博客主页&#xff1a;青竹雾色间. &#x1f618;博客制作不易欢迎各位&#x1f44d;点赞⭐收藏➕关注 ✨人生如寄&#xff0c;多忧何为 ✨ C是一种强大的编程语言&#xff0c;其面向对象的特性使得代码结构更加清晰、易于维护和扩展。在C中&#xff0c;类与…

文件同步工具哪个好

背景 今天介绍一款文件实时同步工具PanguFlow,它能够实时地监控源端文件夹的变化&#xff0c;然后将这种变化实时同步到目标端&#xff0c;对于文件灾备冗余的场景可谓是再合适不过了&#xff0c;一些老铁可能有这样的需求&#xff0c;比如两台服务器需要做文件的双机热备&…

乐知付-如何制作html文件可双击跳转到指定页面?

标题: 乐知付-如何制作html文件可双击跳转到指定页面&#xff1f; 标签: [乐知付, 乐知付加密, 密码管理] 分类: [网站,html] 为了便于买家理解使用链接进行付费获取密码&#xff1b;现开发个小工具&#xff0c;将支付链接转为浏览器可识别的文件&#xff0c;双击打开即可跳转到…

游戏引擎中的粒子系统

一、粒子基础 粒子系统里有各种发射器&#xff08;emitter&#xff09;&#xff0c;发射器发射粒子&#xff08;particle&#xff09;。 粒子是拥有位置、速度、大小尺寸、颜色和生命周期的3D模型。 粒子的生命周期中&#xff0c;包含产生&#xff08;Spawn&#xff09;、与环…

以太网布局指南

2层板 顶层走信号线以及地平面底层走信号线以及地平面信号走线应至少沿一条边被接地或接地走线包围如果使用地走线&#xff0c;应接本层接地平面&#xff0c;与上层接地平面解耦。 4层板 当信号走线被重新引用到功率平面时&#xff0c;在地平面和功率平面之间需要去耦电容器(0…

9、逆序对的数量(含源码)

逆序对的数量 难度&#xff1a;简单 题目描述 给定一个长度为n的整数数列&#xff0c;请你计算数列中的逆序对的数量。 逆序对的定义如下&#xff1a;对于数列的第 i 个和第 j 个元素&#xff0c;如果满足 i < j 且 a[i] > a[j]&#xff0c;则其为一个逆序对&#xf…