首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >用 Rust 从零实现一个极简 HTTP 客户端命令行工具

用 Rust 从零实现一个极简 HTTP 客户端命令行工具

原创
作者头像
一只牛博
发布2026-01-20 13:33:38
发布2026-01-20 13:33:38
3840
举报
文章被收录于专栏:rustrust

本文将详细介绍如何使用 Rust 语言实现一个类似 HTTPie 的命令行 HTTP 客户端工具。这个工具命名为 h,追求极简的使用体验,让 API 测试只需几个字符。

前言

作为开发者,我们每天都在与 API 打交道。无论是调试后端接口、测试第三方服务,还是快速验证某个想法,一个趁手的 HTTP 客户端工具必不可少。

虽然 curl 功能强大,但它的语法对于日常使用来说过于繁琐。比如发送一个带 JSON body 的 POST 请求:

代码语言:bash
复制
curl -X POST -H "Content-Type: application/json" -d '{"name":"test"}' https://api.example.com/users

而使用我们今天要实现的 h 工具,同样的请求只需要:

代码语言:bash
复制
h p api.example.com/users name=test

是不是简洁多了?这就是我们的目标:用最少的输入完成最常见的操作。

项目设计

核心设计原则

在开始编码之前,我们先确定几个核心设计原则:

  1. 极简优先:命令名只有一个字母 h,方法可以用简写(g/p/u/d/pa
  2. 智能推断:根据参数格式自动识别用户意图,无需繁琐的 flag
  3. 美观输出:语法高亮让响应一目了然,不同状态码用不同颜色区分

架构设计

整个项目采用模块化架构,将功能清晰地分离到不同模块:

image-20260120132947114
image-20260120132947114

这种设计的好处是每个模块职责单一,便于测试和维护。

请求处理流程

用户输入到响应输出的完整处理流程如下:

image-20260120133137654
image-20260120133137654

项目初始化

首先创建项目并配置依赖。我们的 Cargo.toml 如下:

代码语言:toml
复制
[package]
name = "rust_http"
version = "0.1.0"
edition = "2021"

[lib]
name = "rust_http"
path = "src/lib.rs"

[[bin]]
name = "h"
path = "src/main.rs"

[dependencies]
reqwest = { version = "0.11", features = ["json"] }
tokio = { version = "1", features = ["full"] }
clap = { version = "4", features = ["derive"] }
colored = "2"
serde_json = "1"
serde = { version = "1", features = ["derive"] }
urlencoding = "2"

[dev-dependencies]
proptest = "1"

依赖说明:

  • reqwest:Rust 生态中最流行的 HTTP 客户端库
  • tokio:异步运行时,reqwest 需要它来执行异步请求
  • clap:命令行参数解析库,支持 derive 宏简化代码
  • colored:终端颜色输出库
  • serde_json:JSON 序列化/反序列化
  • proptest:属性测试库,用于编写更健壮的测试
依赖配置
依赖配置

核心模块实现

1. URL 智能解析模块 (url.rs)

URL 解析是第一个要实现的模块。我们希望用户可以用最简短的方式输入 URL:

  • :8080/apihttp://localhost:8080/api(本地开发最常用)
  • api.example.com/usershttps://api.example.com/users(自动补全 https)
  • http://...https://... → 原样使用
代码语言:rust
复制
pub struct UrlResolver;

impl UrlResolver {
    pub fn resolve(input: &str) -> Result<String, UrlError> {
        let input = input.trim();

        if input.is_empty() {
            return Err(UrlError {
                message: "URL cannot be empty".to_string(),
            });
        }

        // 已有协议,直接返回
        if input.starts_with("http://") || input.starts_with("https://") {
            return Ok(input.to_string());
        }

        // 以冒号开头后跟端口号,补全为 localhost
        if input.starts_with(':') {
            let rest = &input[1..];
            if rest.chars().next().map(|c| c.is_ascii_digit()).unwrap_or(false) {
                return Ok(format!("http://localhost:{}", rest));
            }
        }

        // 默认补全 https://
        Ok(format!("https://{}", input))
    }
}

这个模块虽然简单,但极大地提升了使用体验。开发时测试本地接口,只需要 h :8080/api 就够了。

测试 URL 解析
测试 URL 解析

2. 参数智能解析模块 (parser.rs)

这是整个工具的核心模块之一。我们通过参数的格式来自动识别用户意图:

模式

类型

示例

key=value

JSON 数据

name=test{"name":"test"}

key:value

请求头

Authorization:Bearer token

key==value

URL 参数

page==1?page=1

代码语言:rust
复制
pub enum ParamType {
    JsonData(String, String),    // key=value → JSON 字段
    Header(String, String),      // key:value → 请求头
    QueryParam(String, String),  // key==value → URL 参数
}

impl Parser {
    pub fn parse_param(input: &str) -> Result<ParamType, ParseError> {
        // 先检查 query param (key==value)
        if let Some(pos) = input.find("==") {
            let key = &input[..pos];
            let value = &input[pos + 2..];
            if !key.is_empty() {
                return Ok(ParamType::QueryParam(key.to_string(), value.to_string()));
            }
        }

        // 检查 JSON data (key=value),但不是 ==
        if let Some(pos) = input.find('=') {
            let next_char = input.chars().nth(pos + 1);
            if next_char != Some('=') {
                let key = &input[..pos];
                let value = &input[pos + 1..];
                if !key.is_empty() {
                    return Ok(ParamType::JsonData(key.to_string(), value.to_string()));
                }
            }
        }

        // 检查 header (key:value)
        if let Some(pos) = input.find(':') {
            let key = &input[..pos];
            let value = &input[pos + 1..];
            if !key.is_empty() {
                return Ok(ParamType::Header(key.to_string(), value.to_string()));
            }
        }

        Err(ParseError {
            message: format!(
                "Invalid parameter '{}', expected key=value, key:value, or key==value",
                input
            ),
        })
    }
}

注意这里的解析顺序很重要:必须先检查 ==(双等号),再检查 =(单等号),否则 page==1 会被错误地解析为 JSON 数据 {"page": "=1"}

实现 Parser
实现 Parser

方法简写也是一个贴心的设计:

代码语言:rust
复制
pub fn parse_method(input: &str) -> String {
    match input.to_lowercase().as_str() {
        "g" | "get" => "GET".to_string(),
        "p" | "post" => "POST".to_string(),
        "u" | "put" => "PUT".to_string(),
        "d" | "delete" => "DELETE".to_string(),
        "pa" | "patch" => "PATCH".to_string(),
        _ => input.to_uppercase(),
    }
}

这样 h p 就等于 h POSTh d 就等于 h DELETE,大大减少了输入量。

3. HTTP 客户端模块 (client.rs)

HTTP 请求的发送使用 reqwest 库,我们封装了一个简洁的接口:

代码语言:rust
复制
pub struct Response {
    pub status: u16,
    pub status_text: String,
    pub headers: Vec<(String, String)>,
    pub body: String,
    pub duration: Duration,
}

impl Client {
    pub async fn send(
        method: &str,
        url: &str,
        headers: &[(String, String)],
        body: Option<&str>,
    ) -> Result<Response, ClientError> {
        let client = reqwest::Client::builder()
            .timeout(Duration::from_secs(30))      // 30秒超时
            .redirect(Policy::limited(10))          // 最多跟随10次重定向
            .build()?;

        let reqwest_method = match method.to_uppercase().as_str() {
            "GET" => reqwest::Method::GET,
            "POST" => reqwest::Method::POST,
            "PUT" => reqwest::Method::PUT,
            "DELETE" => reqwest::Method::DELETE,
            "PATCH" => reqwest::Method::PATCH,
            _ => return Err(ClientError::other(&format!("Unsupported method: {}", method))),
        };

        let mut request_builder = client.request(reqwest_method, url);

        // 添加请求头
        for (key, value) in headers {
            request_builder = request_builder.header(key.as_str(), value.as_str());
        }

        // 添加请求体
        if let Some(body_content) = body {
            request_builder = request_builder.body(body_content.to_string());
        }

        let start_time = Instant::now();
        let response = request_builder.send().await?;
        let duration = start_time.elapsed();

        // 提取响应信息...
        Ok(Response { status, status_text, headers, body, duration })
    }
}

错误处理也很重要,我们为不同类型的错误提供了友好的提示:

代码语言:rust
复制
pub enum ClientErrorKind {
    ConnectionFailed(String),  // 连接失败
    Timeout(Duration),         // 请求超时
    InvalidUrl(String),        // 无效 URL
    Other(String),             // 其他错误
}

当连接失败时,用户会看到 Error: Failed to connect to api.example.com - connection refused,而不是一堆难以理解的堆栈信息。

4. 语法高亮模块 (highlighter.rs)

美观的输出是提升使用体验的关键。我们为 JSON、HTML、XML 实现了语法高亮:

代码语言:rust
复制
impl Highlighter {
    pub fn highlight_json(json: &str) -> String {
        if let Ok(value) = serde_json::from_str::<serde_json::Value>(json) {
            Self::highlight_json_value(&value, 0)
        } else {
            json.to_string()
        }
    }

    fn highlight_json_value(value: &serde_json::Value, indent: usize) -> String {
        match value {
            serde_json::Value::Null => "null".red().to_string(),
            serde_json::Value::Bool(b) => b.to_string().magenta().to_string(),
            serde_json::Value::Number(n) => n.to_string().yellow().to_string(),
            serde_json::Value::String(s) => format!("\"{}\"", s).green().to_string(),
            serde_json::Value::Array(arr) => {
                // 递归处理数组...
            }
            serde_json::Value::Object(obj) => {
                // 递归处理对象,键用 cyan 颜色...
            }
        }
    }
}

JSON 高亮的配色方案:

  • 键名:青色 (cyan)
  • 字符串:绿色 (green)
  • 数字:黄色 (yellow)
  • 布尔值:品红色 (magenta)
  • null:红色 (red)

对于 HTML 和 XML,我们也实现了基础的高亮:

代码语言:rust
复制
fn highlight_tag(tag: &str) -> String {
    // 标签名:蓝色
    // 属性名:青色
    // 属性值:绿色
    // 注释:灰色
}
语法高亮效果
语法高亮效果

5. 响应格式化模块 (formatter.rs)

格式化模块负责将所有信息以美观的方式呈现给用户:

代码语言:rust
复制
impl Formatter {
    pub fn print_response(
        request: &ParsedRequest,
        response: &Response,
        options: &FormatOptions,
    ) {
        // 静默模式:只输出响应体
        if options.quiet {
            Self::print_body(&response.body, Self::get_content_type(&response.headers));
            return;
        }

        // 详细模式:先显示请求信息
        if options.verbose {
            Self::print_request(request);
            println!();
        }

        // 状态行(带颜色)
        Self::print_status_line(response);

        // 耗时
        Self::print_duration(response);

        // 响应头
        Self::print_response_headers(&response.headers);

        // 响应体(带语法高亮)
        Self::print_body(&response.body, Self::get_content_type(&response.headers));
    }
}

状态码的颜色编码让用户一眼就能看出请求是否成功:

代码语言:rust
复制
pub fn status_color(status: u16) -> &'static str {
    match status {
        200..=299 => "green",   // 成功:绿色
        300..=399 => "yellow",  // 重定向:黄色
        400..=599 => "red",     // 错误:红色
        _ => "white",
    }
}

