Rust in Everything
基于 reqwest 0.13.5

源码地图

主线位置:三条主线的地图。请求主线走 async_impl/request.rs、async_impl/client.rs,经过 cookie.rs、retry.rs、redirect.rs 定义的层;连接主线走 connect.rs、dns/、tls.rs、proxy.rs;响应主线走 async_impl/response.rs、async_impl/body.rs。本页先看 lib.rs 怎么把三套实现接到同一组类型名上,再列出 src 下每个文件管什么,最后用 cargo tree 看 reqwest 依赖谁、每个下层 crate 在哪一层干活。

flowchart TB
  app[应用] --> api["Client Request Response"]
  api --> async["async_impl"]
  api --> blk["blocking"]
  api --> wasm["wasm fetch"]
  blk --> async
  async --> layers["tower 中间件"]
  layers --> hu["hyper-util Client"]
  hu --> conn["connect.rs"]
  conn --> dns["dns"]
  conn --> tls["rustls 或 native-tls"]
  hu --> hyper["hyper 与 h2"]

三套实现,一组名字

reqwest::Client 在不同目标上是不同的类型。lib.rs 定义了两个宏,按目标平台给一组条目加上 cfg:

macro_rules! if_wasm {
    ($($item:item)*) => {$(
        #[cfg(all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none")))]
        $item
    )*}
}

macro_rules! if_hyper {
    ($($item:item)*) => {$(
        #[cfg(not(all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none"))))]
        $item
    )*}
}

(if_wasm、if_hyper)然后各自导出同名的类型:

if_hyper! {
    // ...
    pub use self::async_impl::{
        Body, Client, ClientBuilder, Request, RequestBuilder, Response, Upgraded,
    };
    pub use self::proxy::{Proxy,NoProxy};
    // ...
    mod async_impl;
    #[cfg(feature = "blocking")]
    pub mod blocking;
    mod connect;
    // ...
}

if_wasm! {
    mod wasm;
    mod util;

    pub use self::wasm::{Body, Client, ClientBuilder, Request, RequestBuilder, Response};
    // ...
}

(第 353 行、第 390 行)所以「三套实现共用类型」的准确说法是:共用名字和方法签名,不共用实现。

  • 非 WASM 目标上,reqwest::Client 就是 async_impl::Client。blocking 不是第三套独立实现:blocking::ClientBuilder 里装的是一个 async_impl::ClientBuilder,blocking::Client 里是一个 ClientHandle,它用 thread::Builder 起一个后台线程跑异步客户端,请求经 mpsc 通道送过去,见 阻塞客户端。
  • WASM 目标上,wasm::Client 只是一个 Arc<Config>,没有连接池、连接器、tower 层,connect、proxy、tls、dns、redirect、retry、blocking、cookie 这些模块根本不编译,见 WASM。
  • 两边都编译的只有 error、into_url、response(ResponseBuilderExt)和 util,外加 http crate 转导出的 Method、StatusCode、Version、header 和 url::Url。lib.rs 里有一句注释「TODO: remove if_hyper if wasm has been migrated to new config system」,说的是 config.rs(按请求覆盖超时等设置的 RequestConfig)目前只有非 WASM 用。

lib.rs 还有一个只为编译期检查存在的函数 _assert_impls:断言 Client、Error、Body 是 Send + Sync(Client 还要 Clone),Request、RequestBuilder 是 Send,非 wasm32 上 Response 也是 Send。

src 下的文件

按 tag 统计 src 下 .rs 的行数(含文档注释和文件内的单元测试),合计 20323 行:

文件或目录行数管什么讲它的页
async_impl/client.rs3196Client、ClientBuilder 和它的 Config(六十多个字段)、build 组装连接器和 tower 栈、execute_request、Pending一个 Client、执行与 Pending
async_impl/request.rs1151Request、RequestBuilder:头、查询、表单、JSON、认证、try_clone请求构建
async_impl/multipart.rs754multipart::Form、Part:边界、长度请求体与 multipart
async_impl/response.rs508Response:bytes、text(字符集解码)、json、chunk、error_for_status响应体
async_impl/body.rs486请求和响应共用的 Body;总超时、读超时两个响应体包装请求体与 multipart、超时
async_impl/h3_client/840HTTP/3(实验):自己的连接器和连接池连接池与 HTTP 版本
async_impl/upgrade.rs、mod.rs75 + 14Response::upgrade 和 Upgraded—
connect.rs2139Connector、ConnectorBuilder、ConnectorService:本地套接字、代理隧道、直连三条路;TLS 分支(Inner);Conn 和 TlsInfo;SOCKS;connection_verbose 的读写日志连接器、代理
proxy.rs948Proxy、NoProxy,内部的 Matcher(包 hyper-util 的 Matcher 或自定义闭包)代理
tls.rs922Certificate、Identity、CertificateRevocationList、Version、TlsBackend、TlsInfo;NoVerifier、IgnoreHostname 两个危险校验器TLS 配置
dns/312Resolve trait、DynResolver、GaiResolver、HickoryDnsResolver、按域名覆盖的 DnsResolverWithOverridesDNS
error.rs543Error 和 Kind、is_timeout 等判断方法错误
retry.rs491retry::Builder、tower retry::Policy 的实现、默认只重试 HTTP/2 的 GOAWAY 和 REFUSED_STREAM重试
redirect.rs454redirect::Policy(默认最多 10 次)、给 tower-http 用的 TowerRedirectPolicy、跨域去敏感头重定向
cookie.rs292CookieStore trait、Jar、CookieService 中间件cookie
blocking/4217阻塞版的 Client、Request、Response、Body、multipart,wait.rs 在当前线程上等一个 future阻塞客户端
wasm/2186浏览器 fetch 的包装,同名的 Client、Request、Response、Body、multipartWASM
其余795lib.rs(397 行,大半是文档)、util.rs、into_url.rs、config.rs、response.rs本页

