Skip to content

第1章 为什么需要 Hyper 与 Tower:HTTP 栈的抽象边界

1.1 从一个 cargo new 说起

请你想象自己刚打开终端,输入:

bash
cargo new my-server && cd my-server

这是一个空白的 Rust 项目。你想写一个"最小可用"的 HTTP 服务器。下一步你会做什么?几乎不假思索,你会打开 Cargo.toml 添加:

toml
[dependencies]
tokio = { version = "1", features = ["full"] }
axum = "0.8"

然后写一个最简单的 handler,cargo run,浏览器访问 localhost:3000,"Hello, World!" 出现。你可能花了五分钟。

但让我们做一个思想实验:把依赖里的 axum 删掉,尝试不借助任何 Web 框架,只用 tokio 和一个 TCP socket 手写一个 HTTP/1.1 服务器。你会发现自己立刻撞上一堵墙:

  • 你得自己解析 HTTP 请求行(GET /foo HTTP/1.1\r\n),得实现一个状态机处理不完整的缓冲(因为 TCP 不保证一次读到完整的 header)。
  • 你得处理 Transfer-Encoding: chunked 的 body 解码(每块 HEX\r\n<data>\r\n),以及 Content-Length 指定长度的 body。
  • 你得维护 keep-alive 的长连接状态——什么时候该关闭?客户端发了 Connection: close 怎么办?HTTP/1.1 pipelining 要不要支持?
  • 你还得写一个写回包的编码器,处理 Transfer-Encoding: chunked 的响应 body。
  • 如果你的用户想支持 HTTP/2(当前绝大多数 API 网关的默认协议),你还得重新写一套完全不同的二进制 frame 解析、HPACK header 压缩、流控窗口管理、多路复用调度——HTTP/2 和 HTTP/1 除了都叫"HTTP"以外,在线路层几乎是两种毫不相关的协议。

这些事情加起来是几千到几万行精细调教、经得住 Fuzz 测试的代码,并且没有一行和你的业务逻辑有关。它们全都属于"HTTP 这个协议"——不属于"我的服务"。

这就是 Hyper 存在的第一个理由:把"HTTP 协议"这件事从业务代码里抽出来,让每个人不必重新发明轮子。

但 Hyper 只解决了一半问题。

1.2 一个中间件一写十遍的故事

现在你已经跑起来了一个 Hyper 写的 HTTP 服务。下一个需求来了:所有请求要打印一行日志。简单,你写一个函数包裹原来的 handler:

rust
async fn with_logging<H>(handler: H, req: Request) -> Response
where
    H: Fn(Request) -> Future<Output = Response>,
{
    println!("-> {} {}", req.method(), req.uri().path());
    let resp = handler(req).await;
    println!("<- {}", resp.status());
    resp
}

这段代码写起来容易。下一个需求:给每个请求加一个 30 秒超时。再写一个包裹函数:

rust
async fn with_timeout<H>(handler: H, req: Request, duration: Duration) -> Response {
    match tokio::time::timeout(duration, handler(req)).await {
        Ok(r) => r,
        Err(_) => Response::builder().status(504).body(...).unwrap(),
    }
}

下一个需求:限流、追踪、CORS、鉴权、Compression、重试……每一个都是一个包裹函数。两个月之后,你的团队在 axum、在 Tonic、在 reqwest、在 MQTT 客户端、在 Kafka 客户端、在自研 RPC 框架上都有自己的"一套包裹函数",每一套都要单独测试、单独维护、每次升级都有微妙的行为差异。

这正是 2016 年 Carl Lerche(同时也是 Tokio 的作者)意识到的问题:网络编程里 80% 的"中间件"并不依赖具体协议。超时、重试、限流、熔断、负载均衡、缓存——这些逻辑可以用一句话描述:

给定一个"异步函数 Request → Result<Response, Error>",返回一个新的"异步函数 Request → Result<Response, Error>",只是在输入输出之间多做一些事情。

如果我们能把"异步函数 Request → Result<Response, Error>"抽象成一个 trait,那么所有中间件都只依赖这个 trait,与底层协议完全无关——不管你的 Requesthttp::Request、是 gRPC 的 protobuf、是 Redis 命令还是自研的二进制协议,中间件都能复用。

这个 trait 就是 tower::Service

rust
// tower-service/src/lib.rs:322-367
pub trait Service<Request> {
    type Response;
    type Error;
    type Future: Future<Output = Result<Self::Response, Self::Error>>;

