Rust in Everything
基于 hyper 1.12.0

源码地图

主线位置:不在主线上。本页说明两条主线经过的代码在哪个 crate、哪个目录,以及要打开哪个 feature 才能编译到它。

一个 hyper 程序依赖的几个 crate(实线是 crate 之间的依赖,虚线是「通常由你自己加进来」):

flowchart TB
  app[你的程序] --> util[hyper-util]
  app --> hyper[hyper]
  app -.-> hbu[http-body-util]
  util --> hyper
  hyper --> http[http]
  hyper --> hb[http-body]
  hyper --> httparse["httparse:http1"]
  hyper --> h2["h2:http2"]
  hyper --> want["want:client"]
  hyper --> tokio["tokio:只开 sync"]
  util --> tokio2["tokio:rt、time、net"]

几个 crate 各管什么

crate版本管什么本卷
hyper1.12.0HTTP/1 和 HTTP/2 的连接:服务端 server::conn、客户端 client::conn,以及 rt、service、body、upgrade 这些核心 trait 和类型全卷
hyper-util0.1.21建在 hyper 上的常用件:接 tokio 的 TokioIo / TokioExecutor / TokioTimer,一个端口同时服务两个协议的 auto,优雅停机,连接池客户端 legacy::Client,tower 转换 TowerToHyperService02、06、07
http1.5.0Request、Response、HeaderMap、Uri、StatusCode 等纯数据类型,没有 IOhyper 在 lib.rs 第 121 行直接重新导出
http-body1.1.0Body trait、Frame、SizeHint02、04
http-body-util0.1.5Full、Empty、BodyExt::collect 等现成的 body 和组合子;hyper 自己只在 ffi 里用它,普通程序要自己加依赖04
h20.4.20HTTP/2 的帧、流、流控;开 http2 才会拉进来05(只讲 hyper 怎么用它)
httparse1.10.1HTTP/1 请求头、响应头的零拷贝解析;开 http1 才会拉进来03
want0.3.2一对 Giver / Taker,客户端用它表达「连接现在要不要下一个请求」;开 client 才会拉进来06

hyper 仓库和 hyper-util 仓库各一个 crate。tag v1.12.0 下 hyper 的 src/ 一共 22 758 行,tag v0.1.21 下 hyper-util 的 src/ 一共 14 607 行,都含单元测试和注释,只用来估计分量。h2、httparse 这些依赖在各自的仓库里,本卷提到它们时只写 crate 名和版本。

为什么拆成 hyper 和 hyper-util 两个 crate,hyper 的 docs/VISION.md 里有一节「Not quite stable, but utile (useful)」:hyper-util 放的是有用、但达不到 hyper 稳定性承诺的东西,「That crate does not have the same stability level as hyper」;目标是其中值得别的库公开依赖的部分最后「stabilize and get promoted into hyper」。hyper-util 自己的 crate 文档第一句也是「This crate is less-stable than hyper」。

hyper 的 feature:默认全关

Cargo.toml 第 71 行起的 [features] 开头就写明了:

[features]
# Nothing by default
default = []

# Easily turn it all on
full = [
    "client",
    "http1",
    "http2",
    "server",
]

# HTTP versions
http1 = ["dep:atomic-waker", "dep:futures-core", "dep:httparse", "dep:itoa"]
http2 = ["dep:atomic-waker", "dep:futures-channel", "dep:futures-core", "dep:h2"]

# Client/Server
client = ["dep:want", "dep:pin-project-lite", "dep:smallvec"]
server = ["dep:httpdate", "dep:pin-project-lite", "dep:smallvec"]

四个开关分两组:协议版本 http1 / http2,角色 client / server。两组各至少开一个,协议代码才会编译。cfg.rs 第 14 行的 cfg_proto! 就是这个条件:

macro_rules! cfg_proto {
    ($($item:item)*) => {
        cfg_feature! {
            #![all(
                any(feature = "http1", feature = "http2"),
                any(feature = "client", feature = "server"),
            )]
            $($item)*
        }
    }
}

lib.rs 第 145 行起,proto(协议实现)和 headers 包在 cfg_proto! 里,client 模块挂在 client 后面,server 模块挂在 server 后面。body、rt、service、upgrade、ext 和错误类型不挂任何 feature,总在。所以只开 http1 而不开 server,能编译,但 hyper::server 不存在,连接代码一行都没有。

只写 hyper = "=1.12.0" 时,依赖树是这样(cargo tree,M2 Max、rustc 1.99.0):

nofeat v0.1.0
└── hyper v1.12.0
    ├── bytes v1.12.1
    ├── http v1.5.0
    │   ├── bytes v1.12.1
    │   └── itoa v1.0.18
    ├── http-body v1.1.0
    │   ├── bytes v1.12.1
    │   └── http v1.5.0 (*)
    └── tokio v1.54.1
        └── pin-project-lite v0.2.17