6. CLI 入口 (main.rs)

最后是命令行入口,使用 clap 库来解析参数:

代码语言:rust
复制
#[derive(ClapParser, Debug)]
#[command(name = "h")]
struct Cli {
    /// HTTP 方法或 URL
    #[arg(required = true)]
    method_or_url: String,

    /// URL(如果第一个参数是方法)或第一个参数
    #[arg()]
    url_or_param: Option<String>,

    /// 请求参数
    #[arg(trailing_var_arg = true)]
    params: Vec<String>,

    /// 详细模式
    #[arg(short, long)]
    verbose: bool,

    /// 静默模式
    #[arg(short, long)]
    quiet: bool,
}

这里有个巧妙的设计:第一个参数可以是方法也可以是 URL。如果是方法(如 gpPOST 等),则第二个参数是 URL;如果不是方法,则第一个参数就是 URL,默认使用 GET 方法。

属性测试

除了常规的单元测试,我们还使用 proptest 库进行属性测试。属性测试的思想是:定义系统应该满足的属性,然后让测试框架自动生成大量随机输入来验证这些属性。

例如,URL 解析的属性测试:

代码语言:rust
复制
proptest! {
    #![proptest_config(ProptestConfig::with_cases(100))]

    // 属性1:以 :port 开头的 URL 应该解析为 localhost
    #[test]
    fn prop_port_shortcut_resolves_to_localhost(port in 1u16..65535u16) {
        let input = format!(":{}", port);
        let result = UrlResolver::resolve(&input).unwrap();
        prop_assert!(result.starts_with("http://localhost:"));
        prop_assert!(result.contains(&port.to_string()));
    }

    // 属性2:已有 http:// 协议的 URL 应该原样返回
    #[test]
    fn prop_http_url_unchanged(host in "[a-z]{3,10}") {
        let input = format!("http://{}.com", host);
        let result = UrlResolver::resolve(&input).unwrap();
        prop_assert_eq!(result, input);
    }
}