    fn poll_ready(&mut self, cx: &mut Context<'_>)
        -> Poll<Result<(), Self::Error>>;

    #[must_use]
    fn call(&mut self, req: Request) -> Self::Future;
}

区区不到十行代码,但它是整个 Rust 异步服务生态的公共语言。Tonic 的 gRPC 服务实现了它、Axum 的 Router 实现了它、reqwest::Client 内部用它、Linkerd 的代理基于它。你写一个 retry 中间件实现 Service<Req> for Retry<S>,它就能同时用在 HTTP 客户端、HTTP 服务端、gRPC、甚至你自己写的 WebSocket 框架上。

这就是 Tower 存在的理由:把"网络中间件"这件事从协议里抽出来,让每个人都能复用同一套工业级中间件。

1.3 M×N 问题与 M+N 解法——又一次

如果你读过丛书卷四《Serde 元编程》,你会对这个思路感到亲切。Serde 解决的是"M 种数据结构 × N 种序列化格式"的组合爆炸:不做抽象的话需要 M×N 份实现,而 Serde 用 Serializer + Deserializer 两个 trait 把矩阵拆成 M+N。

Hyper 和 Tower 解决的是同类型的问题,只不过这次的矩阵是:

\text{(协议种类)} \times \text{(中间件种类)} \times \text{(业务逻辑种类)}
  • 协议维度:HTTP/1、HTTP/2、HTTP/3、gRPC、WebSocket、自定义 RPC——假设有 6 种。
  • 中间件维度:timeout、retry、rate limit、load balance、tracing、auth、metrics、compression、circuit breaker——假设有 10 种。
  • 业务逻辑维度:你公司里上百个微服务。

没有抽象的话,每个业务写自己的中间件栈,每个栈 6 × 10 = 60 种组合。整个生态要做 6 × 10 × 100 = 6000 次工作。

Hyper 和 Tower 用两条正交的抽象把这个立方体拆平:

  • Hyper 定义"协议 → Service"的边界:它只关心"如何把字节流翻译成 Request、如何把 Response 翻译回字节流",中间那个"业务逻辑"被定义为任何实现了 Service trait 的东西。协议侧只需做 6 次工作。
  • Tower 定义"中间件 → Service"的边界:它只关心"给我一个 Service,我给你一个包裹后的新 Service",完全不管底下 Service 在做 HTTP、gRPC 还是 Redis。中间件侧只需做 10 次工作。
  • 你的业务:直接写一个 Service,由 Tower 包上中间件、由 Hyper 接到协议上。
6 \text{(协议)} + 10 \text{(中间件)} + 100 \text{(业务)} = 116 \ \ \ \text{替代了 6000}

这就是为什么我说 Hyper 和 Tower 是一对正交的抽象:它们一个切协议维度,一个切中间件维度,交点处留给你的业务。整个 Rust 后端生态就架在这两道边界上。

1.4 一张生态地图

我们把前面讨论的"谁定义了什么 trait、谁依赖谁"整理成一张图。下面每一个节点都是一个独立发布的 crate:

这张图里有几个关键信息值得你立刻记住:

  1. tower-servicetower-layer 是独立的小 crate。它们只定义 trait,几乎不含逻辑。之所以独立发布,是为了让它们能被 Hyper、Tonic、Axum 等库作为稳定接口依赖,而不被 tower 主 crate 的任何版本升级影响。这是一个工程细节——把"约定"和"实现"分开 crate 发布——值得你在自己项目里借鉴。

  2. hyper-util 是一个非常有故事的 crate。它的存在本身就是 Hyper 1.0 版本的一个"妥协痕迹":Hyper 把自己原来的一些便利工具(比如基于 Tokio 的 TokioExecutor、基于 tower 的连接池 Builder)剥离出去,让 hyper 主 crate 能对 runtime、对 Tower 都保持完全中立。换句话说,hyper 1.x 本身并不依赖tower——这是一个历史上罕见的抽象分离动作,第 13 章和第 19 章会展开讲。

  3. httphttp-body 是 trait only 的数据模型层。它们定义"什么是请求、什么是响应、什么是 body",但不负责任何实际的网络 I/O。Axum 的 handler 签名里出现的 Request<B>Response<B>,归根到底都是 http crate 里的类型。