按 feature 组合看 hyper 的直接依赖(cargo tree -e normal --depth 2):

featurehyper 的直接依赖
不开bytes、http、http-body、tokio
http1 + server上面 4 个,加 atomic-waker、futures-core、httparse、httpdate、itoa、pin-project-lite、smallvec
http1 + client同上,但没有 httpdate,多了 want
full以上全部,加 futures-channel、h2

httpdate 只给服务端用:服务端默认会给响应加 Date 头。want 只给客户端用:见怎么读这一卷里客户端主线的第 3 步。

📝NOTE
没有蠢问题

问: 不是说 hyper 不绑运行时吗?什么 feature 都不开,依赖树里也有 tokio。

答: 有,但只开了 tokio 的 sync(Cargo.toml 第 26 行:tokio = { version = "1", features = ["sync"] })。hyper 用的是里面的通道:客户端把请求送进连接任务用 mpsc 和 oneshot(client/dispatch.rs 第 11 行),升级用 oneshot(upgrade.rs 第 53 行)。tokio 1.54.1 的 sync 模块文档写着这些原语「are runtime agnostic」,可以在非 tokio 的运行时里用。cargo tree -e features -i tokio 能看到,不开别的 feature 时 tokio 只有 sync 一项;开了 http2,h2 还会再打开 tokio 的 io-util。

hyper 不依赖的是 tokio 的运行时、反应器和计时器:IO、spawn、sleep 都走 hyper 自己的 rt trait,由 hyper-util 的 TokioIo、TokioExecutor、TokioTimer 接到 tokio 上,见 02 运行时抽象。

照着文档写 hyper::server::conn::http1::Builder::new(),却忘了开 feature,rustc 会直接指出缺哪个(只依赖 hyper = "=1.12.0" 时的真实输出,路径已缩短):

error[E0433]: cannot find `server` in `hyper`
   --> src/main.rs:3:25
    |
  3 |   let _builder = hyper::server::conn::http1::Builder::new();
    |                         ^^^^^^ could not find `server` in `hyper`
    |
note: found an item that was configured out
   --> .../hyper-1.12.0/src/lib.rs:159:13
    |
157 |     #![feature = "server"]
    |        ------------------ the item is gated behind the `server` feature
158 |
159 |     pub mod server;
    |             ^^^^^^

只开 server 还不够:hyper::server::conn 下的 http1 模块还挂在 http1 后面(server/conn/mod.rs 第 17 行),要两个都开。

不稳定的 feature:tracing 和 ffi

lib.rs 第 76 行起列了两个不稳定的 feature,光在 Cargo.toml 里开不够,还要在编译时传一个 --cfg:

feature还要的 cfg作用
tracinghyper_unstable_tracinghyper 内部的 debug!、trace! 等日志真正发给 tracing
ffihyper_unstable_ffiC API,另外要求同时开 client 和 http1

只开 tracing、不传 cfg 时,trace.rs 第 6 行的 compile_error! 会让编译失败:

error: The `tracing` feature is unstable, and requires the `RUSTFLAGS='--cfg hyper_unstable_tracing'` environment variable to be set.
  --> .../hyper-1.12.0/src/trace.rs:6:1

ffi 有两条同样的 compile_error!(ffi/mod.rs 第 36 行起):没开 client + http1 报一条,没传 hyper_unstable_ffi 报另一条。

不开 tracing 时,源码里到处都是的 debug!、trace! 是 hyper 自己在 trace.rs 里定义的宏,展开后是一个 #[cfg(feature = "tracing")] 块,什么也不做,所以读源码时看到日志调用不用担心开销。这也意味着默认情况下 hyper 内部的 warn! 不会出现在任何地方,02 的 Executor 与 Timer 会讲到一个因此「静默不生效」的配置。

docs.rs 上的文档是开着 ffi、full、tracing 和两个 cfg 构建的(Cargo.toml 第 196 行),每个条目旁边会标出需要哪个 feature。

📝NOTE
冷知识:0.14 的 full 里还有 runtime、tcp、stream

hyper 0.14 的 full 是 client、http1、http2、server、stream、runtime 六项,runtime 又打开 tcp 和 tokio 的 rt、time。那时 hyper 自带基于 tokio 的 TcpListener 接受连接、TcpStream 发起连接,默认用 tokio 的定时器和 spawn。

1.0 的路线图 docs/ROADMAP-1.0.md 把它列在「Runtime woes」下:「The runtime cargo-feature isn't additive」,以及 runtime 打开的超时、keep-alive 等选项「implicitly relies on Tokio's timer context. This can be quite confusing.」。对应的方案是「Remove the tcp and runtime features. The automatic executor and timer parts are handled by providing implementations of Executor and Timer.」;stream 也要删,因为「The Stream trait is not stable, and we cannot depend on an unstable API.」

