Rust in Everything
基于 hyper 1.12.0

怎么读这一卷

主线位置:两条主线的总览。本页把服务端主线和客户端主线各画一遍,标出每一步落在哪个函数上;下一部分的「一个请求的一生」用一段程序把两条线同时跑一遍。

flowchart TB
  start[从这里开始] --> q{读这一卷为了什么}
  q -->|会用| use1[01 全景]
  use1 --> use2[02 运行时抽象]
  use2 --> use3[07 hyper-util 服务端]
  use3 --> use4[10 常见坑]
  q -->|读懂实现| all[00 到 12 按顺序]

锁定版本

全卷只对着一个版本写:

项值
cratehyper 1.12.0,hyper-util 0.1.21
taghyper v1.12.0,hyper-util v0.1.21
commithyper 7f67699c(2026-10-06),hyper-util 23a8689(2026-09-24)
用到的依赖http 1.5.0、http-body 1.1.0、http-body-util 0.1.5、h2 0.4.20、httparse 1.10.1、want 0.3.2
实验用的 tokio1.54.1,和 tokio 卷相同

源码链接都用 tag,形如 https://github.com/hyperium/hyper/blob/v1.12.0/src/...#L123 和 https://github.com/hyperium/hyper-util/blob/v0.1.21/src/...#L123。不是 hyper 仓库里的依赖(h2、httparse、http-body-util 等)只写 crate 名和版本,不给行号。

想在本地对照着读:

git clone https://github.com/hyperium/hyper
git -C hyper checkout v1.12.0
git clone https://github.com/hyperium/hyper-util
git -C hyper-util checkout v0.1.21