参数解析的属性测试确保了解析逻辑的正确性:

代码语言:rust
复制
proptest! {
    // key=value 应该被解析为 JSON 数据
    #[test]
    fn prop_json_data_parsing(key in valid_key(), value in valid_value()) {
        let input = format!("{}={}", key, value);
        let result = Parser::parse_param(&input).unwrap();
        prop_assert_eq!(result, ParamType::JsonData(key, value));
    }

    // key:value 应该被解析为请求头
    #[test]
    fn prop_header_parsing(key in valid_key(), value in valid_value()) {
        let input = format!("{}:{}", key, value);
        let result = Parser::parse_param(&input).unwrap();
        prop_assert_eq!(result, ParamType::Header(key, value));
    }

    // key==value 应该被解析为查询参数
    #[test]
    fn prop_query_param_parsing(key in valid_key(), value in valid_value()) {
        let input = format!("{}=={}", key, value);
        let result = Parser::parse_param(&input).unwrap();
        prop_assert_eq!(result, ParamType::QueryParam(key, value));
    }
}
运行测试
运行测试

属性测试帮助我们发现了一些边界情况的 bug,比如空键名、特殊字符等。每个属性测试运行 100 次,大大提高了代码的可靠性。

使用示例

让我们看看这个工具的实际使用效果:

GET 请求

代码语言:bash
复制
# 最简形式
h :8080/api/users

# 带查询参数
h api.example.com/users page==1 limit==10

# 带自定义请求头
h api.example.com/users Authorization:"Bearer token123"
测试 GET 方法
测试 GET 方法

POST 请求

代码语言:bash
复制
# 发送 JSON 数据
h p api.example.com/users name=test email=test@example.com

# 等价于
curl -X POST -H "Content-Type: application/json" \
     -d '{"name":"test","email":"test@example.com"}' \
     https://api.example.com/users
测试 POST 方法
测试 POST 方法

其他方法

代码语言:bash
复制
# PUT 请求
h u api.example.com/users/1 name=updated

# DELETE 请求
h d api.example.com/users/1

# PATCH 请求
h pa api.example.com/users/1 status=active

详细模式和静默模式

代码语言:bash
复制
# 详细模式:显示请求详情
h -v api.example.com/users

# 静默模式:只输出响应体(适合管道处理)
h -q api.example.com/users | jq '.data'

总结

通过这个项目,我们实现了一个功能完整、使用简便的 HTTP 客户端命令行工具。主要特点包括:

  1. 极简语法:命令名只有一个字母,方法可以简写,URL 可以省略协议
  2. 智能推断:根据参数格式自动识别 JSON 数据、请求头、查询参数
  3. 美观输出:语法高亮、状态码着色、格式化显示
  4. 健壮可靠:完善的错误处理、属性测试保证正确性

Rust 语言的类型系统和模式匹配让代码既安全又优雅。reqwestclap 等优秀的库让我们可以专注于业务逻辑,而不用重复造轮子。

这个项目的代码量不大(约 1000 行),但涵盖了 Rust 开发的很多实践:模块化设计、错误处理、异步编程、命令行解析、属性测试等。希望这篇文章能对你有所帮助!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 项目设计
    • 核心设计原则
    • 架构设计
    • 请求处理流程
  • 项目初始化
  • 核心模块实现
    • 1. URL 智能解析模块 (url.rs)
    • 2. 参数智能解析模块 (parser.rs)
    • 3. HTTP 客户端模块 (client.rs)
    • 4. 语法高亮模块 (highlighter.rs)
    • 5. 响应格式化模块 (formatter.rs)
    • 6. CLI 入口 (main.rs)
  • 属性测试
  • 使用示例
    • GET 请求
    • POST 请求
    • 其他方法
    • 详细模式和静默模式
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档