三个 feature 分三个 PR 删掉:#2896 删 stream(issue #2855),#2929 删 tcp(issue #2856:「since we are already removing client::connect and server::accept」),#2975 删 runtime(issue #2857:有了 Timer trait,「we no longer have need of the runtime feature」)。三个都最早出现在 v1.0.0-rc.1(2022-10-25),随 v1.0.0 正式发布。所以今天的 full 只剩四项,接 tokio 的部分全在 hyper-util 的 tokio feature 里。

src/ 的目录

下表的行数是 tag v1.12.0 里各目录 .rs 文件的总行数,含测试和注释:

路径行数管什么本卷
proto/h1/9 021HTTP/1:Dispatcher、Conn 状态机、role(解析与编码)、decode / encode(body 编解码)、io(缓冲读写)。其中 3 480 行是测试,role.rs 一个文件 3 619 行,第 1 697 行之后全是测试03
proto/h2/2 484HTTP/2:把 h2 的流接到 Service 和 body 上,keep-alive ping、自适应窗口05
client/2 304conn::http1、conn::http2 的 Builder / SendRequest / Connection,dispatch.rs 里的请求通道06
server/946conn::http1、conn::http2 的 Builder / Connection03、05
body/1 064Incoming 和它背后的通道、长度04
rt/883Read / Write、ReadBuf、Executor、Timer / Sleep、bounds 里的 sealed trait02
service/289hyper 自己的 Service、HttpService、service_fn02
common/876内部小工具:Date 头缓存、Rewind、Time 等散见
error.rs740hyper::Error08
upgrade.rs421Upgraded、OnUpgrade:协议升级后交还 IO03
ext/575放进 http::Extensions 的扩展类型:HTTP/1 的 ReasonPhrase、HTTP/2 扩展 CONNECT 的 Protocol、1xx 响应回调 on_informational少量
ffi/2 293C API,本卷不讲—

proto 是协议实现,全部 pub(crate),用户看不到;server 和 client 是对外的 builder,把配置交给 proto 里的 Conn 和 Dispatcher;rt 是运行时抽象,hyper 和外界(IO、spawn、计时器)打交道全走这里。hyper 的 server::conn 模块文档把边界说得很清楚:「Accepting a connection and binding it with a service are not handled at this level.」client::conn 对应的一句是「Connecting to a host, pooling connections, and the like are not handled at this level.」这两件事都交给了 hyper-util 或你自己。

hyper-util 的 feature 与模块

hyper-util 同样默认全关,full 打开所有标准 feature。本卷只讲下面这些模块:

模块feature行数本卷
rt::tokio:TokioIo、TokioExecutor、TokioTimertokio(打开 tokio 的 rt、time)72702
server::conn::autoserver-auto(= server + http1 + http2)1 45207
server::gracefulserver-graceful49007
service::TowerToHyperServiceservice13202
client::legacy:Client、pool、connect::HttpConnectorclient-legacy7 121(其中 SOCKS 与隧道代理 1 807)06
rt::tracing:CurrentSpanExecutor 等三个包装执行器tracing54602 的 rt::tracing
client::legacy::connect::proxy:Tunnel、SocksV4、SocksV5client-legacy(不需要 client-proxy)1 807(含在上一行)06 的 代理连接器
client::pool:singleton、cache、negotiate、mapclient-pool2 57206 的 client::pool

没进主线的只剩 client::proxy(代理规则匹配 Matcher,982 行,feature client-proxy / client-proxy-system):环境变量、NO_PROXY、macOS 系统设置的读取在 reqwest 卷的 代理 和本卷的 本站的出站客户端 里讲。

注意 hyper-util 的 http1 / http2 只是转发到 hyper/http1、hyper/http2(Cargo.toml 第 92 行),而 server-auto 自带这两个。

MSRV

hyper:rust-version = "1.63",edition 2021。docs/MSRV.md 的政策是至少支持 6 个月前的编译器,只在需要重要的新特性时才提高。

hyper-util:rust-version = "1.85",edition 2024。它比 hyper 新得多,用 hyper-util 的项目实际的 MSRV 由它决定。

小结

  • hyper 管连接,hyper-util 管连接之外的常用件(接 tokio、auto、优雅停机、连接池客户端),后者稳定性承诺更弱;http、http-body 是公共类型,http-body-util 要自己加。
  • hyper 默认不开任何 feature;http1 / http2 和 client / server 两组各开至少一个,proto 才会编译;full 是这四个。
  • 什么都不开时,hyper 也依赖 tokio,但只有 sync(通道);运行时、IO、计时器都走 rt trait。
  • tracing 和 ffi 除了 feature 还要 --cfg hyper_unstable_*,否则 compile_error!;不开 tracing 时内部日志宏展开为空。
  • src/proto/h1 是最大的一块,9k 行里约 3.5k 行是测试;server / client 只是 builder。
  • MSRV:hyper 1.63,hyper-util 1.85。

💬 关联讨论

发起讨论
加载中...