几点读法:

  • async_impl/ 一共 7024 行,其中 client.rs 就占 3196 行。它长不是因为逻辑多:ClientBuilder 有约九十个公开方法,每个都带文档注释,多数还带例子;build 一个函数从第 409 行到第 1110 行,大半是按 feature 展开的 TLS 配置和 HTTP/3 分支。
  • 没有 async_impl/decoder.rs。gzip 等解压在 0.12.25 改由 tower-http 的 Decompression 层做(#2840,删掉了 812 行),reqwest 只负责按 feature 打开对应的算法。
  • blocking/ 比 async_impl/ 少不了多少行,因为它把每个公开方法都重写了一遍(多是转发加文档),真正的逻辑在 ClientHandle 和 wait.rs 两处。

依赖层次:cargo tree 核实

只写 reqwest = "=0.13.5"(默认 feature)的程序,cargo tree -e normal -p reqwest --depth 1:

reqwest v0.13.5
├── base64 v0.23.1
├── bytes v1.12.1
├── encoding_rs v0.8.42
├── futures-core v0.3.34
├── h2 v0.4.20
├── http v1.5.0
├── http-body v1.1.0
├── http-body-util v0.1.5
├── hyper v1.12.0
├── hyper-rustls v0.27.10
├── hyper-util v0.1.21
├── log v0.4.34
├── mime v0.3.17
├── percent-encoding v2.3.2
├── pin-project-lite v0.2.17
├── rustls v0.23.46
├── rustls-pki-types v1.15.1
├── rustls-platform-verifier v0.7.1
├── sync_wrapper v1.0.2
├── tokio v1.54.1
├── tokio-rustls v0.26.6
├── tower v0.5.3
├── tower-http v0.6.11
├── tower-service v0.3.3
└── url v2.5.8

每个下层 crate 在哪一层:

crate在哪一层reqwest 怎么用它
hyper 1.12.0HTTP/1 和 HTTP/2 的连接协议不直接建连接;hyper::Request / Response、body::Incoming 是栈里流动的类型
hyper-util 0.1.21legacy Client、连接池、HttpConnector、代理规则 Matcherbuild 里 hyper_util::client::legacy::Client::builder 建出栈底的客户端;HttpConnector 负责 DNS 和 TCP;系统代理从它的 Matcher::from_system 读
tower 0.5.3Service / LayerRetry 层、connector_layer 的装箱和 connect_timeout 的 TimeoutLayer
tower-http 0.6.11通用的 HTTP 中间件FollowRedirect、Decompression 两层
h2 0.4.20HTTP/2 帧和流由 hyper 使用;reqwest 自己只在 retry.rs 里把错误 downcast 成 h2::Error,判断是不是可重试的 GOAWAY / REFUSED_STREAM
rustls 0.23.46、hyper-rustls 0.27.10、tokio-rustls 0.26.6TLSbuild 拼 rustls::ClientConfig;hyper-rustls 的 HttpsConnector 包住 HttpConnector 做握手
rustls-platform-verifier 0.7.1证书校验默认的 ServerCertVerifier,用系统信任库
url 2.5.8URL 解析IntoUrl、Response::url;它带来 idna 和一串 icu_* crate
encoding_rs、mimecharset featureResponse::text 的解码
log日志reqwest 自己用 log 打日志,hyper-util 用 tracing;本卷用 tracing-subscriber 把两者一起收上来

整棵树(去重,含 reqwest 自己和过程宏)是 98 个 crate,比直接用 hyper-util 加 hyper-rustls、rustls-platform-verifier 的同功能程序多出 43 个,多的是 url 带来的 IDNA 和 Unicode 数据、tower-http、system-configuration(macOS 系统代理)、encoding_rs 和 mime。逐项对比见 reqwest 解决什么问题。

仓库里的其他目录

  • tests/:按功能分文件(client.rs、redirect.rs、proxy.rs、timeouts.rs、retry.rs、cookie.rs、gzip.rs 等),tests/support/ 里是用 hyper 写的测试服务器;badssl.rs 连外网,本卷不跑。
  • examples/:simple.rs、blocking.rs、json_typed.rs、form.rs、h3_simple.rs、tor_socks.rs、connect_via_lower_priority_tokio_runtime.rs 和一个 wasm_github_fetch 目录。
  • CHANGELOG.md:每个版本的改动,本卷当资料用,0.13.0 的「Breaking changes」一节和代码核对过。

小结

  • if_hyper! / if_wasm! 按目标平台导出同名的 Client、Request、Response、Body:共用名字,不共用实现;blocking 建在 async_impl 上,WASM 是另一套。
  • src 合计 20323 行:async_impl/ 7024、blocking/ 4217、wasm/ 2186、connect.rs 2139,client.rs 一个文件 3196 行。
  • 没有自己的解压器、重定向循环和连接池:分别是 tower-http 的 Decompression、FollowRedirect 和 hyper-util 的池。
  • 直接依赖 25 个,整棵树 98 个 crate;hyper-util 在栈底,tower / tower-http 在中间,rustls 经 hyper-rustls 接进连接器。

💬 关联讨论

发起讨论
加载中...