  4. HTTP/1 和 HTTP/2 的 wire 解析分别依赖 httparseh2——两个完全独立的 crate,不是 Hyper 的内部模块。Hyper 只是把它们"串到"同一个 Connection<T, S> 抽象上。

后面第 9、10 章会逐层精读这张图的每一条边。

1.5 一个请求的完整旅行

抽象讲完了,我们用一段真实的代码串起来。这是一个最简化的"基于 hyper 1.x + tower 的服务端"程序——注意,axum 或任何高层框架都没有出现,这就是 axum 自己依赖的底层:

rust
use std::convert::Infallible;
use std::net::SocketAddr;
use http_body_util::Full;
use hyper::body::Bytes;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Request, Response};
use hyper_util::rt::TokioIo;
use tokio::net::TcpListener;

async fn hello(_: Request<hyper::body::Incoming>)
    -> Result<Response<Full<Bytes>>, Infallible>
{
    Ok(Response::new(Full::new(Bytes::from("Hello, World!"))))
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let addr: SocketAddr = "127.0.0.1:3000".parse()?;
    let listener = TcpListener::bind(addr).await?;
    loop {
        let (stream, _) = listener.accept().await?;
        let io = TokioIo::new(stream);
        tokio::task::spawn(async move {
            if let Err(err) = http1::Builder::new()
                .serve_connection(io, service_fn(hello))
                .await
            {
                eprintln!("Error: {:?}", err);
            }
        });
    }
}

这段代码来自 hyper 官方 guide(hyper/examples/hello.rs 的简化版)。我们把一次请求从到达到响应的完整旅程拆开,读到的每一行日后都会在本书某一章出现:

  1. TcpListener::bind + accept:这是 Tokio 的事,底层是 mio 注册的 epoll/kqueue,具体在卷四《Tokio》第 8 章I/O Driver 架构。每一个 accept 返回的 TcpStream 实现了 tokio::io::AsyncReadAsyncWrite

  2. TokioIo::new(stream)TcpStream 实现的是 Tokio 的 AsyncRead/AsyncWrite,而 Hyper 1.x 为了 runtime 中立定义了自己的 hyper::rt::Readhyper::rt::Write trait。TokioIohyper-util 提供的适配器——它把 Tokio 的 AsyncRead 转换成 hyper 的 Read。这个细节是第 19 章的主角。

  3. http1::Builder::new().serve_connection(io, service):这是 Hyper 的入口。它接收一个 IO 对象和一个 Service,返回一个 Connection<T, S> future。这个 future 会在它的整个 .await 期间:

    • 读取 IO 上的字节流,交给 httparse 解析成 Request<Incoming>
    • Request 交给你传入的 Service 处理,拿到 Response
    • Response 编码回字节流写回 IO;
    • 管理 keep-alive,直到对端关闭连接或超时。

    整个 Connection<T, S> 的状态机在 hyper/src/proto/h1/conn.rsdispatch.rs 里,是第 12 章的重头戏。

  4. service_fn(hello)service_fn 是一个适配器,把一个普通的 async fn 包装成 hyper::service::Service。关键点在于,这里是hyper 的 Service(定义在 hyper/src/service/service.rs),不是 tower 的 Service。两者的签名不同:

    • tower::Service::call(&mut self, req: Req) -> Self::Future
    • hyper::Service::call(&self, req: Req) -> Self::Future

    差一个字符——&mut&——但这个差别背后是一场关于"并发请求在同一个 Service 上的语义"的长达数年的讨论。第 13 章会把这段历史和源码一起读清楚。

  5. tokio::task::spawn(async move { ... }):每一个新连接被独立 spawn 成一个 task。这是最朴素的 per-connection task 模型——它意味着 hyper 本身完全不管"并发连接数",你可以自由选择直接 spawn、用 semaphore 限流、或者用 tower 的 Limit 中间件。

短短不到 30 行代码,串起了:

  • 卷四讲过的 tokio::nettokio::spawn
  • 本书要讲的 hyper::rt::Read / Write(第 19 章)
  • hyper::server::conn::http1::BuilderConnection<T, S>(第 11-12 章)
  • hyper::service::Service trait 及 service_fn 适配器(第 13 章)
  • http::Request / http::Response / http_body_util::Full(第 9-10 章)
  • hyper_util::rt::TokioIo 适配器(第 19 章)

每一段都有源码故事,本书会把它们一个个讲清楚。

1.6 如果加一层中间件——Tower 登场

现在给上面这段代码加一个需求:所有请求 30 秒超时,超过就返回 504