两个仓库的 master 都已经走过了这两个 tag。写作时 hyper 的 master 比 tag 多 4 个提交,其中一个改了本卷重点讲的 Dispatcher(#4222,「drain in-flight requests before dispatcher shutdown」);hyper-util 多 11 个提交,有新的 TokioLocalExecutor(#330)、TokioHandleExecutor(#331),以及把 HttpConnector 放到 feature 后面(#341)。这些都不在本卷的版本里,读 master 会看到对不上的代码和行号。

本站自己锁的是 hyper 1.10.1、hyper-util 0.1.20(框架仓库的 Cargo.lock),比本卷早两个小版本,差异放在「在本站中」一部分讲。

两条主线

hyper 只做一件事:把一条字节流变成一串 Request / Response,或者反过来。字节流在 hyper 眼里只是实现了 hyper::rt::Read 和 Write 的东西,TCP、TLS、Unix socket 都一样。全卷沿这件事的两个方向展开,每页开头都会标出本页在哪条主线上。

服务端主线(字节 → 请求 → 响应 → 字节):

flowchart TB
  A["accept 得到 TcpStream"] --> B["TokioIo::new 包一层"]
  B --> C{"auto::Builder:看前 24 字节"}
  C -->|HTTP/1| D["http1::Builder::serve_connection"]
  C -->|HTTP/2 前言| H2["http2 连接,见 05"]
  D --> E["Connection future,即 Dispatcher"]
  E --> F["poll_read:读请求头,parse"]
  F --> G["recv_msg:service.call(req)"]
  G --> W["poll_write:等响应,encode"]
  W --> FL["poll_flush:写回 socket"]
  FL -->|keep-alive| F
  1. 你的代码 accept 一条连接,用 hyper-util 的 TokioIo 把 tokio 的 TcpStream 包成 hyper 认识的 IO。
  2. 用 hyper-util 的 auto::Builder 时,它先读最多 24 个字节和 HTTP/2 的连接前言比较,不一样就当 HTTP/1;只要 HTTP/1 的话直接用 hyper 的 http1::Builder::serve_connection。
  3. serve_connection 返回的 Connection 是一个 future,里面就是 Dispatcher。这条连接上的所有请求都在这一个 future 里处理。
  4. Dispatcher 的 poll_loop 每圈做三件事:poll_read 读出请求头(role::Server::parse,底下是 httparse),通过 Dispatch::recv_msg 调用你的 service.call(req);poll_write 等 service 返回的 future 完成,把响应头编码写进缓冲区,再按需写 body;poll_flush 把缓冲区写进 socket。
  5. 一个请求处理完,读写状态回到初始状态,同一条连接上读下一个请求(keep-alive)。

客户端主线(请求 → 字节 → 响应):

flowchart TB
  A["legacy::Client::request"] --> B{"连接池 checkout"}
  B -->|没有空闲连接| C["HttpConnector:DNS 再 TCP"]
  C --> D["http1::handshake"]
  D --> E["SendRequest 留给调用方"]
  D --> F["Connection 交给后台任务"]
  B -->|有空闲连接| E
  E --> G["send_request:请求放进通道"]
  G --> F
  F --> H["写请求,读响应头"]
  H --> I["回调把 Response 交回"]
  I --> J["body 读完,连接回池"]
  1. hyper-util 的 legacy::Client::request 先去连接池里找空闲连接,同时准备新建一条,两者赛跑。
  2. 新建连接时,HttpConnector 做 DNS 解析和 TCP 连接,再调用 hyper 的 client::conn::http1::handshake,得到一对东西:SendRequest 和 Connection。Connection 被交给执行器在后台跑,SendRequest 留给发请求的一方。
  3. SendRequest::send_request 不碰 socket,只把请求和一个 oneshot 回调放进 dispatch::channel。这个通道底下是 tokio 的无界 mpsc,再用 want 限制成「连接空闲时才收下一个请求」。
  4. 后台任务里跑的同样是 Dispatcher,只是 Dispatch 换成了客户端的实现:从通道取出请求、编码写出、读回响应头,通过回调把 Response 交回 send_request 的调用方。
  5. 响应 body 读完,连接重新就绪,回到池里。

两条主线共用同一个 Dispatcher 和 Conn(src/proto/h1/),区别只在两处:Dispatch trait 的实现(dispatch::Server 调用 service,dispatch::Client 读通道),以及 Http1Transaction 的实现(role::Server 解析请求、编码响应,role::Client 反过来)。所以「03 HTTP/1 连接」是全卷的地基,后面的客户端、body、升级都要链回那里。

阅读路线

  • 只想会用:读 01 全景、02 运行时抽象与核心类型、07 hyper-util 服务端,以及常见坑。读完能自己写 accept 循环、知道 TokioIo / TokioExecutor / TokioTimer 各管什么、会用 auto::Builder 和优雅停机,也知道最常见的几条报错从哪里来。
  • 想读懂实现:按顺序读。03 是重点,Dispatcher 的循环和 Conn 的读写状态机在那里;04 到 06 都建立在它上面;09 把各章的取舍汇总。
  • 用 hyper-util 的边角件:要给后台任务带 tracing span,读 02 的 rt::tracing;要经 HTTP 代理或 SOCKS 连出去,读 06 的 代理连接器 和两个相应的坑;想自己拼连接池,先读 06 的 连接池,再读 client::pool。这三章都建立在 02 和 06 前几章上,可以跳着读。

摘自 hyper 的代码片段一律保留原来的格式(4 空格缩进),方便和原文件对照;本站自己写的示例用 2 空格缩进。每个示例上方写出它需要的依赖和 feature,只开用到的那几个。实验的平台是 M2 Max、rustc 1.99.0。

前置知识

这一卷默认你会写 async Rust。hyper 不绑定运行时,但示例都跑在 tokio 上;hyper 1.x 的 Service 和 tower 的 Service 也要对照着看。用到的东西在前两卷讲过,这里只链过去:

概念讲它的页本卷用到它的地方
Future::poll、Wakerpoll 与唤醒Dispatcher 返回 Pending 前,读写两边都要留下唤醒
socket 读到 Pending从 TcpStream::read 到挂起、反应器Conn 读请求头、读 body 时挂起,数据到了由反应器唤醒连接任务
tokio::spawnspawn 全路径每条连接一个任务;HTTP/2 每个流再一个任务
SleepSleep 的一生header_read_timeout、HTTP/2 keep-alive ping
mpsc、oneshotmpsc、oneshot、watch、broadcast、Notify客户端把请求送进连接任务、把响应送回来
tower 的 Service 与 poll_readyService traithyper 自己的 Service 去掉了 poll_ready,call 只取 &self
OneshotServiceExt 与 OneshotTowerToHyperService 每个请求克隆一次服务,再走一遍 Oneshot
MakeServiceSteer 与 MakeServicehyper 0.14 和 axum 每条连接造一个服务

hyper 官网的 guides 有服务端和客户端的入门,lib.rs 的 crate 文档第一段也让新手先去读它。先照着写一个 hello world,再来看源码会轻松很多。

目录

部分内容
00 导读本页;源码地图
01 全景hyper 解决什么问题、1.0 为什么拿掉 Server 和 Client;一个请求的一生
02 运行时抽象与核心类型rt::Read / Write、Executor 与 Timer、hyper 的 Service 与 body、给执行器带 span
03 HTTP/1 连接本卷重点:服务端 Builder、Dispatcher、Conn 状态机、解析与编码、body 编解码、缓冲 IO、升级
04 bodyIncoming 与容量为 1 的通道;http-body-util
05 HTTP/2hyper 怎么用 h2:服务端、客户端、ping
06 客户端client::conn、legacy Client、连接池、HttpConnector、代理连接器、client::pool
07 hyper-util 服务端auto:一个端口两个协议;优雅停机
08 错误hyper::Error 的种类和自动回的状态码
09 设计取舍把各章的取舍汇总到一起
10 常见坑用报错原文做标题,讲它在源码里怎么产生
11 在本站中本站(开源)代码里怎么用 hyper,和本卷版本的差异
12 动手不用 hyper,从零写一个最小的 HTTP/1.1 服务端

小结

  • 全卷锁定 hyper v1.12.0(7f67699c)和 hyper-util v0.1.21(23a8689);两边的 master 都已走过 tag,行号以 tag 为准。
  • 服务端主线:accept → TokioIo →(auto 判断版本)→ serve_connection → Dispatcher 每圈读、写、刷;一条连接一个 future。
  • 客户端主线:池里取连接或新建 → handshake 得到 SendRequest 和 Connection → send_request 把请求放进通道 → 后台的 Dispatcher 写请求、读响应、经回调交回。
  • 两条主线共用 proto/h1 的 Dispatcher 和 Conn,差别只在 Dispatch 和 Http1Transaction 两个 trait 的实现。
  • 只想会用读 01、02、07 和常见坑;想读懂实现按顺序读,03 是重点。

💬 关联讨论

发起讨论
加载中...