reqwest 解决什么问题
主线位置:请求主线的起点之前。本页不跟某一个请求,而是回答「为什么要有这个库」:README 承诺了什么,这些承诺落在哪几段代码上;再把同一批请求用 reqwest、直接用 hyper-util legacy 客户端、ureq 各写一遍,比能测的四样东西:客户端代码量、依赖数、二进制大小、本机吞吐。
flowchart TB app["应用代码"] --> rq["reqwest Client"] rq --> dec["解压 重定向"] dec --> ck["cookie 重试"] ck --> hu["hyper-util Client"] hu --> conn["Connector"] conn --> px["代理 系统代理"] conn --> tls["rustls 平台校验"] hu --> hyper["hyper h1 h2"]
定位
README 的第一句是「An ergonomic, batteries-included HTTP Client for Rust」,下面列了七条。逐条对到 tag 上的代码:
| README 的条目 | 代码里在哪 |
|---|---|
Async and blocking Clients | async_impl::Client;blocking::Client 用一个后台线程包住它,见 源码地图 |
| Plain bodies, JSON, urlencoded, multipart | RequestBuilder::json、form、multipart;各要一个 feature |
| Customizable redirect policy | redirect::Policy,默认最多 10 次;循环本身是 tower-http 的 FollowRedirect |
| HTTP Proxies | Proxy,默认还读环境变量和系统设置 |
| HTTPS via rustls (or optionally, system-native TLS) | build 里的 TLS 分支和 connect.rs 的 Inner,见 TLS 配置 |
| Cookie Store | CookieStore 和 CookieService 层,要 cookies feature 加 cookie_store(true) |
| WASM | wasm/ 下另一套实现 |
「batteries-included」体现在默认值上:不调任何 builder 方法,一个 Client 就带着连接池(空闲 90 秒、每主机不限)、10 次重定向、系统代理、gzip 解压(开了 gzip feature 时)、Accept: */* 默认头、HTTP/2(ALPN 协商)、rustls 加系统信任库。它没有默认超时:timeout、connect_timeout、read_timeout 都是 None,见 超时。
reqwest 自己几乎不碰字节:解析 HTTP 的是 hyper,管连接池的是 hyper-util,重定向和解压是 tower-http,重试是 tower 的 Retry,TLS 是 rustls。它做的是选默认值、把这些部件接起来、给出一套顺手的 API。下面的对照就是在量「接起来」这件事值多少代码、花多少字节和时间。
同一批请求,三种写法
任务:建一个客户端,信任本机测试服务器的 CA(HTTPS 时),对一个 URL 反复发 GET,检查状态码,读完响应体。三个程序共用同一个计时外壳,只有标出的「客户端」部分不同。
reqwest(reqwest = "=0.13.5",默认 feature):
type Client = reqwest::Client;
fn make_client(ca_pem: Option<&[u8]>) -> Result<Client, Box<dyn std::error::Error>> {
let mut b = reqwest::Client::builder().no_proxy();
#[cfg(feature = "tls")]
if let Some(pem) = ca_pem {
b = b.add_root_certificate(reqwest::Certificate::from_pem(pem)?);
}
#[cfg(not(feature = "tls"))]
let _ = ca_pem;
Ok(b.build()?)
}
async fn fetch(client: &Client, url: &str) -> Result<usize, Box<dyn std::error::Error + Send + Sync>> {
let body = client.get(url).send().await?.error_for_status()?.bytes().await?;
Ok(body.len())
}
直接用 hyper-util 0.1.21 的 legacy Client,为了和 reqwest 的默认值对齐,TLS 也用 aws-lc-rs 加 rustls-platform-verifier,经 hyper-rustls 0.27.10 接上,打开 HTTP/2:
use bytes::Bytes;
use http_body_util::{BodyExt, Empty};
use hyper_util::client::legacy::{connect::HttpConnector, Client as LegacyClient};
use hyper_util::rt::TokioExecutor;
#[cfg(feature = "tls")]
type Connector = hyper_rustls::HttpsConnector<HttpConnector>;
#[cfg(not(feature = "tls"))]
type Connector = HttpConnector;
type Client = LegacyClient<Connector, Empty<Bytes>>;
fn make_client(ca_pem: Option<&[u8]>) -> Result<Client, Box<dyn std::error::Error>> {
let mut http = HttpConnector::new();
http.set_nodelay(true);
#[cfg(feature = "tls")]
let connector = {
use rustls::pki_types::{pem::PemObject, CertificateDer};
use std::sync::Arc;
http.enforce_http(false);
let provider = Arc::new(rustls::crypto::aws_lc_rs::default_provider());
let extra: Vec<CertificateDer<'static>> = match ca_pem {
Some(pem) => CertificateDer::pem_slice_iter(pem).collect::<Result<_, _>>()?,
None => Vec::new(),
};
let verifier = rustls_platform_verifier::Verifier::new_with_extra_roots(extra, provider.clone())?;
let tls = rustls::ClientConfig::builder_with_provider(provider)
.with_safe_default_protocol_versions()?
.dangerous()
.with_custom_certificate_verifier(Arc::new(verifier))
.with_no_client_auth();
hyper_rustls::HttpsConnectorBuilder::new()
.with_tls_config(tls)
.https_or_http()
.enable_http1()
.enable_http2()
.wrap_connector(http)
};
#[cfg(not(feature = "tls"))]
let connector = {
let _ = ca_pem;
http
};
Ok(LegacyClient::builder(TokioExecutor::new()).build(connector))
}
async fn fetch(client: &Client, url: &str) -> Result<usize, Box<dyn std::error::Error + Send + Sync>> {
let req = hyper::Request::get(url)
.header(hyper::header::ACCEPT, "*/*")
.body(Empty::new())?;
let resp = client.request(req).await?;
if !resp.status().is_success() {
return Err(format!("status {}", resp.status()).into());
}
let body = resp.into_body().collect().await?.to_bytes();
Ok(body.len())
}
ureq 3.5.0(本机 cargo 缓存里有,--offline 能编)是阻塞客户端,不依赖 tokio,只说 HTTP/1.1,默认 TLS 是 rustls 加 ring、根证书用编进去的 webpki-roots:
type Client = ureq::Agent;
fn make_client(ca_pem: Option<&[u8]>) -> Result<Client, Box<dyn std::error::Error>> {
let mut b = ureq::Agent::config_builder().proxy(None);
#[cfg(feature = "tls")]
if let Some(pem) = ca_pem {
use ureq::tls::{Certificate, RootCerts, TlsConfig};
let roots = RootCerts::new_with_certs(&[Certificate::from_pem(pem)?]);
b = b.tls_config(TlsConfig::builder().root_certs(roots).build());
}
#[cfg(not(feature = "tls"))]
let _ = ca_pem;
Ok(b.build().new_agent())
}
fn fetch(client: &Client, url: &str) -> Result<usize, Box<dyn std::error::Error + Send + Sync>> {
let body = client.get(url).call()?.body_mut().read_to_vec()?;
Ok(body.len())
}
客户端部分去掉空行,reqwest 15 行、hyper-util 53 行、ureq 17 行(各含 3 到 5 行 cfg 切换)。ureq 不用调 error_for_status:它的 http_status_as_error 默认就是真,4xx / 5xx 直接成为 Err。
hyper-util 版多出来的 38 行全在 make_client 里:拼 rustls 配置、选校验器、把 TLS 接到连接器上。而且它只做到了「发出去、收回来」,reqwest 在同样 15 行里还免费带着这些 hyper-util 版没有的东西:
- 重定向(tower-http
FollowRedirect,最多 10 次,跨域去掉敏感头)和 gzip 解压(Decompression,自动补Accept-Encoding); - 环境变量和系统代理、
NO_PROXY规则、CONNECT 隧道、SOCKS; - HTTP/2 的 GOAWAY / REFUSED_STREAM 自动重试;
Url解析(含国际化域名),带 URL 的Error,json/form/query、text的字符集解码;- 总超时、读超时、连接超时三个开关,
https_only,cookie。
依赖数与二进制大小
每个程序编两次:带 TLS(各自的默认 TLS 配置)和不带 TLS(reqwest 关掉默认 feature,hyper-util 只用 HttpConnector,ureq 关掉默认 feature)。依赖数是 cargo tree -e normal --prefix none 去重后的 crate 数(含过程宏,不含程序自己);大小是 release、strip = true 的可执行文件:
| 程序 | TLS 栈 | 依赖(TLS / 无 TLS) | 二进制字节(TLS / 无 TLS) |
|---|---|---|---|
| reqwest 0.13.5 | rustls + aws-lc-rs + platform verifier,HTTP/2 | 98 / 67 | 5372704 / 1859936 |
| hyper-util 0.1.21 + hyper-rustls | 同上 | 55 / 30 | 4533392 / 1281024 |
| ureq 3.5.0 | rustls + ring + webpki-roots,gzip | 27 / 10 | 2263904 / 880976 |
读法:
- 带 TLS 时 reqwest 比 hyper-util 版多 43 个 crate、多 0.84 MB。hyper-util 版的依赖是 reqwest 的真子集,多出来的有:
url带来的idna和一串icu_*、zerovec、yoke等 Unicode 数据 crate(约 25 个),tower、tower-layer、tower-http,system-configuration(读 macOS 系统代理),encoding_rs和mime(charset),以及 reqwest 自己。 - 关掉默认 feature 后差距仍有 37 个 crate、0.58 MB:
url、tower、tower-http 不是可选依赖,没有 TLS 的 reqwest 也带着它们。 - 二进制里的大头是 TLS:reqwest 从无 TLS 到有 TLS 多 3.5 MB,这一档 feature 带进来的是 aws-lc-rs、rustls、平台校验器、h2、系统代理和字符集支持。ureq 的 TLS 版不到一半大,它用 ring 而不是 aws-lc-rs,没有 HTTP/2,也没有 tokio;两个 provider 的大小差别见 rustls 卷的 两个 provider:二进制大小。
- ureq 不需要 tokio。对一个只发几个请求的命令行工具,这一项比上面所有数字都重要。
本机吞吐
服务端是独立进程里的测试服务器(hyper 1 服务端,强制 HTTP/1.1,响应体约 40 字节),客户端用上面三个带 TLS 的程序,每个先发一个请求把连接建好再计时。「并发 32」对 reqwest 和 hyper-util 是 32 个 tokio 任务共享一个客户端,对 ureq 是 32 个线程共享一个 Agent。每格 5 次,取最好的一次和中位数(requests/s,release,uptime 负载 12.6 到 17.3,机器 12 核):
| 场景 | reqwest | hyper-util | ureq |
|---|---|---|---|
| HTTP,单连接顺序 10000 次 | 26522 / 25163 | 30869 / 30524 | 28343 / 28176 |
| HTTP,并发 32,共 32000 次 | 69390 / 69163 | 72038 / 69821 | 68014 / 63608 |
| HTTPS,单连接顺序 10000 次 | 26517 / 25792 | 28212 / 27813 | 5414 / 5287 |
| HTTPS,并发 32,共 32000 次 | 62863 / 61449 | 62460 / 58003 | 7100 / 6480 |
读法:
- 单连接顺序请求最能看出客户端自身的开销。 HTTP 下 reqwest 每个请求约 37.7 µs,hyper-util 约 32.4 µs,差约 5 µs;HTTPS 下差约 2 µs。这几微秒花在四层 tower、
execute_request里的头合并和一次HeaderMap克隆、Pending和响应体包装、Url和Uri的转换上。tag 之后的8e1c806(#3116,「avoid unnecessary HeaderMap clones when dispatching requests」)就是在削这一块,还没有发版。 - 并发 32 时三者差不多,都在每秒六七万左右:这时瓶颈是同一台机器上的服务端和调度,不是客户端。
- ureq 走 HTTPS 时慢了约五倍,并发也上不去。服务端日志确认它复用了连接(200 次请求只握手 1 次),所以不是在反复握手。原因本卷没有追,它和 reqwest 无关;这里只说明「阻塞客户端一定更轻」不能直接推出「一定更快」,要量。
- 这些数字都是回环上的。真实网络上一个往返是毫秒级,客户端的几微秒淹没在里面;能拉开差距的是连接复用和 TLS 握手次数,见 一个 Client。
问: 既然 hyper-util 版快几微秒、少 43 个依赖,为什么不直接用 hyper-util?
答: 要看那 38 行之外还要补多少。上一节列的重定向、解压、代理、URL 解析、超时,每一样自己写都比 38 行长,而且容易写错:跨域重定向要去掉 Authorization,系统代理要认 NO_PROXY,解压要和 Accept-Encoding 对上。只调一个固定的内部服务、不需要这些的程序,直接用 hyper-util 是合理的;本卷 09 动手 就拼一个这样的小客户端,看看补齐一部分要多少代码。依赖数对编译时间和供应链审计有意义,对运行时性能几乎没有。
小结
- reqwest 是把 hyper、hyper-util、tower、tower-http、rustls 接起来,再配上默认值和一套 API:连接池、10 次重定向、系统代理、HTTP/2、rustls 加系统信任库都是默认的;解压和 cookie 要开对应的 feature,超时也不是默认的。
- 同一批请求:客户端代码 reqwest 15 行、hyper-util 53 行、ureq 17 行;hyper-util 版只做到发和收。
- 带 TLS:依赖 98 / 55 / 27 个 crate,剥离后二进制 5.37 / 4.53 / 2.26 MB;reqwest 多出的是
url的 Unicode 数据、tower / tower-http、系统代理和字符集支持。 - 回环单连接:reqwest 每个请求比 hyper-util 多约 2 到 5 µs;并发时三者都被服务端限住;ureq 的 HTTPS 明显慢,原因未查。