Appearance
前言
一个被忽视的共识
先讲一个场景。
你刚接手一个 Rust 后端服务,打开项目跑起来——cargo run 之后一行熟悉的 listening on 0.0.0.0:8080 就出现了,浏览器访问 /health 返回 OK,你心里松了一口气,觉得 Rust 后端"真不难"。接下来是阅读代码:入口是一个 tokio::main,里面 new 了一个 axum::Router,挂上若干路由,把它交给了某个 serve 函数;再点进去,是一堆你从来没读过的泛型:Service<Request<Body>, Response = Response<Body>>、Layer<S>、MakeService、HttpService……它们层层嵌套,每一个都是十几个 where 子句。
你心里忽然有了个问题:我每天都在写的这个东西,它真正长什么样?
这时候你去看 Cargo.lock,发现 axum 依赖 hyper,hyper 依赖 http、http-body、h2、httparse,而 axum 的路由、中间件、错误处理全部建立在 tower::Service、tower::Layer 之上。tokio 在更底下,是所有东西的发动机。
你的直觉是对的——axum 并不是一个"新写的 Web 框架",它更像是 Tower 和 Hyper 这两套基础设施之上的一层薄胶水。真正把字节从网卡搬到你的 async fn handler 面前、再把你的返回值搬回网卡的,是 Hyper 和 Tower。它们一个管协议,一个管抽象。
这本书要写的就是这两位。
为什么不该跳过这两本书
Rust 后端生态有一个不同寻常的特点:中间层太成熟了。
你在 Node.js 里写一个 HTTP 服务,下面是 libuv 和 Node 的 C++ bindings,中间没什么你看得到的"中间层"——Express 或者 Fastify 直接就是业务框架。你在 Go 里写 net/http.ListenAndServe,标准库把事情一口气做完了,协议栈、多路复用、中间件模式都藏在一个黑盒里。而在 Rust 里,Tower 和 Hyper 是两层显式、开源、用户可替换的基础设施,它们甚至有自己独立的版本号、独立的 breaking change 周期、独立的设计哲学。
这意味着两件事。
第一件,你很难绕开它们。Rust 生态里任何一个能扛生产流量的 Web 框架——Axum、Actix(的 actix-web-lab 分支)、Poem、Salvo——都建立在 Tower 的 Service 抽象之上;任何一个 gRPC 栈(Tonic、grpc-rs)、任何一个反向代理(Pingora 用的不是 Hyper,但思路高度一致)都在和 Hyper 的 HTTP/2 层打交道。你的业务代码可能永远用不到 tower::Service::poll_ready,但你用到的每一个中间件都在调用它;你可能永远不会手写 hyper::server::conn::http1::Builder::new().serve_connection(io, service).await,但 axum 的 serve 函数、hyper-util 的 auto::Builder,它们在里面做的事情就是这一行。理解这两层,你的心智模型从"axum 是一个 Web 框架"升级到"axum 是把 Tower 和 Hyper 粘起来的一套风格选择"。你会开始理解:为什么同一个 tower::Limit 中间件能同时用在 HTTP 客户端和 HTTP 服务端;为什么 Tonic 的 gRPC 服务能和 Axum 的 REST 服务共享一套 Layer;为什么你在 reqwest 里的 retry 插件换个写法就能套到 Postgres 连接池上——因为它们共享同一个抽象:async fn(Request) -> Result<Response, Error>。
第二件,它们是 Rust 社区最好的工程教材之一。Hyper 从 2015 年的 0.1 版发展到今天的 1.9,这九年里它经历过整整一次大的重写(2019 年的 0.13 引入 async/await 之前是纯 Future combinator 风格)和一次 API 大拆分(2023 年的 1.0 把 tokio 绑定从核心拆出去、让 hyper 本身对 runtime 完全不可知),每一次改动都有公开的 RFC 和讨论线程。Tower 也一样——Service trait 的签名从 &mut self + poll_ready + call 到 hyper 1.0 版本重新定义的 &self + 无 poll_ready,中间有一篇长达百条评论的 issue(hyperium/hyper#3040)。这些讨论不是纸上谈兵,每一条都来自真实的生产事故或重构需求。读它们的源码,相当于旁听一场为期九年的工程评审会。
读完这本书,你不会再觉得"Rust 后端是一个难以下手的黑盒"。你会变成那个在团队里可以说"我来看一下 hyper 这边的行为"的人,那个能在 issue 里直接贴出源码行号反驳错误理解的人,那个能为自己的项目写出通用中间件的人。
这本书不是什么
为了避免你带着错误的期待读下去,先把"不是什么"说清楚。
这本书不是 Axum/Tonic 的使用手册。 Axum 的 #[derive(FromRequest)]、Tonic 的 tonic-build、reqwest 的 Client::builder()——这些框架层 API 如何使用,社区里有大量优秀资源(官方 guide、examples、博客),本书不会和它们重复。Axum/Tonic 在第 22 章出现,但那一章的主题是"Axum 如何把 Tower + Hyper 粘起来",不是"怎么用 Axum"。
这本书不是 HTTP 协议标准解读。 我们会读 RFC 9110(HTTP/1.1 核心语义)、RFC 9112(HTTP/1.1 wire format)、RFC 9113(HTTP/2)——但只引用到"为什么 hyper 这样实现"需要的部分。如果你想系统学习 HTTP 协议,建议直接读 RFC 原文或《HTTP/2 in Action》。本书的每一处协议细节都是为了解读 Hyper 的源码选择而存在。
这本书不是 Tokio 入门书。 我假设你至少知道什么是 Future、什么是 Poll::Pending、Waker 大概是干什么的、tokio::spawn 会把任务交给谁。如果这些名词还让你犹豫,请先读《Tokio 源码深度解析》——这本书会反复引用卷四的章节,尤其是第 2 章(Future trait 与 poll 模型)、第 3 章(Waker)、第 4 章(Runtime 架构)、第 8 章(I/O Driver 架构)。没有这些作为地基,第 12 章的 Connection Dispatcher 状态机会看得很累。
这本书不是速成教程。 每一章都会走一遍"问题背景 → 设计选项 → Hyper/Tower 的选择 → 源码验证 → 工程含义"这条主线。如果你只想十分钟知道 "Service trait 长什么样",翻 docs.rs 就行。本书要讲的是"为什么是这个签名"——&mut self 还是 &self?有没有 poll_ready?type Future 为什么是关联类型而不是 Pin<Box<dyn Future>>?这些问题每一个背后都是一场长达数年的工程权衡。
这本书是什么
这本书是 Hyper 和 Tower 的一份"读码日记"。 每一章都像是你和我对着屏幕,一起 cargo expand、一起 rg '^impl'、一起 git blame 某一个关键 commit。2026 年 4 月锁定的 hyper 1.9.0 和 tower 0.5.3 是我们共同的地图——每一处源码引用都会标注 文件:行号,你可以随时在自己的 clone 里对照验证。不是"我告诉你它怎么写",而是"我陪你一起把它读懂"。
这本书是一张 Rust HTTP 生态的地图。 hyper、tower、http、http-body、h2、httparse、hyper-util、tower-http——这些 crate 听起来像是堆得差不多的碎片,但它们之间有清晰的协议边界和依赖方向。第 1 章会给你一张完整的图:谁定义了什么 trait,谁消费了谁的 trait,一个 HTTP 请求从 TCP 字节到你的 handler 函数签名之间经过多少层转换。读完这张图,你以后再看任何一个新的"HTTP 相关 crate",都能很快判断它属于哪一层、填了哪个空白。
这本书是一份"协议工程"的案例研究。 HTTP/1 和 HTTP/2 在线路层完全不同——一个是纯文本请求-响应对,一个是二进制多路复用帧流——但 Hyper 用同一套 Connection<T, S> 抽象把它们都包住了。它是怎么做到的?答案在 proto::h1::Dispatcher 和 proto::h2::Server 两个模块里。我们会花三章(第 11-12 章)把 HTTP/1 的状态机拆开:parser 怎么和 I/O loop 交互、chunked 编码怎么在 Body::poll_frame 里被解码成 Frame、keep-alive 的 idle 超时怎么在 read_keep_alive 里触发;然后再花三章(第 15-17 章)把 HTTP/2 拆开:多路复用的流怎么被调度、HTTP/2 的 flow control 怎么桥接到 Body::poll_frame 的背压上、GOAWAY 如何实现优雅关闭。这些内容没法用一句话讲完——但每一章后你都会有一个新的、能落到键盘上的心智模型。
这本书是一份"抽象边界"的教材。 Tower 的 Service trait 只有两个方法(poll_ready 和 call),加起来不到 400 字节源代码。它被 Tonic、Axum、Warp、Linkerd 共享多年,成为一个事实标准。但就是这个 400 字节的 trait,在 hyper 1.0 版本里被有意地不采用——hyper 自己定义了一个 Service trait,签名只有 call(&self, req),不带 poll_ready,不带 &mut self。为什么?第 13 章会给出答案:当你需要一个连接上同时处理多个并发请求(HTTP/2 的多路复用),&mut self 的语义就变成了毒药。这个故事讲清楚,你会对"trait 边界的设计"有一个全新的直觉。
这本书是一本理论联系实际的工程手册。 每一章的最后一节叫"落到你键盘上"——会给出一个生产环境可能遇到的问题(比如"connection pool 的 idle timeout 和 server 的 keep-alive timeout 怎么配合"、"HTTP/2 的 SETTINGS_INITIAL_WINDOW_SIZE 设多大"、"gRPC 长连接要不要开 PING"),用本章讲到的源码给出答案,不是"一般建议……",而是"看 hyper 源码在 xxx.rs:nnn 行怎么写,所以答案是……"。
读者画像
如果你属于下面任何一类,这本书是为你写的:
Rust 后端工程师,天天用 Axum / Tonic,但从没"往下看过"。 你能写漂亮的 handler、熟练使用 extractors、会配置 tracing 中间件,但看到
S: Service<Request<B>, Response = Response<BoxBody>>这样的签名就自动跳过。本书会带你把这些泛型一个一个拆开,让你再看到它们的时候,脑子里是清晰的信号流图而不是黑盒。基础设施方向的 Rust 工程师。 你在维护公司内部的 RPC 框架、API 网关、或者一个多租户的代理。你需要知道 Hyper 的连接池怎么实现背压、HTTP/2 的流控窗口怎么调、WebSocket upgrade 在 Hyper 里经过了什么路径。这本书会把这些生产环境痛点的实现细节逐一剖开。
准备给 Rust 生态贡献代码的开发者。 你想给 Tower 写一个新中间件、给 Hyper 修一个 bug、或者 fork 出一个"更适合我们场景的"HTTP 栈。第 2-8 章会带你把 Tower 的所有内置中间件读一遍,你会学到"工业级中间件的代码组织法"——错误类型怎么设计、
poll_ready怎么传播、Clone边界怎么权衡。转岗或跨栈学习的工程师。 你在 Go 或 Java 的生态里写过很多年网络服务,想看看 Rust 社区在同一个问题上做出了什么不同的选择。本书在关键决策点会给出对比——
tower::Servicevs Go 的http.Handler、Hyper 的 flow control vs Gonet/http的Flusher、Rust 的Send + 'static约束 vs Go 的"天然 goroutine 友好"。对比之后你会发现:不是 Rust 刻意复杂,而是 Rust 把许多在其他语言里隐藏的"运行时代价"摆到了类型系统里让你显式选择。准备面试 Rust 后端岗位的候选人。 本书的每一章都可以独立成为一个"深度技术话题"——面试官问你 "Rust 里怎么做 HTTP 超时?",你不止能说
tower::timeout,还能说清楚它为什么不直接在call里tokio::time::timeout,而要在poll_ready里先取许可(关联第 5 章);问你"HTTP/2 的背压怎么实现?",你能说出Body::poll_frame和 h2StreamId的 flow control window 之间的桥接(关联第 16 章)。
前置知识
本书不会重复 Rust 基础。阅读前请确认你对以下概念心中有数:
必须掌握:
- trait 与泛型:
impl Trait for Type、关联类型(type Future)、where 子句、T: Service<Req, Response = R>这样的约束。Service / Layer 的设计完全建立在 trait 泛型之上。 - 生命周期基础:能看懂
fn foo<'a>(x: &'a str) -> &'a str、知道'staticbound 在异步代码中意味着什么。Tower 的BoxService和 Hyper 的BoxBody都和'static有关。 Future与Pin:知道Future是什么、Poll::Pending/Poll::Ready的语义、Pin<&mut Self>的存在原因(不需要能手写 unsafe 的Pin实现,但能看懂pin_project!的产物)。async/await语法:会写async fn、理解.await表达式展开后是一个状态机。第 13 章会深入讨论 hyperServicetrait 和 async trait 的关系,这里需要你至少有过"async fn 就是 returns-impl-Future"的直觉。- 错误处理:
Result、?运算符、Box<dyn Error + Send + Sync>。Tower 中间件错误传播全是这套。
最好掌握(不掌握也能读,但会比较吃力):
- Tokio 基础:
tokio::main、tokio::spawn、tokio::io::AsyncRead/AsyncWrite、tokio::sync::mpsc。第 11-12 章会直接读 Hyper 的 I/O loop,这些原语会频繁出现。 - Waker 与 Context:知道
poll函数为什么要传&mut Context、cx.waker().wake_by_ref()在干什么。第 4 章讨论poll_ready的挂起唤醒语义时会用到。 - 一点 HTTP 知识:知道 HTTP 请求分为 request line、headers、body;HTTP/2 把这三件事都装进 frames;
Content-Length和Transfer-Encoding: chunked互斥;Connection: keep-alive是 HTTP/1.1 的默认值。 - bytes crate:
bytes::Bytes是引用计数的不可变字节切片、BytesMut可变。hyper 内部和外部接口几乎都围绕Bytes做零拷贝传递。
完全不需要:
- 能手写
Pin/Unpin。我们只读源码,不要求你自己设计!Unpin的类型。 - C 或 C++ 的网络编程经验。Hyper 完全用 Rust 写,我们不会涉及 epoll 的原始 API(那部分在卷四《Tokio》里已经讲过)。
- 协议栈实现经验。Hyper 的 HTTP/1 parser 建立在
httparse之上,我们会把关键部分读清楚;HTTP/2 frame 解码建立在h2之上,我们会站在h2的公共接口讲故事,不深入h2的内部状态机(那是另一本书的体量)。
本书的阅读路径
本书共 25 章(含前言),按八条主线展开。章节之间的依赖关系如下:
三条典型的阅读路径:
顺序阅读(推荐):从前言到第 24 章一路读下来。每一章的论证都建立在之前章节的基础上,尤其是第 4 章(
poll_ready)和第 13 章(hyper Service)——这两章是整本书的"枢纽",如果没读它们就跳到后面,会产生"为什么这里要这么写"的疑惑。中间件作者路径:第 1 → 2 → 3 → 4 → 5 → 6 → 8 → 19 → 22。这条路径聚焦 Tower,读完你应该能独立写一个生产级的 Tower 中间件并把它嵌入 Axum/Tonic。
协议栈路径:第 1 → 9 → 10 → 11 → 12 → 13 → 14 → 15 → 16 → 17 → 18 → 20 → 21。这条路径聚焦 Hyper,读完你应该能理解 HTTP/1 和 HTTP/2 在 Hyper 里的完整实现、能诊断连接池耗尽和流控相关的生产问题。
每一章的结构保持一致:
- 一个真实场景:从一行业务代码或一个生产事故切入。
- 问题背景:为什么这个问题不是显然的。
- 候选方案对比:社区历史上讨论过的几种做法、每种的 trade-off。
- Hyper/Tower 的选择:直接对着源码读决定。
- 工程落地:怎么把这一章的理解用到你的日常工作里。
关于工具
本书在引用源码时会频繁用到以下命令。如果你还没在本地装好,现在是最好的时机:
bash
# 从源码生成 HTML 文档
cargo doc --open --all-features
# 展开宏(尤其是 pin_project! 和 tokio::select!)
cargo install cargo-expand
cargo expand --lib
# 查看某个函数的 MIR(了解 async fn 状态机)
cargo install cargo-show-asm # 查看 asm
# 或者
RUSTFLAGS="-Zmir-opt-level=0" cargo +nightly rustc -- --emit=mir
# ripgrep 用来快速定位符号
cargo install ripgrep # 通常系统包管理器也有Hyper 源码里大量使用 pin_project_lite 和 [tokio::select!],这些宏展开后的代码才是真正跑起来的代码。建议你在阅读第 11 章(Dispatcher)之前,先对一个小例子运行过 cargo expand——这会大大降低读代码的压力。
关于致谢与版本
Hyper 的历史要从 @seanmonstar 于 2014 年启动的第一个 commit 算起。Tower 的历史可以追溯到 @carllerche 2016 年提出的服务抽象雏形。这两个项目背后是十余年积累的工程智慧,是一代 Rust 开源贡献者的共同成果。本书只是一份读码日记——所有的设计功劳都属于他们。书中如有任何理解错误,责任在我。
本书所引用的源码,全部锁定于 2026 年 4 月 20 日的版本快照(hyper 1.9.0 / tower 0.5.3 / http 1.4.0 / http-body 1.0.1)。Hyper 和 Tower 都是仍在积极演进的项目,未来的版本可能会带来 API 变化——但本书强调的是设计决策和工程思路,这些内容即便在未来的版本里也大概率依然成立。
好了,序言结束。翻到第 1 章——为什么需要 Hyper 与 Tower——我们开始这场读码之旅。