本文将带你深入掌握 Rust 中最流行的 HTTP 客户端库 ------
reqwest,通过一个完整的天气信息查询工具案例,演示如何使用异步方式发送 GET 请求、处理 JSON 响应、管理错误以及构建可复用的客户端模块。适合已掌握 Rust 基础语法与异步编程概念的开发者进阶学习。
一、背景与目标
在现代软件开发中,与外部 API 交互是常见需求。Rust 凭借其内存安全和高性能特性,非常适合用于构建稳定可靠的网络客户端程序。而 reqwest 是 Rust 社区中最广泛使用的 HTTP 客户端库之一,支持同步(需启用 blocking 特性)和异步模式(基于 tokio 或 async-std),并提供了简洁易用的 API 来发送请求、处理响应。
本案例将以"调用开放天气 API 获取城市气温"为例,完整实现一个命令行天气查询工具,涵盖以下核心知识点:
- 添加
reqwest到项目依赖 - 发送异步 HTTP GET 请求
- 解析 JSON 响应数据
- 错误处理与类型转换
- 构建结构化程序模块
最终我们将得到一个可通过命令行输入城市名获取当前温度的小工具,展示 reqwest 的实际应用能力。
二、环境准备与依赖配置
首先创建一个新的 Cargo 项目:
bash
cargo new weather_client
cd weather_client
编辑 Cargo.toml 文件,添加必要的依赖项。这里我们使用异步运行时 tokio 和 reqwest,并通过 serde 系列库进行 JSON 反序列化。
toml
[dependencies]
reqwest = { version = "0.11", features = ["json"] }
tokio = { version = "1.0", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
关键字说明:
| 关键字/库 | 作用说明 |
|---|---|
reqwest |
提供高层级的 HTTP 客户端功能,支持异步请求、JSON 序列化等 |
features = ["json"] |
启用内置的 .json() 方法,便于解析响应体为 JSON |
tokio |
异步运行时,使 async fn main() 成为可能 |
features = ["full"] |
启用 Tokio 所有功能模块(包括 TCP、定时器、任务调度等) |
serde |
数据序列化框架,配合 #[derive(Deserialize)] 自动解析结构体 |
serde_json |
处理 JSON 格式编码与解码 |
⚠️ 注意:
reqwest默认使用hyper作为底层 HTTP 引擎,并依赖 TLS 后端(如rustls)。若你遇到编译问题,可尝试显式添加rustls支持:
tomlreqwest = { version = "0.11", features = ["json", "rustls-tls"] }
三、代码演示:构建天气查询客户端
下面我们分步骤实现完整的天气查询程序。
步骤 1:定义响应数据模型
我们需要根据 OpenWeatherMap API 文档 定义对应的结构体。假设我们只关心城市名称、主温标(temp)和天气描述。
rust
// src/main.rs
use serde::Deserialize;
/// 表示天气的主要信息部分
#[derive(Debug, Deserialize)]
struct Main {
temp: f64,
}
/// 表示单条天气状况(如晴天、多云)
#[derive(Debug, Deserialize)]
struct Weather {
description: String,
}
/// 完整的天气响应结构
#[derive(Debug, Deserialize)]
struct WeatherResponse {
name: String,
main: Main,
weather: Vec<Weather>,
}
这些结构体使用了 #[derive(Deserialize)] 宏,由 serde 自动生成反序列化逻辑,字段名需与 JSON 字段一致(默认小写蛇形命名)。
步骤 2:编写异步请求函数
接下来实现从 OpenWeatherMap 获取天气信息的核心函数。
rust
use reqwest;
use std::env;
/// 异步函数:根据城市名获取天气信息
async fn get_weather(city: &str) -> Result<WeatherResponse, Box<dyn std::error::Error>> {
// 替换为你自己的 API Key(注册 https://openweathermap.org/api 免费获取)
let api_key = env::var("OPENWEATHER_API_KEY")
.unwrap_or_else(|_| "your_api_key_here".to_string());
let url = format!(
"http://api.openweathermap.org/data/2.5/weather?q={}&units=metric&appid={}",
city, api_key
);
// 创建客户端并发送 GET 请求
let client = reqwest::Client::new();
let response = client.get(&url).send().await?;
// 检查状态码是否成功
if !response.status().is_success() {
return Err(format!("HTTP Error: {}", response.status()).into());
}
// 将响应体解析为 WeatherResponse 结构
let weather: WeatherResponse = response.json().await?;
Ok(weather)
}
关键点解析:
| 代码片段 | 说明 |
|---|---|
async fn |
声明这是一个异步函数,必须在 tokio 运行时中执行 |
.await |
等待异步操作完成(如网络请求、JSON 解析) |
? 操作符 |
自动传播错误,简化错误处理流程 |
client.get(...).send().await |
发起 HTTP 请求并等待响应头返回 |
response.json().await |
将响应体反序列化为指定结构体(需 reqwest 启用 json feature) |
env::var(...) |
从环境变量读取敏感信息(推荐做法) |
步骤 3:主函数调用与参数处理
现在我们在 main 函数中调用上述方法,并接收命令行参数作为城市名。
rust
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// 获取命令行参数
let args: Vec<String> = env::args().collect();
if args.len() != 2 {
eprintln!("用法: {} <城市名>", args[0]);
std::process::exit(1);
}
let city = &args[1];
println!("正在查询 {} 的天气...", city);
match get_weather(city).await {
Ok(weather) => {
let temp_c = weather.main.temp;
let desc = &weather.weather[0].description;
println!(
"🏙️ 城市: {}\n🌡️ 温度: {:.1}°C\n☁️ 天气: {}",
weather.name, temp_c, desc
);
}
Err(e) => {
eprintln!("❌ 查询失败: {}", e);
}
}
Ok(())
}
✅ 示例输出:
$ OPENWEATHER_API_KEY=abc123 cargo run -- Beijing 正在查询 Beijing 的天气... 🏙️ 城市: Beijing 🌡️ 温度: 23.5°C ☁️ 天气: clear sky
四、数据表格:API 响应字段映射
为了更清晰地理解 JSON 到结构体的映射关系,以下是 OpenWeatherMap 返回的部分 JSON 示例及其对应 Rust 结构体字段:
| JSON 路径 | 示例值 | 对应 Rust 字段 | 类型 |
|---|---|---|---|
name |
"Beijing" |
WeatherResponse.name |
String |
main.temp |
23.5 |
WeatherResponse.main.temp |
f64 |
weather[0].description |
"clear sky" |
WeatherResponse.weather[0].description |
String |
sys.country |
"CN" |
(未定义) | 忽略字段不影响反序列化 |
coord.lon |
116.397 |
(未定义) | serde 自动忽略未知字段 |
💡 提示:如果你希望捕获更多字段,只需在结构体中添加对应字段即可。
serde默认会忽略不在结构体中的 JSON 字段。
五、关键字高亮说明
以下是在本案例中出现的关键字及其含义:
| 关键字 | 高亮颜色建议 | 解释 |
|---|---|---|
async |
🔵 蓝色 | 标记异步函数或块,表示该函数不会阻塞线程 |
await |
🔵 蓝色 | 等待一个 Future 完成,只能在 async 上下文中使用 |
Result<T, E> |
🟡 黄色 | 标准错误处理枚举,代表操作可能成功或失败 |
? |
🟡 黄色 | 错误传播操作符,自动将 Err 向上传递 |
Box<dyn Error> |
🟡 黄色 | 动态错误类型,用于泛化所有实现了 Error trait 的错误 |
Deserialize |
🟢 绿色 | Serde 宏,自动生成从 JSON 到结构体的反序列化代码 |
format! |
🟢 绿色 | 字符串格式化宏,用于拼接 URL |
env::var() |
🟢 绿色 | 读取操作系统环境变量,避免硬编码密钥 |
reqwest::Client |
🔴 红色 | HTTP 客户端实例,可用于复用连接池 |
.json().await |
🔴 红色 | 将响应体反序列化为指定类型,内部调用 serde_json |
六、分阶段学习路径
为了帮助你系统掌握 reqwest 的使用,建议按以下五个阶段逐步深入:
阶段一:基础 GET 请求(✅ 已完成)
- 目标:发送简单 GET 请求并打印文本响应
- 技能点:
- 创建
Client - 使用
.text().await获取纯文本 - 处理基本错误
- 创建
rust
let text = reqwest::get("https://httpbin.org/ip")
.await?
.text()
.await?;
println!("{}", text);
阶段二:JSON 数据处理
- 目标:解析复杂 JSON 响应
- 技能点:
- 定义
#[derive(Deserialize)]结构体 - 使用
.json::<T>().await显式指定类型 - 处理嵌套对象与数组
- 定义
推荐练习:获取 GitHub 用户信息(
https://api.github.com/users/{username})
阶段三:POST 请求与表单提交
- 目标:向服务器发送数据
- 技能点:
- 使用
client.post(url).form(&data).send().await - 构造
HashMap或结构体作为表单数据 - 设置请求头(如
Content-Type)
- 使用
rust
let params = [("key1", "value1"), ("key2", "value2")];
let resp = client.post("https://httpbin.org/post")
.form(¶ms)
.send()
.await?;
阶段四:认证与 Headers 管理
- 目标:支持 Token 认证、自定义 Header
- 技能点:
- 使用
.bearer_auth(token)或.header("Authorization", ...) - 添加 User-Agent、Accept 等标准头
- 复用 Client 实例(连接池优化)
- 使用
rust
let client = reqwest::Client::builder()
.default_headers(headers)
.build()?;
阶段五:生产级健壮性增强
- 目标:构建可维护、高可用的客户端
- 技能点:
- 设置超时(
.timeout(Duration::from_secs(10))) - 重试机制(结合
tokio::time::sleep与循环) - 日志记录(集成
tracing或logcrate) - 错误分类(使用
thiserror自定义错误类型)
- 设置超时(
rust
#[derive(thiserror::Error, Debug)]
pub enum WeatherError {
#[error("网络请求失败: {0}")]
Reqwest(#[from] reqwest::Error),
#[error("无效的城市名称")]
InvalidCity,
}
七、常见问题与解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
编译报错 no function or associated item named 'main' found |
忘记添加 #[tokio::main] |
在 async fn main 上方加上 #[tokio::main] |
报错 error sending request for url (...), connection refused |
网络不通或 URL 错误 | 检查 API 地址是否正确,能否在浏览器访问 |
JSON 反序列化失败(invalid type: string \"...\", expected f64) |
API 返回非数字字符串(如 "N/A") |
使用 Option<f64> 或自定义反序列化函数 |
| API 密钥暴露在代码中 | 硬编码导致安全隐患 | 使用 env::var() 从 .env 文件加载(推荐搭配 dotenvy crate) |
| 并发请求性能差 | 每次新建 Client |
复用同一个 reqwest::Client 实例(它内部自带连接池) |
八、扩展建议
完成本案例后,你可以进一步拓展功能:
- 支持多个城市批量查询 :使用
tokio::join!或futures::future::join_all并发请求 - 缓存机制 :使用
cachedcrate 缓存近期查询结果 - CLI 参数增强 :使用
clapcrate 支持-u单位切换、--city参数命名等 - 输出格式化:支持 JSON 输出模式(方便脚本调用)
- 离线模拟测试 :使用
wiremock或httpmock模拟 API 响应进行单元测试
九、章节总结
在本案例中,我们完成了以下关键内容:
✅ 学会了如何使用 reqwest 发起异步 HTTP 请求
通过 Client::get().send().await 流程掌握了现代 Rust 网络编程的基本范式。
✅ 掌握了 JSON 响应的结构化解析方法
利用 serde 的 Deserialize 宏,将原始 JSON 映射为类型安全的 Rust 结构体。
✅ 实践了错误处理的最佳实践
使用 Result + ? 操作符简化错误传播,并理解了异步上下文中的错误类型限制。
✅ 提升了安全性意识
通过环境变量管理 API 密钥,避免敏感信息泄露。
✅ 构建了一个实用的小工具原型
具备良好的可扩展性,可作为后续 Web API 客户端项目的起点。
十、结语
reqwest 是 Rust 生态中不可或缺的 HTTP 客户端工具,其设计简洁、性能优异、安全性强。通过本案例的学习,你不仅掌握了它的基本用法,还建立了对异步网络编程的整体认知。随着 Rust 在服务端、CLI 工具和边缘计算领域的广泛应用,熟练使用 reqwest 将成为你开发能力的重要组成部分。
下一步,不妨尝试将其集成到更大的项目中,比如定时拉取股票行情、监控网站健康状态,或是构建微服务之间的通信桥梁。Rust 的强类型系统和零成本抽象,将让你的网络客户端既高效又可靠。