不借助 Tower 的话,你会在 hello 函数里写 tokio::time::timeout。但这样有几个问题:

  • 超时逻辑和业务逻辑耦合在一起;
  • 如果你想改成 60 秒,要改业务代码;
  • 如果第二个 handler 也要超时,你要把 tokio::time::timeout 重复一遍;
  • 超时发生时怎么构造 Response?每个业务自己拼。

Tower 的回答是:把超时做成一个 Service wrapper。概念代码如下(真实实现来自 tower/src/timeout/mod.rs):

rust
pub struct Timeout<S> {
    inner: S,
    timeout: Duration,
}

impl<S, Req> Service<Req> for Timeout<S>
where S: Service<Req>, S::Error: Into<BoxError>, ...
{
    type Response = S::Response;
    type Error = BoxError;
    type Future = ResponseFuture<S::Future>;

    fn poll_ready(&mut self, cx: &mut Context) -> Poll<...> {
        self.inner.poll_ready(cx).map_err(Into::into)
    }

    fn call(&mut self, req: Req) -> Self::Future {
        let sleep = tokio::time::sleep(self.timeout);
        let response = self.inner.call(req);
        ResponseFuture::new(response, sleep)
    }
}

这是 Tower 的"洋葱模型"的最小单元:一个 Timeout<S> 自己是一个 Service,它的 call 里调用内层 Scall,再套上 tokio::time::sleep。当有人对 Timeout<S> 调用 call 时,从外到内:外层负责超时、内层负责业务。

如果再加一层限流呢?RateLimit<Timeout<S>>——嵌套一层就完了。再加 retry?Retry<RateLimit<Timeout<S>>>——继续套。每一层都是一个 Service,类型签名在编译期就把整个洋葱的结构钉死,没有运行时开销、没有 vtable 查找。

这个"用 trait 一层层包装"的模式,本质上就是函数式编程里的 decorator,或者面向对象编程里的装饰器模式,但因为 Rust 的 trait + 泛型体系,它能做到零运行时成本完全类型安全——不可能在运行时"忘了加某个中间件",类型系统会在编译期拦下。

但层数多了以后,手写嵌套会很难看:

rust
let svc = Retry::new(retry_policy,
           RateLimit::new(rate,
            Timeout::new(
              LoggingService::new(MyHandler))));  // 反向嵌套顺序,对使用者非常反直觉

Tower 为此提供了 Layer trait 和 ServiceBuilder:把上面的洋葱拆成"一层 Layer"一个,从外到内依次正向组合:

rust
let svc = ServiceBuilder::new()
    .layer(RetryLayer::new(retry_policy))
    .layer(RateLimitLayer::new(rate))
    .timeout(Duration::from_secs(30))
    .service(MyHandler);

LayerServiceBuilder 在类型系统层面做的事情,是第 3 章的主题。你会看到它们如何在编译期把上面的链式调用 unrol 成一个具体的、单态化后没有一丝虚方法的复合 Service 类型。

1.7 Hyper 与 Tower 的分工——再明确一次

到这里你应该能看清这两个库的分工:

  • Hyper 只关心"协议 ↔ Service"这一条边。给它一个 Service 和一个 IO 对象,它负责字节流和 Request/Response 的互译。它的主接口是 Connection<T, S>(server 侧)和 Client(client 侧)——都是把协议包成 Service或者把 Service 包成协议

  • Tower 只关心"ServiceService"这一条边。它接收一个 Service,返回一个被中间件装饰过的新 Service——对底层的协议完全无感。

两者在 Service trait 这一层交会,形成了 Rust 后端生态最重要的抽象分界。用一个不完美但好记的类比:Hyper 是电源(把外部能量转换成内部电压),Tower 是变压器/调节器(在内部做电压变换),你的业务逻辑是电器(消费电)。三者用统一的插头形状(Service trait)连接起来。

更微妙的地方在于:Tower 不只能服务 HTTP。Tonic 用同样的 tower::Service 包 gRPC、reqwest 的连接层也接入 Tower、Linkerd 的代理全栈建立在 Tower 之上、甚至非网络场景——比如 sqlx 的连接池、rdkafka 的 producer——理论上都可以实现 Service。相比之下 Hyper 是专门为 HTTP 设计的。这种"Tower 通用 / Hyper 专精"的分工,在第 2 章拆 Service trait 的时候会看得更清楚。

1.8 一段不短的历史

要真正理解为什么 Hyper 和 Tower 长成今天这个样子,必须说一点历史。

1.8.1 Hyper 的九年

