源码地图
主线位置:不在主线上。本页说明两条主线经过的代码在哪个 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 | 版本 | 管什么 | 本卷 |
|---|---|---|---|
hyper | 1.12.0 | HTTP/1 和 HTTP/2 的连接:服务端 server::conn、客户端 client::conn,以及 rt、service、body、upgrade 这些核心 trait 和类型 | 全卷 |
hyper-util | 0.1.21 | 建在 hyper 上的常用件:接 tokio 的 TokioIo / TokioExecutor / TokioTimer,一个端口同时服务两个协议的 auto,优雅停机,连接池客户端 legacy::Client,tower 转换 TowerToHyperService | 02、06、07 |
http | 1.5.0 | Request、Response、HeaderMap、Uri、StatusCode 等纯数据类型,没有 IO | hyper 在 lib.rs 第 121 行直接重新导出 |
http-body | 1.1.0 | Body trait、Frame、SizeHint | 02、04 |
http-body-util | 0.1.5 | Full、Empty、BodyExt::collect 等现成的 body 和组合子;hyper 自己只在 ffi 里用它,普通程序要自己加依赖 | 04 |
h2 | 0.4.20 | HTTP/2 的帧、流、流控;开 http2 才会拉进来 | 05(只讲 hyper 怎么用它) |
httparse | 1.10.1 | HTTP/1 请求头、响应头的零拷贝解析;开 http1 才会拉进来 | 03 |
want | 0.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):
| feature | hyper 的直接依赖 |
|---|---|
| 不开 | 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 步。
问: 不是说 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 | 作用 |
|---|---|---|
tracing | hyper_unstable_tracing | hyper 内部的 debug!、trace! 等日志真正发给 tracing |
ffi | hyper_unstable_ffi | C 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。
full 里还有 runtime、tcp、streamhyper 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 021 | HTTP/1:Dispatcher、Conn 状态机、role(解析与编码)、decode / encode(body 编解码)、io(缓冲读写)。其中 3 480 行是测试,role.rs 一个文件 3 619 行,第 1 697 行之后全是测试 | 03 |
proto/h2/ | 2 484 | HTTP/2:把 h2 的流接到 Service 和 body 上,keep-alive ping、自适应窗口 | 05 |
client/ | 2 304 | conn::http1、conn::http2 的 Builder / SendRequest / Connection,dispatch.rs 里的请求通道 | 06 |
server/ | 946 | conn::http1、conn::http2 的 Builder / Connection | 03、05 |
body/ | 1 064 | Incoming 和它背后的通道、长度 | 04 |
rt/ | 883 | Read / Write、ReadBuf、Executor、Timer / Sleep、bounds 里的 sealed trait | 02 |
service/ | 289 | hyper 自己的 Service、HttpService、service_fn | 02 |
common/ | 876 | 内部小工具:Date 头缓存、Rewind、Time 等 | 散见 |
error.rs | 740 | hyper::Error | 08 |
upgrade.rs | 421 | Upgraded、OnUpgrade:协议升级后交还 IO | 03 |
ext/ | 575 | 放进 http::Extensions 的扩展类型:HTTP/1 的 ReasonPhrase、HTTP/2 扩展 CONNECT 的 Protocol、1xx 响应回调 on_informational | 少量 |
ffi/ | 2 293 | C 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、TokioTimer | tokio(打开 tokio 的 rt、time) | 727 | 02 |
server::conn::auto | server-auto(= server + http1 + http2) | 1 452 | 07 |
server::graceful | server-graceful | 490 | 07 |
service::TowerToHyperService | service | 132 | 02 |
client::legacy:Client、pool、connect::HttpConnector | client-legacy | 7 121(其中 SOCKS 与隧道代理 1 807) | 06 |
rt::tracing:CurrentSpanExecutor 等三个包装执行器 | tracing | 546 | 02 的 rt::tracing |
client::legacy::connect::proxy:Tunnel、SocksV4、SocksV5 | client-legacy(不需要 client-proxy) | 1 807(含在上一行) | 06 的 代理连接器 |
client::pool:singleton、cache、negotiate、map | client-pool | 2 572 | 06 的 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、计时器都走rttrait。 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。