Hyper 由 Sean McArthur(@seanmonstar)在 2014 年启动,当时的目标很朴素:给 Rust 一个能用的 HTTP 客户端和服务端实现。早期版本(0.1 - 0.9)是同步的——接口用 std::io::Read/Write,没有 async/await(因为 Rust 当时还没有)。

2016 - 2018 年是 Tokio 和 Future combinator 的时代。Hyper 0.10 重写为基于 futures 0.1 的 Future combinator 风格——那个时代的代码里充满了 .and_then().map_err().for_each(),连接状态机是用 loop_fn 这样的工具手动展开的。读过那段代码的人都记得它的复杂,即使它是当时最好的写法。

2019 年 async/await 进入 Rust 稳定版。Hyper 0.13 是第一个全面拥抱 async/await 的版本,内部代码一下子变得可读。这是 Hyper 历史上最大的一次重写。

接下来的 0.14 系列(2020 - 2023)是 Hyper 最稳定的时期,被广泛用在生产环境。但有一个历史负担始终没解决:hyper 0.x 直接依赖 tokiobytes,并且暴露了 hyper::server::Serverhyper::client::Client 等带便利默认实现的 API。这些便利是好事,但也意味着:

  • hyper 没法用在非 Tokio runtime 上(除非 fork);
  • hypertokio 的版本必须锁死在一起,一方升级另一方就得跟;
  • hyper::Server 的 API 和 tower 生态紧密耦合——要把中间件栈塞进去很别扭。

2023 年底 Hyper 1.0 正式发布——这是一次有意识的最小化hyper 1.x 主 crate 里几乎只保留了协议实现,tokio 绑定、executor、连接池、服务构建器全部剥离到 hyper-util。新的 hyper::rt::{Read, Write, Executor, Timer} trait 让 hyper 本身对 runtime 中立——你可以把 hyper 嵌入任何 async runtime,只要实现这几个 trait。

这是一次漂亮的抽象收缩:用 API 面积缩小换取长期的生态独立性。本书使用的 1.9.0 是 1.0 之后数次迭代的成熟版本。

1.8.2 Tower 的十年

Tower 的历史比 Hyper 稍晚。2016 年 Carl Lerche 提出一个想法:既然 Finagle(Twitter 开源的 Scala RPC 栈)用"Service = Request => Future[Response]"这个抽象搞出了整个微服务生态,Rust 为什么不能抄一个?

最早的 tower crate(当时叫 tower-service)只有 Service trait 的定义。接着的几年里,Tower 经历了几次大的演进:

  1. poll_ready 的加入:早期的 Service 没有 poll_ready。加入它是因为"背压"——服务不应该被动拒绝请求,而应该主动告诉调用方"我还能不能接"。这个设计后来成为 Tower 的灵魂(详见第 4 章)。

  2. Layer trait 的独立Layer 在 2018 年左右从 tower 主 crate 里剥离到独立的 tower-layer crate。动机和 tower-service 独立一样——让它能被 hyper、Tonic 等核心库稳定依赖

  3. 中间件逐一成熟:Timeout、Retry、Balance、LoadShed、Buffer 等中间件在 Linkerd 的生产环境中被反复打磨。Linkerd 用 Tower 承担了数万个生产集群的流量,这些中间件的每一个 bug 都被真实事故推过,才演化到今天的源码形态。

  4. Service::call&self 讨论:2022 - 2023 年发生了一场至今仍在 hyperium/hyper#3040 上可以看到的长讨论:hyper 1.0 里要不要直接用 tower::Service?最后的答案是"不"——hyper 自己定义了一个简化版的 Service trait,签名用 &self 而不是 &mut self,不带 poll_ready。这个决定的原因和影响是第 13 章的核心。

今天的 Tower 0.5.x 是一个非常稳定的版本——ServiceLayerServiceBuilder 的 API 已经多年没有 breaking change。生态基本把它当作一个准标准库对待。

1.8.3 为什么这段历史对你重要

你读到这里可能觉得这段史料有些啰嗦——但如果你想成为一个能在源码层面对话的 Rust 后端工程师,有几个东西必须你自己看清:

  • Hyper 1.0 是一次主动缩小 API 面的动作——这在开源世界非常罕见。多数项目的演进方向是"加功能",Hyper 选择"去功能",把权力交给上游适配层。这种自我克制,本身就是一种可借鉴的工程美学。
  • Tower 早期就把 tower-servicetower-layer 单独发 crate——这让后来的 hyper、tonic、axum 都能稳定依赖它们,而不受 tower 主 crate 升级影响。这是一个你完全可以在自己的内部库设计里借鉴的模式:把"约定"分离成独立的小 crate,让上游项目安心依赖
  • poll_ready 不是凭空想出来的,是从 Finagle 的实战经验抄过来的——背压是分布式系统长期痛点,Tower 把它变成类型系统里显式的东西,是一次从 API 设计层面解决工程问题的经典案例。

1.9 对照其他语言生态

你可能在 Go、Node.js 或 Java 里写过类似的服务。为什么在这些语言里我们不需要"Hyper / Tower 这样的两层抽象"?

Go 的回答:标准库net/http.Handler 是 Go 标准库里的"Service"——一个接口,ServeHTTP(w ResponseWriter, r *Request)——所有中间件都围绕它构建。Go 的抽象层次比 Rust 低:Handler 是 imperative 的(直接写入 ResponseWriter),没有 poll_ready 这种显式背压信号;但 Go 用 goroutine 和运行时调度隐藏了大部分并发复杂性,开发者不需要看到 Future、Waker、Pin 这些东西。代价是:Go 的背压模式弱(经典问题是 goroutine 泄漏),超时需要手动 context 传递,运行时调度不可定制。

Node.js 的回答:Express/Fastify 之类的用户态框架。Node 标准库的 http.Server 很朴素,社区用 Express 等框架填空。Express 的"middleware = function(req, res, next)"是一种事实上的"Service"抽象,但它是协议绑定的(middleware 只能用在 HTTP 上),迁移到 TCP、WebSocket、MQTT 都得重新发明。

Java 的回答:Servlet + 各种 Filter。Servlet 规范从 1998 年开始就有了类似 Service 的抽象,但它是同步 + 线程池模式。Netty 之后引入了 ChannelHandler pipeline——这才是一个真正和 Tower/Hyper 可对比的抽象。Netty 和 Tower 的差别:Netty 是面向 bytes 和 frames 的低层流水线(更像 Hyper 内部的 proto 模块),Tower 是面向请求-响应语义的高层抽象(更像 Hyper + Tower 的组合)。

Rust 的回答就是 Hyper + Tower——两个独立的抽象层,明确区分"协议"和"中间件"。代价是学习曲线陡(你得同时看懂两个库),收益是极致的可组合性零运行时成本(因为所有的抽象都在编译期单态化,底层代码和你手写的 imperative Rust 几乎一样快)。

要说 Rust 生态在这件事上的"独特贡献",就是明确地把协议 / 中间件 / 业务三件事拆成三道独立的 trait 边界,并且借助 Rust 的 trait 单态化让这种拆分完全免费。你会在本书后续章节反复看到这种"显式拆分 + 零成本抽象"的组合拳。

1.10 小结:落到你键盘上

我们在本章做了三件事:

  1. 回答了"为什么":为什么 Rust 后端生态需要 Hyper 和 Tower 这两层抽象——它们把协议、中间件、业务三件事解耦到三个正交维度,把潜在的 M×N×K 组合爆炸降成 M+N+K。
  2. 画了一张地图:Hyper、Tower 和它们的依赖 crates(http、http-body、h2、httparse、tower-service、tower-layer、hyper-util)之间的依赖方向和 trait 交会点。
  3. 讲了一段历史:Hyper 从 0.1 到 1.0 的九年演进、Tower 十年来的设计选择、Hyper 1.0 为什么没有直接用 tower::Service。

落到你键盘上的三件事:

  • 打开 hypertower 的源码——本书每一章都会带你读它们。现在就 git clone https://github.com/hyperium/hyper && git checkout 0d6c7d5,和 git clone https://github.com/tower-rs/tower && git checkout 251296d
  • cargo doc --open 一遍。对着生成的文档过一下 hyper::server::connhyper::servicehyper::bodytower::Servicetower::Layertower::ServiceBuilder——混个脸熟,下一章开始我们会逐个深入。
  • 跑一遍本章那 30 行的 hyper 最简服务端。别跳过——把它在本地 debug 一次,用 tracing 或者 println! 打印出每个请求到达的时间和 handler 被调用的时间,你会直观感受到 serve_connection 这个 future 在驱动什么。

下一章,我们把 tower::Service trait 的每一行、每一个关联类型、每一个边界条件,逐字读清楚——看它为什么是这个签名,而不是别的签名。

基于 VitePress 构建