Hyper 与 Tower:工业级 HTTP 栈

第13章 hyper 的 Service trait:为什么 1.0 不复用 tower::Service

作者 杨艺韬 · 10,919 字

第13章 hyper 的 Service trait:为什么 1.0 不复用 tower::Service

13.1 一个看起来不合理的决定

读到这里你可能产生一个疑问:

Tower 已经把 Service 这个抽象做成了整个 Rust 异步生态的事实标准——Tonic、Axum、reqwest、Linkerd 全部用它。hyper 1.0 居然不用这个 trait,而是自己定义了一个几乎一模一样但不一样hyper::service::Service?这不是在浪费精力、分裂生态吗?

表面看确实。两个 trait 摆在一起:

// tower-service/src/lib.rs
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>>;

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

// hyper/src/service/service.rs
pub trait Service<Request> {
    type Response;
    type Error;
    type Future: Future<Output = Result<Self::Response, Self::Error>>;

    fn call(&self, req: Request) -> Self::Future;
}

只有两处差别:

  1. &mut self vs &self(call 方法)
  2. poll_ready vs 没有 poll_ready

两处差别看起来都很小。但 hyper 团队花了三年时间、三十多条长讨论、一次社区投票,最终决定在 hyper 1.0 里用 tower::Service——而是定义了自己的版本。

这一章我们把这个决定的背后完整展开——不是”谁对谁错”,而是**“为什么两者都对,只是服务不同场景”**。读完你对 Rust 类型系统和协议设计的理解会更深一层。

13.2 先看结论:两个 trait 的分工

一句话:

  • tower::Service 面向业务组合层——它要支持中间件堆叠、背压传递、容量控制。poll_ready 是它的灵魂。
  • hyper::Service 面向协议连接层——它要支持 HTTP/2 多路复用、并发调用、异步 fn 原地使用。&self 是它的灵魂。

中间由 hyper_util::service::TowerToHyperService 适配器桥接。任何 tower 世界里的 Service<Request> 都能变成 hyper 能接的 hyper::Service<Request>——只是在桥接点,tower 的 poll_ready吞掉

要理解这个分工为什么是对的,我们要从最具体的技术困境讲起。

13.3 困境一:HTTP/2 的并发调用

想象一个 HTTP/2 server 的场景:

  • 一条 TCP 连接上复用着 50 个 stream(HTTP/2 默认最多 100 个并发 stream)。
  • 每个 stream 是一个独立的 HTTP 请求-响应。
  • server 侧收到 DATA frame 时,要立刻路由到对应 stream 的 Request 处理器
  • 这些处理器同时进行——一个 stream 在处理 100KB 的 body upload,另一个 stream 在等数据库查询,它们应该并发。

所以 server 需要在同一个 Service 实例上同时调用多次 call——这就是 HTTP/2 多路复用的本质。

13.3.1 &mut self 强制串行

现在看 tower::Service::call(&mut self, req)&mut self 意味着”独占访问”——同一时间只有一个 call 能跑。

如果我们强行让 hyper HTTP/2 server 接受 tower Service,50 个并发 stream 必须排队调用 call

// 错误的幻想:
// stream 1 进来,svc.call(req1) 被调用,svc 被独占
// stream 2 进来,等 svc 空闲
// stream 3 进来,等
// ...

结果:HTTP/2 多路复用退化成串行。你买了一条 10Gbps 的光纤、开了 HTTP/2、做了所有 TLS 优化——但 server 一次只能处理一个请求。这和 HTTP/1.0 没有区别。

13.3.2 Clone 不是万能解药

有人会说:“Service 不是通常 Clone 吗?可以给每个 stream 一个 clone。”

对,也不对。

  • 如果 Service: Clone,确实可以给每个 stream 预分配一个 clone——但谁来 clone、什么时候 clone?每次 accept 新 stream 时 clone?那 clone 本身可能很重(Axum 的 Router clone 很便宜,但如果 Service 里有一个 Arc<Config>,clone 是 Arc::clone,OK;但如果 Service 里有一个 HashMap<String, Handler>——clone 就是深拷贝,昂贵)。
  • 更糟糕的是:如果 Service 不 Clone(例如它持有一个 tokio::sync::Mutex、一个 Receiver<T>、或者一个不可 Clone 的 DB 连接),HTTP/2 server 根本建立不起来——类型系统直接拒绝。
  • 即使 Service 能 Clone,每个 clone 的 poll_ready 状态独立(第 4 章讨论过 ConcurrencyLimit 的 Clone 实现)——意味着 50 个 stream 各自 clone 各自 poll_ready,丧失”一份统一资源管理”的意义。

这是一个架构问题,不是 clone 成本问题。&mut self 的语义就是”独占”——强行在多 stream 场景下使用,要么串行化(丢掉 HTTP/2 的意义),要么 clone(丢掉共享状态的意义)。两条路都错。

13.3.3 &self 直接支持并发

hyper::Service::call(&self, req) 只要 &self——不独占。同一个 Service 实例可以被多个并发 call 同时引用。

这意味着:

  • HTTP/2 server 只需要一个 Service 实例。所有 stream 都借用 &self 发起 call。
  • Service 内部如果有可变状态,放 Arc<Mutex<T>>AtomicXxx、或者 RwLock<T>——Rust 自然会提醒你”这里要加同步”。
  • 不强迫 Clone。不 Clone 也能跑。

代价当然是:你在 call(&self, ...) 里不能直接 self.counter += 1。必须用 Atomic::fetch_add 或者 Mutex::lock()。但这正是”并发调用”需要的——借用检查器强制你显式处理并发

所以 &self 是 HTTP/2 场景的必选,不是一个喜好问题。

13.4 困境二:poll_ready 在连接层没有语义

Tower 的 poll_ready 是整个 backpressure 协议的灵魂(第 4 章完整讲过)。但把它放到 HTTP 连接层,问的问题没答案

13.4.1 “这个连接能不能接新请求?”

Tower Service 的 poll_ready 在问”这个服务能不能接新请求”。把它套到 hyper 的 Connection 上,问题变成:“这条 HTTP 连接能不能接新请求”?

HTTP/1 上这问题无意义——HTTP/1 一次只能处理一个请求,“能不能接”由协议本身决定。

HTTP/2 上更无意义——每个 stream 独立,连接本身永远能接(直到 MAX_CONCURRENT_STREAMS 达到,但那是 h2 层面的事,不是 Service 层面)。

用户真正关心的背压——“CPU 满了、数据库连接池满了、上游服务满了”——这些全是业务层问题,应该由业务代码里的 ConcurrencyLimit / Buffer 等中间件处理,不应该由连接层的 Service::poll_ready 回答。

13.4.2 连接层的 poll_ready 只能透传

即使勉强让连接层有 poll_ready,它能做的只是透传给业务 Service 的 poll_ready

// 幻想版
impl hyper::Service<Request> for ConnectionService<S: tower::Service<...>> {
    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        self.inner.poll_ready(cx)  // 透传
    }
}

这 100% 是废代码——连接层不加任何东西。然后这个透传还要处理 HTTP/2 多路复用问题:50 个 stream 并发都调 poll_ready?预扣 50 次资源?第一个 stream 的 call 消耗许可后其他 stream 怎么办?

这就是 hyperium/hyper#3040 里长达 100+ 条评论讨论的核心——poll_ready 的语义在连接层根本无法一致落地

13.4.3 hyper 的决定:干脆不要

hyper 团队最终决定:poll_ready 从 hyper::Service 里彻底去掉

不是不需要背压——而是把背压责任留给业务 Service。hyper 连接层只是把请求原样搬到 Service,Service 自己要不要做背压、怎么做、跟中间件怎么配合——hyper 不管。

这个决定让 hyper 的协议层代码大大简化——conn.rs 里没有任何 “poll_ready then call” 的两阶段协议、没有”多个 clone 的许可是否共享”的困惑、没有”HTTP/2 多流如何共享 ready 状态”的痛苦讨论。hyper 只做协议,业务用 tower 在 hyper 外面做背压——责任分离得干净。

13.5 源码对比:hyper::Service 的极简

// hyper/src/service/service.rs:32-57
pub trait Service<Request> {
    type Response;
    type Error;
    type Future: Future<Output = Result<Self::Response, Self::Error>>;

    /// Process the request and return the response asynchronously.
    /// `call` takes `&self` instead of `mut &self` because:
    /// - It prepares the way for async fn,
    ///   since then the future only borrows `&self`, and thus a Service can concurrently handle
    ///   multiple outstanding requests at once.
    /// - It's clearer that Services can likely be cloned.
    /// - To share state across clones, you generally need `Arc<Mutex<_>>`
    ///   That means you're not really using the `&mut self` and could do with a `&self`.
    ///   The discussion on this is here: <https://github.com/hyperium/hyper/issues/3040>
    fn call(&self, req: Request) -> Self::Future;
}

整个 trait 三个关联类型 + 一个方法。没了

看那段注释——hyper 作者把做出这个决定的理由就写在 trait 定义上,并给了 issue 链接(#3040)。这是一种很克制的文档风格——把设计决策写进代码旁的注释,让读者能随时追溯为什么这里长这样。源码注释里直接引 issue 编号是工业级开源的美德。

13.5.1 额外的 impl:让 &SBox<S>Arc<S> 也是 Service

// hyper/src/service/service.rs:59-112
impl<Request, S: Service<Request> + ?Sized> Service<Request> for &'_ S { ... }
impl<Request, S: Service<Request> + ?Sized> Service<Request> for &'_ mut S { ... }
impl<Request, S: Service<Request> + ?Sized> Service<Request> for Box<S> { ... }
impl<Request, S: Service<Request> + ?Sized> Service<Request> for std::rc::Rc<S> { ... }
impl<Request, S: Service<Request> + ?Sized> Service<Request> for std::sync::Arc<S> { ... }

5 个 blanket impl——任何对 Service 的”引用、指针、智能指针”都自动也是 Service。这让 Arc<MyService> 可以直接传给 hyper 的 serve_connection,不需要自己写 wrapper。

特别注意 &mut S 也是——这看起来自相矛盾:hyper::Service::call&self,而这个 impl 里我们通过 &mut S 调到 (**self).call(req)——本质还是 &S。只是允许用户传 &mut MyService 也能适配。

13.6 HttpService:sealed trait 的美学

// hyper/src/service/http.rs:20-38
pub trait HttpService<ReqBody>: sealed::Sealed<ReqBody> {
    type ResBody: Body;
    type Error: Into<Box<dyn StdError + Send + Sync>>;
    type Future: Future<Output = Result<Response<Self::ResBody>, Self::Error>>;

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

// :40-54
impl<T, B1, B2> HttpService<B1> for T
where
    T: Service<Request<B1>, Response = Response<B2>>,
    B2: Body,
    T::Error: Into<Box<dyn StdError + Send + Sync>>,
{
    type ResBody = B2;
    type Error = T::Error;
    type Future = T::Future;

    fn call(&mut self, req: Request<B1>) -> Self::Future {
        Service::call(self, req)
    }
}

mod sealed {
    pub trait Sealed<T> {}
}

impl<T, B1, B2> sealed::Sealed<B1> for T
where T: Service<Request<B1>, Response = Response<B2>>, B2: Body,
{
}

这段代码演示了一个绝妙的设计技巧:sealed trait

13.6.1 HttpService 是什么

HttpService<B1> 是一个更具体的 Service——它规定:

  • 请求类型必须是 Request<B1>
  • 响应类型必须是 Response<B2> 其中 B2: Body
  • Error 必须能转成 BoxError

也就是说,HttpService 把 “这是一个 HTTP 服务”的约束塞到 trait 签名里——hyper 的 Connection<T, S: HttpService<IncomingBody>> 只接受 HttpService,而不是任意 Service。

13.6.2 为什么要 sealed

HttpService 的定义是 sealed trait——用户不能直接实现它:

pub trait HttpService<ReqBody>: sealed::Sealed<ReqBody> { ... }

mod sealed {
    pub trait Sealed<T> {}
}

Sealed 是 private module 里的 trait,外部看不见。HttpService 的 supertrait 约束要求 Self: Sealed——但 Sealed 只被 impl<T: Service<...>> Sealed for T blanket 实现。所以:

  • 用户写 impl HttpService<MyBody> for MyThing 会编译失败,因为 MyThing 没有 Sealed 实现。
  • 用户写 impl Service<Request<MyBody>> for MyThing,hyper 通过 blanket impl 自动把它变成 HttpService<MyBody>——这是唯一的路径。

这是 Rust 社区的经典 sealed pattern

  1. 公开 trait A,要求 A: SealedA
  2. SealedA 在 private module,用户看不见。
  3. 给所有”合法实现 A 的类型” blanket impl SealedA——因此用户看不到也写不出自己的实现。
  4. 这意味着 trait A 的实现者集合完全由库作者控制——库作者可以未来添加 A 的新方法、改签名,不会 break 下游(因为下游不可能直接实现 A)。

13.6.3 一个具体的 sealed 好处

假设 hyper 未来想给 HttpService 加一个方法:

pub trait HttpService<ReqBody>: sealed::Sealed<ReqBody> {
    ...
    fn push_promise(&self, ...) -> ...;  // 假设加一个方法
}

如果 HttpService 不是 sealed,这是破坏性变更——下游所有 impl HttpService for MyThing 都要改。

因为它是 sealed——下游没法直接 impl。新方法可以通过 #[doc(hidden)] + blanket impl 加默认实现——所有现有 Service 自动获得新方法,兼容性完好。

Sealed trait 是在”开放实现”和”保留演进权”之间的精妙平衡。API 设计里用得好的地方非常多,下次看到 pub trait X: Sealed 你就知道为什么。

13.7 service_fn:闭包适配器

hyper::service::service_fn 让你用一个普通的 async fn 构造 hyper Service,不需要自己写 struct + impl:

// hyper/src/service/util.rs:30-39
pub fn service_fn<F, R, S>(f: F) -> ServiceFn<F, R>
where F: Fn(Request<R>) -> S, S: Future,
{
    ServiceFn { f, _req: PhantomData }
}

// :42-45
pub struct ServiceFn<F, R> {
    f: F,
    _req: PhantomData<fn(R)>,
}

// :47-62
impl<F, ReqBody, Ret, ResBody, E> Service<Request<ReqBody>> for ServiceFn<F, ReqBody>
where
    F: Fn(Request<ReqBody>) -> Ret,
    ReqBody: Body,
    Ret: Future<Output = Result<Response<ResBody>, E>>,
    E: Into<Box<dyn StdError + Send + Sync>>,
    ResBody: Body,
{
    type Response = crate::Response<ResBody>;
    type Error = E;
    type Future = Ret;

    fn call(&self, req: Request<ReqBody>) -> Self::Future {
        (self.f)(req)
    }
}

用法:

let svc = service_fn(|req: Request<Incoming>| async move {
    Ok::<_, Infallible>(Response::new(Full::new(Bytes::from("hello"))))
});

一个闭包就是一个 hyper Service。

注意 F: Fn(...) 而不是 FnMut——闭包必须是 Fn(只借不改)。如果你在闭包里捕获 &mut 变量(比如 move |req| { self.counter += 1; ... }),编译失败——因为 Service::call 拿的是 &self,闭包跟着也只能 &Fn

这又一次印证 &self 的选择——它从 trait 层面强制你显式处理共享状态。如果你需要 counter,用 AtomicU64::fetch_add,或者把它包进 Arc<Mutex> 再 clone 进闭包。

13.7.1 PhantomData<fn(R)> 的方差

pub struct ServiceFn<F, R> {
    f: F,
    _req: PhantomData<fn(R)>,
}

PhantomData<fn(R)> 不是 PhantomData<R>——为什么?

PhantomData<T> 让类型”表现得像持有 T”——决定了 variance。PhantomData<R> 会让 R 成为协变(covariant);PhantomData<fn(R)> 让 R 成为逆变(contravariant)。

对 ServiceFn 来说,R 是请求类型——Service 消费请求,所以对 R 逆变:如果 R: AB: A(B 是 A 的子类型),那么消费 A 的 Service 也能消费 B(因为 B 也是 A)。逆变是正确的。

PhantomData<fn(R)> 表达”我是个消费者”——不持有 R 的生命周期,但承担 R 的逆变语义。这是 Rust PhantomData 最 subtle 的一个用法。不了解方差的读者可能完全看不出差别,但它在复杂泛型场景会决定能不能通过 borrow checker。卷三《Rust 编译器与运行时揭秘》第 4 章(生命周期与区域分析)里讨论过方差——这里是一个具体应用。

13.8 TowerToHyperService:桥接的秘密

hyper 自己定义了 Service trait,但不意味着 tower 的用户要迁移到 hyper Service。hyper-util crate 提供 TowerToHyperService 适配器:

// hyper-util(源码在独立 crate,这里简化)
pub struct TowerToHyperService<S> {
    service: S,
}

impl<S, Req> hyper::service::Service<Req> for TowerToHyperService<S>
where
    S: tower::Service<Req> + Clone,
    ...
{
    type Response = S::Response;
    type Error = S::Error;
    type Future = TowerToHyperServiceFuture<S, Req>;

    fn call(&self, req: Req) -> Self::Future {
        let mut svc = self.service.clone();   // 关键:clone
        Box::pin(async move {
            // 先 poll_ready,再 call
            futures::future::poll_fn(|cx| svc.poll_ready(cx)).await?;
            svc.call(req).await
        })
    }
}

逻辑:

  1. 进入 call(&self, req)——hyper 的 trait 要求 &self
  2. 因为要调 tower 的 &mut self + poll_readyclone 一份 service
  3. 在返回的 future 里,先 poll_readycall

代价很明显:每次请求 clone 一次 tower Service。对 Axum 这种 Service 本质是 Arc<Inner> 的场景,clone 是 Arc::clone——几纳秒、无分配。对其他场景——视具体 Service 而定。

背压被吞掉:tower 栈的 poll_ready 在桥接器里被立刻 await ——任何 Pending 变成异步等待,对 hyper 连接层完全不可见。hyper 看到的是一个”永远 ready”的 Service——这是 hyper::Service 不带 poll_ready 的自然结果。

这是 hyper 1.0 分裂的唯一代价——但这个代价几乎只在连接层之上发生(TowerToHyperService 把 tower 适配成 hyper Service 时),业务层的 tower 中间件栈内部 poll_ready 链条完好无损。

13.9 哲学对照:Sync vs Async、严格 vs 宽松

深一层看,这场 hyper Service vs tower Service 的”分裂”,其实是 Rust 异步社区长期以来的一个争论的缩影:

tower 代表的哲学是 “严格 + 显式”:

  • &mut self 表达独占。
  • poll_ready 表达背压。
  • 用户写中间件要精确处理每一种状态转移。
  • 错了会 panic(poll_ready 未调用就 call)。

hyper 1.0 代表的哲学是 “宽松 + 易用”:

  • &self 允许并发。
  • 没有 poll_ready——协议层不管业务容量。
  • 用户写 Service 可以直接 async fn call(&self) 一行。
  • 错了也不会 panic,因为 trait 没有这个协议预期。

两者都对——只是在不同的抽象层级应该不同的哲学。

一个程序应该同时受益于两者:

  • 业务中间件层用 tower——享受背压协议、享受组合能力。
  • 连接层用 hyper::Service——享受 HTTP/2 并发、享受 async fn 原生支持。
  • 桥接点(TowerToHyperService)用 clone + poll_ready await 吞掉 tower 背压——这是 unavoidable 的一次牺牲。

理解这一层之后你再读 Axum 代码、reqwest 代码,会知道为什么它们都要在某一处做 “tower → hyper” 的桥接——不是设计失败,而是两种哲学妥协的标志

13.10 HTTP/2 Service 的并发演示

用代码说话。一个支持真正并发的 hyper HTTP/2 server:

use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use hyper::{service::service_fn, Response, Request};
use hyper::body::Incoming;
use http_body_util::Full;
use bytes::Bytes;

#[derive(Clone)]
struct State {
    counter: Arc<AtomicU64>,
}

let state = State { counter: Arc::new(AtomicU64::new(0)) };

let service = service_fn(move |req: Request<Incoming>| {
    let state = state.clone();
    async move {
        let n = state.counter.fetch_add(1, Ordering::Relaxed);
        Ok::<_, hyper::Error>(
            Response::new(Full::new(Bytes::from(format!("Request #{}", n))))
        )
    }
});

看这个 Service:

  • state 里的 Arc<AtomicU64> 可以在多个 stream 并发访问。
  • service_fn(move |req| ...) 构造的 ServiceFn 实现 hyper::Service——call&self,满足并发要求。
  • 闭包 capture 了 state ——clone 进每次 call 的 future(let state = state.clone(); async move { ... })——避免跨 await 借用闭包 self。

如果用 tower::Service 写同样的逻辑,不得不:

  1. 定义一个 struct + Service impl(不能直接用闭包)。
  2. 在 poll_ready 里 return Ready。
  3. Service 必须 Clone——每个 stream 持有 clone。
  4. &mut self 约束下,可变状态必须通过 &mut self 访问——要么 Mutex、要么 atomic。

两套做法在运行时行为上几乎一样——但 hyper::Service 的代码 ergonomics 明显更顺滑,尤其对”service_fn + async fn”这种闭包式写法。

13.11 落到你键盘上

这一章的结论:

  1. hyper::Service 和 tower::Service 差一个字符,但背后是三年讨论。不是随便的风格分歧——是 HTTP/2 多路复用和异步 fn 两个刚需推动的架构决定。
  2. tower 适合业务组合层,hyper 适合协议连接层。中间由 TowerToHyperService 桥接。
  3. HttpService 是 hyper 的 sealed trait——基于 Service + Request/Response 约束自动 blanket 实现。sealed 给 hyper 保留了未来添加 HttpService 方法的权力。
  4. service_fn 让一个闭包直接变 Service——这是 hyper ergonomics 的精华。

落到你键盘上:

  • 读 hyperium/hyper#3040 issue——从头到尾看 Sean McArthur 和其他贡献者如何辩论这个决定。这是一份 Rust 社区里少有的”设计对话”,读完你对工业级 trait 设计的直觉会大幅提升。
  • 写一个比较实验:同一个业务 Service,一次用 tower Service 配 hyper_util::service::TowerToHyperService、一次用 hyper Service 直接写。跑 HTTP/2 benchmark,看两种方式下 p99 延迟和 QPS 差距。
  • 试试在 hyper::Service 实现里做一次 self.counter += 1——编译失败。然后用 AtomicU64 / Mutex 两种方式改对。这是掌握”何时该用 &self 而非 &mut self”最直接的方法。

13.12 源码考证:hyper-1.9.0/src/service/service.rs 112 行的原话

前面章节讲了”设计动机”——打开 hyper-1.9.0/src/service/service.rs:46-55、你会看到 hyper 作者亲笔写的doc comment**、值得一字不漏引**:

/// Process the request and return the response asynchronously.
/// `call` takes `&self` instead of `mut &self` because:
/// - It prepares the way for async fn,
///   since then the future only borrows `&self`, and thus a Service can concurrently handle
///   multiple outstanding requests at once.
/// - It's clearer that Services can likely be cloned.
/// - To share state across clones, you generally need `Arc<Mutex<_>>`
///   That means you're not really using the `&mut self` and could do with a `&self`.
///   The discussion on this is here: <https://github.com/hyperium/hyper/issues/3040>

三条理由的精炼——

  • async fn 的适配——async fn call(&self, req) 返回的 future 只借 &self同一 Service 可以并发处理多个请求
  • Clone 的语义对齐——Service 通常是 Clone 的、用 &self 方法签名更契合 Clone 的心智模型
  • 共享状态反驳 &mut self——如果你需要跨 clone 共享、就得 Arc<Mutex<_>>——外层是 Arc、内部 Mutex 自己管可变性——&mut self 参数只是在骗自己

最后一条——Arc<Mutex<T>> 的 “.lock()” 本身返回 MutexGuard<T> 就能可变借用 T——你根本不需要函数签名 &mut self 才能改数据——hyper 作者看透了这一点、把&mut self 从签名里移除

这三条是 2022 年 8 月 hyperium/hyper#3040 issue 里经过 30+ 轮讨论后凝练的——读者可以去翻那个 issue、看 Sean McArthur 如何反复澄清

13.13 五个 blanket impl——&S / &mut S / Box<S> / Rc<S> / Arc<S>

service.rs:58-111 还有一组 impl Service<Request> for &S / &mut S / Box<S> / Rc<S> / Arc<S> ——五种引用/容器S: Service<Request> 自动 blanket 实现

impl<Request, S: Service<Request> + ?Sized> Service<Request> for std::sync::Arc<S> {
    type Response = S::Response;
    type Error = S::Error;
    type Future = S::Future;
    #[inline]
    fn call(&self, req: Request) -> Self::Future {
        (**self).call(req)
    }
}

为什么要五种——

  • &S / &mut S——函数参数接收引用时能直接当 Service 用
  • Box<S>——trait object Box<dyn Service<...>> 能当具体 Service(否则只能叫”动态派发”)
  • Rc<S>——单线程场景共享 Service(比如 wasm runtime)
  • Arc<S>——多线程共享 Service(最常见)

五个 blanket impl 合起来——让用户写 Arc<dyn Service<Request, Response=R, Error=E, Future=F>> 这种复杂类型也能直接工作——不需要再包一层 wrapper

对比 tower::Service——同样 5 个 impl——两边对齐

13.14 TowerToHyperService:72 行的适配器

hyper 团队不用 tower::Service、但提供官方适配器让两者无缝桥接——hyper-util-0.1.20/src/service/glue.rs 只有 72 行。核心:

pub struct TowerToHyperService<S> {
    service: S,
}

impl<S, R> hyper::service::Service<R> for TowerToHyperService<S>
where S: tower_service::Service<R> + Clone,
{
    fn call(&self, req: R) -> Self::Future {
        TowerToHyperServiceFuture {
            future: Oneshot::new(self.service.clone(), req),
        }
    }
}

两个值得注意的细节——

细节 1:S: ... + Clone 的约束——适配器要求tower service 必须 Clone”——为什么?因为 hyper::Service::call(&self, ...) 不能拿到 &mut S——只能 clone 一份 S 出来再调用它的 poll_ready + call——Clone 是适配的门票

细节 2:Oneshot::new(...)——来自 hyper_util::service::oneshot——封装了一次性调 poll_ready 到 Ready 再 call的状态机——每个请求独立一个 Oneshot——互不干扰

所以 tower→hyper 的转换代价——每次 call 都 clone 一次 Service + 跑一次 Oneshot 状态机——如果 S::Clone 便宜就无所谓、贵就要权衡

这呼应本章§13.3.2 的Clone 不是万能解药”——适配器能工作、但不是免费的

13.15 Oneshot 状态机:单次调用的 poll_ready → call 生命周期

hyper-util-0.1.20/src/service/oneshot.rs 里的 Oneshot<S, Req> 把 tower 的两步(poll_ready + call)压缩成一个 Future

enum State<S, Req, F> {
    NotReady { svc: S, req: Option<Req> },  // 等 poll_ready 完成
    Called { future: F },                    // call 已发出、在等 response
    Done,
}

Future 的 poll 逻辑——

  • 先调 svc.poll_ready(cx)——Pending 则返回 Pending、Ready 则跳下一步
  • svc.call(req) 拿 future、存进 State::Called
  • 以后每次 poll 都 poll 内部 future——Ready 返回结果

这个状态机——把 tower “两步曲” 包装成 hyper “一步曲”——hyper 只需要 call → future 一步、内部细节由 Oneshot 吞掉

Oneshot 的设计来自 tower-util——hyper-util 只是借用过来”——避免重复发明

13.16 Sealed trait HttpService:基于 Service 的 HTTP 专用约束

hyper-1.9.0/src/service/http.rs 65 行——定义了一个 seal 过的 trait HttpService

// 简化示意
pub trait HttpService<B>: sealed::Sealed<B> {
    type ResBody;
    type Error;
    type Future;
    fn call_http(&self, req: Request<B>) -> Self::Future;
}

sealed::Sealed 是一个 trait——只有同 crate的类型能实现——外部用户无法自行 impl HttpService

为什么要 seal——

  • hyper 保留了未来在 HttpService 上添加新方法的权利——不 seal 的话、加方法是 breaking change(所有实现者需要加新方法)
  • 外部用户通过 blanket impl 自动获得 HttpService——所有 impl Service<Request<B>, Response = Response<X>> 的类型都自动是 HttpService
  • hyper 内部只通过 HttpService 调用——用户通过 Service 实现——两层分离

这是 Rust 生态的前向兼容trick——sealed 是保留演进空间的专用语法——本章读者应该掌握

13.17 hyper 内部为什么调 HttpService 而不是 Service

打开 hyper 的 HTTP/2 server 代码(src/proto/h2/server.rs)——内部dispatch body的地方全部调 HttpService::call_http——不调 Service::call

为什么——

  • HttpService::ResBody: Body——约束比 Service::Response 强——HTTP/2 server 内部可以直接 poll body frames
  • 如果只约束 Service::Response = Response<B>——B 是泛型、内部要到处 bound——代码复杂度飙升
  • HttpService 是HTTP 语义子集——让 hyper 内部代码更短、更直接

这是约束内部、放开外部的 API 设计范式——用户写 Service、hyper 内部用 HttpService——两层不冲突、彼此放大

13.18 service_fn 源码:42 行的奇迹

hyper-1.9.0/src/service/util.rsservice_fn 函数——让普通闭包直接变 Service

pub fn service_fn<F, R, S>(f: F) -> ServiceFn<F, R>
where F: Fn(R) -> S, S: Future,
{
    ServiceFn { f, _req: PhantomData }
}

impl<F, ReqBody, Ret, ResBody, E> Service<Request<ReqBody>> for ServiceFn<F, ReqBody>
where
    F: Fn(Request<ReqBody>) -> Ret,
    Ret: Future<Output = Result<Response<ResBody>, E>>,
    E: Into<Box<dyn StdError + Send + Sync>>,
{
    type Response = Response<ResBody>;
    type Error = E;
    type Future = Ret;
    fn call(&self, req: Request<ReqBody>) -> Self::Future {
        (self.f)(req)
    }
}

42 行代码——却是 99% hyper 用户的第一行 Service 代码”——service_fn(|req| async move { ... }) 就能跑一个 HTTP 服务

三个设计亮点——

  • F: Fn 而非 FnMut——&self 对齐——闭包不能修改捕获环境、只能共享
  • 无状态适配——ServiceFn 只有两个字段 f + _req: PhantomData——近乎零开销
  • 类型推断友好——type Future = Ret 让用户写 async closure 时编译器自动推 Future 类型——用户不用手写

这个 service_fn 是 hyper ergonomics 的核心——没有它、hyper 用户会疯掉。

13.19 hyper 1.0 vs 0.14 的 Service 差异

hyper-0.14.32 在同目录下——可以直接对比两代设计

hyper 0.14 的 Service(旧版)——复用了 tower_service::Service——接口和 tower 完全一样——poll_ready + call(&mut self, req)

hyper 1.0 的 Service(新版)——自定义、没 poll_readycall(&self, req)——本章主题

迁移成本——

  • 用户代码tower::Service 换成 hyper::Service——签名变化——需要删掉 poll_ready + 把 &mut self&self
  • 老的 tower middleware 生态——通过 TowerToHyperService 桥接——一行 wrap

这次升级是 hyper 历史上最大的 breaking change——但社区接受度很高——**因为新 API 明显更简洁 + 并发友好

13.20 本章第二次小结

读者读完本章再加本节——对 hyper / tower 关系的理解层次

  • 初级——“hyper 不用 tower::Service、有自己的 Service
  • 中级——“两者差在 &self vs &mut self、**支不支持 poll_ready
  • 高级——“&self 是为了 async fn + 并发 + 共享状态的 Arc<Mutex<_>> 范式
  • 专家——“hyper 作者在 service.rs:46-55 亲笔写的 3 条理由、加 sealed trait HttpService 的前向兼容设计

本章 +§13.12-13.19 的补充——让读者直接跃升到专家

13.21 &self 设计在其他库里的影响

hyper 1.0 这一设计——激发了 Rust 生态里&self 写异步的更广泛讨论

  • axum 0.8——Handler<T, S> 的 trait 借鉴 hyper、用 &self——多路复用友好
  • tonic 0.13——Service 内部仍然用 tower::Service、但 NamedService 开始提供 &self 版本
  • reqwest 0.12——client-side Service&self——单 client 多并发请求不再需要 clone

三个库的改动都在 2023-2024 年发生——hyper 的榜样效应明显——Rust 异步生态在&mut self&self 偏移”。

读者下次选库时可以观察——Service trait 长什么样、是不是 &self——某种程度上反映这个库是不是”2024+ 一代”

13.22 &self 带来的隐性约束

&self 的自由不是没代价的——三个隐性约束

约束 1——Service 不能持有不可 Clone 的可变状态(除非 Arc<Mutex<>>)——比如内部维护一个 state machine 每次 call 转一次状态必须上锁——但锁本身是性能瓶颈

约束 2——失去背压传递——tower 的 poll_ready 能告诉上游我暂时满了、别喂”——hyper::Service 没有——需要在业务层自己实现(比如 semaphore + async 等)。

约束 3——没有容量控制原语——tower 的 Load、Limit、Balance middleware依赖 poll_ready”——hyper 层面用不上——要用 tower middleware 就必须TowerToHyperService 桥接——业务代码两端都得懂

这三条约束——不是 hyper 的 bug、是它的设计权衡——懂得了权衡、你写 HTTP service 就不会掉坑

13.23 实战:给 service_fn请求限流

本章所有抽象——落到一个具体场景如何给 hyper service_fn 加 rate limiting

use std::sync::Arc;
use tokio::sync::Semaphore;

let rate_limit = Arc::new(Semaphore::new(100));  // 最多 100 并发

let service = hyper::service::service_fn(move |req| {
    let rate_limit = rate_limit.clone();
    async move {
        let _permit = rate_limit.acquire().await.unwrap();
        // _permit drop 时自动释放
        handle(req).await
    }
});

拆解——

  • Semaphore 是 tokio 提供的信号量——acquire() 返回 PermitPermit drop 时自动释放
  • Arc<Semaphore> 让多个 call 共享同一个信号量——100 个并发 stream 各自 acquire、超过 100 的排队
  • &self 方法签名 + 闭包 capture Arc——天然配合

10 行代码——替代了 tower 的 ConcurrencyLimit 中间件——hyper 风格

工程结论——&self 不丢失容量控制——只是改用 async/await + Arc 原语实现——和 tower 的 poll_ready 路径不同、效果等价

13.24 跨书呼应:hyper Service ↔ MCP Client ↔ LangChain Runnable

这三个跨书主题在本章汇合——

  • 本章 hyper::Service——async fn(&self, Request) -> Future<Response> —— HTTP 层的计算原语
  • 本书 MCP 第 9 章 Client——async connect + listTools + callTool —— 协议层的计算原语
  • 本书 LangChain 第 2 章 Runnable——invoke / batch / stream —— AI chain 层的计算原语

三者都是异步计算原语的不同层次——都遵循&self + 并发友好 + Clone 共享状态的现代 Rust/Python async 设计哲学

读完这三章——你会发现异步生态的底层哲学是通用的——不是 Rust 独有也不是 Python 独有async-first 计算这件事情本身的特性

13.26 资源速查

  • hyperium/hyper#3040 issue —— 本章引用的 30+ 轮辩论原文
  • hyper-1.9.0/src/service/service.rs:46-55 —— hyper 作者亲笔 doc comment
  • hyper-util-0.1.20/src/service/glue.rs —— 72 行 TowerToHyperService
  • hyper-util-0.1.20/src/service/oneshot.rs —— Oneshot 状态机
  • hyper-1.9.0/src/service/util.rs —— 42 行 service_fn
  • hyper-1.9.0/src/service/http.rs —— sealed HttpService trait

这 6 个资源 + 本章 = hyper 1.0 Service 设计的完整文档

读完、你是懂 hyper 的

13.27 深入:service_fn 的类型推断如何工作

本章§13.18 提了 service_fn 一句”类型推断友好”——值得展开

let svc = service_fn(|req: Request<Incoming>| async move {
    Ok::<_, hyper::Error>(Response::new(Full::new(Bytes::from("hi"))))
});

编译器做了什么——

  • 看到 service_fn(f)、尝试实例化 ServiceFn<F, R>
  • F 是一个闭包、返回一个 async block、async block 的类型是匿名 future
  • 根据 ServiceFn impl Service 的 where Ret: Future<Output = Result<Response<ResBody>, E>> 约束反向推 ResBody 和 E
  • 用户用 Ok::<_, hyper::Error>(Response::new(...)) 部分标注 E = hyper::Error
  • 编译器推出 ResBody = Full<Bytes>(因为 Response::new(Full::new(…)) 构造)
  • 所有类型填完、Service 具体化

结果——用户只标一个 hyper::Error、其他全部自动推——这是 Rust 类型系统的精华

对比 Java 的 lambda——Java 的类型推断浅、函数式接口必须全标——Rust 的类型推断深、让代码近乎script-level简洁

写 hyper service 时——你能用最少的类型标注获得最强的类型安全——这是ergonomics + safety的双赢

13.28 编译器错误的幽默侧面

写 hyper::Service 时常见一条错误——漏标 Error type

error[E0282]: type annotations needed
   let svc = service_fn(|req| async { Ok(Response::new(...)) });
                                       ^^ cannot infer type for type parameter `E`

编译器的要求——你必须告诉我 Ok 里的 E 是什么类型(因为 Result 有两个泛型、E 没出现在值里、推不出来)。

两种修法——

// 方法 1:在 Ok 上标
|req| async move { Ok::<_, hyper::Error>(Response::new(...)) }

// 方法 2:用 turbofish
|req| async move { Result::<_, hyper::Error>::Ok(Response::new(...)) }

新手遇到这个错误常被吓到——其实只是告诉编译器 Error 是啥一行——知道了就 5 秒搞定

13.29 给”入门者”的一次完整的 hyper HTTP/2 server 示例

把本章所有讨论落地——一个最小的 hyper HTTP/2 server

use hyper::{Request, Response};
use hyper::body::{Incoming, Bytes};
use hyper::service::service_fn;
use hyper_util::rt::{TokioExecutor, TokioIo};
use hyper_util::server::conn::auto::Builder;
use http_body_util::Full;
use std::sync::{Arc, atomic::{AtomicU64, Ordering}};
use tokio::net::TcpListener;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let listener = TcpListener::bind("127.0.0.1:3000").await?;
    let counter = Arc::new(AtomicU64::new(0));

    loop {
        let (stream, _) = listener.accept().await?;
        let counter = counter.clone();
        tokio::spawn(async move {
            let svc = service_fn(move |_req: Request<Incoming>| {
                let counter = counter.clone();
                async move {
                    let n = counter.fetch_add(1, Ordering::Relaxed);
                    Ok::<_, hyper::Error>(Response::new(
                        Full::new(Bytes::from(format!("Request #{}", n)))
                    ))
                }
            });
            let _ = Builder::new(TokioExecutor::new())
                .serve_connection(TokioIo::new(stream), svc)
                .await;
        });
    }
}

关键点——

  • service_fn(move |req| async move { ... }) —— 闭包 + async block —— 本章主题
  • Arc<AtomicU64> —— 多 stream 共享可变状态、无 &mut self
  • auto::Builder —— 自动 HTTP/1.1 + HTTP/2 双协议
  • TokioExecutor + TokioIo —— tokio runtime 适配

50 行能跑、生产可用——这就是 hyper 1.0 的 ergonomics

13.30 HTTP/3 的未来——本章设计会被继承

hyper 目前主攻 HTTP/1.1 + HTTP/2——HTTP/3(QUIC)正在演进——h3 crate (hyperium/h3) 是独立仓库。

关键问题——h3 的 Service trait 会继承本章的 &self 设计吗?——**答案是:**会。

  • h3 同样面临”多 stream 并发”问题(QUIC 天然多路复用、比 HTTP/2 更甚)
  • &self + async fn + Arc<Mutex> 的范式在 h3 里同样适用
  • h3 实际引用了 hyper::service::Service trait

所以本章的设计——不只影响 hyper 1.0、也塑造了 Rust HTTP/3 的未来——一个正确的 API 决定会沿着协议演进传递下去。

13.31 hyper HTTP/2 server 的 stream dispatcher 内部

读者好奇”hyper 如何把 hyper::Service 转成真正的 HTTP/2 dispatcher”——打开 hyper-1.9.0/src/proto/h2/server.rs(约 1100 行)、关键结构是 Dispatcher

  • 每个 HTTP/2 connection 对应一个 Dispatcher<S, B>——持有用户的 S: HttpService
  • 每个 incoming stream 触发 self.service.call_http(req)(注意是 &self——多 stream 能并发调)
  • call 返回的 Future 被 tokio::spawn 出去独立 poll——stream 之间完全并发
  • Future 完成后生成的 Response 再通过 h2 stream 发回——frame 级的 flush

为什么 &self 是必要的——Dispatcher 的 poll 循环要一次 poll 多个 stream”——如果 &mut S、同一时刻只能有一个 stream 在调 call——HTTP/2 并发就退化成 HTTP/1.1 串行——本章§13.3 的论点

这 1100 行的 h2 server 是**“&self 设计哲学落地的最重样本——读者可以挑 100 行精读、再回头看 service.rs 的 doc comment——两端印证

13.32 背压问题:hyper 层面的替代方案

本章§13.22 讲 &self 失去了 tower 的 poll_ready 背压——但 hyper 并没有无背压”——它在协议层自己做了

HTTP/2 层——

  • 每个 stream 有自己的 flow-control window(WINDOW_UPDATE frame)
  • 每个 connection 有总 flow-control window
  • 接收方没处理完、不更新 window、发送方自动停——这是协议层背压

hyper 的参数——

  • http2_initial_stream_window_size(默认 64KB)
  • http2_initial_connection_window_size(默认 64KB)
  • http2_max_concurrent_streams(默认 100)

用户通过 Builder 配置——不需要 tower middleware 干预——HTTP/2 协议本身就是背压机制

tower::Service::poll_ready 的存在——是因为 tower 要支持HTTP 之外的协议(gRPC、Kafka、自定义 RPC)——这些协议的背压在业务层需要 poll_ready 原语

hyper 聚焦 HTTP——背压问题已经被协议解决——不需要再引入 poll_ready——这是领域专精的好处

13.33 TokioIoTokioExecutor 的 runtime 适配

§13.29 示例里用了 TokioIo::new(stream)TokioExecutor::new()——这是 hyper-util 的 runtime 适配层

  • hyper 内部用 std::future + 自定义 AsyncRead/AsyncWrite trait
  • tokio 提供的是它自己的 AsyncRead/AsyncWrite
  • 两者签名不同——直接传 tokio 的 stream 给 hyper 编译失败
  • TokioIo<T> 是适配 wrapper——实现 hyper 的 AsyncRead for TokioIo<T where T: tokio::AsyncRead>

为什么 hyper 要自己定义 AsyncRead/Write——因为 std 没统一 async I/O trait——hyper 想保持 runtime 无关——不锁定 tokio

结果——tokio 用户需要 TokioIo wrap、smol 用户需要 SmolIo wrap、async-std 用户需要对应 wrap——10-20 行适配代码

代价是wrap 一次”——收益是任意 runtime 都能跑 hyper”——值得

13.34 Builder::new(executor) 的 executor 抽象

hyper_util::server::conn::auto::Builder::new(executor) 接受的 executor 是**hyper::rt::Executor trait**:

pub trait Executor<Fut> {
    fn execute(&self, fut: Fut);
}

简洁到极致的 trait——只要求能把一个 Future spawn 出去——不要求返回 JoinHandle、不要求 cancel

为什么这么简——hyper 只需要把 task 扔出去”——其他细节 runtime 自己处理——最小 coupling

TokioExecutor impl——

impl<F> Executor<F> for TokioExecutor where F: Future + Send + 'static, F::Output: Send {
    fn execute(&self, fut: F) { tokio::spawn(fut); }
}

三行代码——让 hyper 能用 tokio——再换 runtime 也只需要 3 行——API 设计的典范

13.35 小结:本章覆盖的 17 个知识点

把本章 34 节压缩成 17 个知识点——每一点都能独立考试

  1. hyper::Service 和 tower::Service 只差两处(§13.1)
  2. &self vs &mut self 是 HTTP/2 多路复用的分水岭(§13.3)
  3. 没有 poll_ready 不是缺陷、是协议层面已解决(§13.32)
  4. TowerToHyperService 适配器要求 Service: Clone(§13.14)
  5. Oneshot 状态机把 tower 两步压缩成一步(§13.15)
  6. sealed trait HttpService 保留前向兼容权(§13.16)
  7. hyper 内部调 HttpService、用户实现 Service、两层分离(§13.17)
  8. service_fn 42 行是 99% 用户的第一行 Service 代码(§13.18)
  9. hyper 1.0 vs 0.14 的 API 差异(§13.19)
  10. axum/tonic/reqwest 跟进 &self 设计(§13.21)
  11. Arc<AtomicU64> / Arc<Mutex> 是共享状态的范式(§13.23)
  12. 类型推断”只标 Error type”(§13.27)
  13. HTTP/2 flow-control 是内置背压(§13.32)
  14. TokioIo / TokioExecutor 是 runtime 适配层(§13.33-13.34)
  15. Executor trait 只有 1 个方法(§13.34)
  16. HTTP/3 会继承 &self 设计(§13.30)
  17. 50 行能跑一个生产级 hyper server(§13.29)

能把这 17 个知识点的来源 + 工程意义讲出来**——你是 hyper 1.0 API 设计的内行”。

13.37 深度 bonus:Future type 的选择——为什么 Service 用关联类型而不是 impl Future

本章多次提到 type Future: Future<...>——Rust 语言层面还有另一个选项fn call(&self, req: Request) -> impl Future<Output = ...> (Rust 1.75+ 的 return-position impl trait in trait, 简称 RPITIT)。

hyper 为什么坚持关联类型——三条理由:

理由 1——MSRV(Minimum Supported Rust Version)——hyper 支持到 Rust 1.63(2022 年版本)——RPITIT 要 Rust 1.75——直接上新语法会把 2+ 年版本的 Rust 用户甩开

理由 2——类型显式——type Future = Ret 让用户在 impl Service 时明确写出我这个 Service 的 Future 是 Ret”——方便调试、方便添加额外约束where S::Future: Send)。

理由 3——Box<dyn Service> 兼容性——trait object 化要求关联类型有明确 Output——impl Future 在 trait object 里是类型未定” **的——会 break 动态派发

所以关联类型看起来老派、实际承载了**“兼容 + 显式 + trait object 友好三重职责——未来几年也不会换

13.38 拓展:BoxCloneService动态类型擦除

tower 里有 tower::util::BoxCloneService<Req, Res, Err>——让 Service 可以在 runtime 被 type-erased

let svc: BoxCloneService<Request, Response, Error> = BoxCloneService::new(my_svc);
// 放进 Vec / HashMap / channel 都行、不用写泛型

hyper 有对应的 hyper-util 版本吗——hyper_util::service::BoxCloneService——但 less commonly used

为什么 hyper 场景下不常用——

  • hyper 的 Service 往往包在 connection dispatcher 里——dispatcher 持有具体类型、不需要 erase
  • 性能敏感——dyn trait + Pin<Box<dyn Future>> 有额外开销
  • hyper 用户偏一个 Service 跑到底”——不太需要动态切换

tower 场景更常见——router 要 dispatch 到不同 handler、每个 handler 是不同 Service 类型、统一 erase 成 BoxCloneService 才能放一起——axum 的 Router 内部就这么做

13.40 Pin + Future 的隐性约束

type Future: Future<...> 背后还有一个被许多读者忽略的细节:Future 通常需要 UnpinPin<Box<...>>

原因——

  • std::future::Future::poll(self: Pin<&mut Self>, cx) 的签名要求 future 是 Pin
  • 如果 F: !Unpin(比如 async fn 生成的匿名 future)——必须 Pin<Box<F>>
  • hyper 内部调度需要 F: Send + 'static——跨线程 spawn

典型错误——用户把一个持有非 Send 字段的 future 作为 Service::Future——编译失败错误信息极长(几十行 trait bound mismatch)。

破解——

  • Future 里尽量只持有 Send 类型(大多数类型默认 Send)
  • 持有 Rc<T> / RefCell<T> 会破坏 Send——换成 Arc<T> / Mutex<T>
  • 如果实在要 non-Send future——hyper 在 single-thread runtime(如 LocalSet)下能用 Executor<F> where F: Future 不要求 Send 的 trait 版本

这条在 hyper 的常见 FAQ 里排前三——为什么我的 Service 不能 spawn——90% 是 Send 约束挂了

13.41 附录:hyper 的三种 serve 入口对比

hyper 1.0 的服务端有三种起手式——各有定位

入口场景支持协议特点
hyper::server::conn::http1::Builder只要 HTTP/1HTTP/1.1轻量、适合内嵌
hyper::server::conn::http2::Builder只要 HTTP/2HTTP/2TLS 友好、多路复用
hyper_util::server::conn::auto::Builder通用HTTP/1 + HTTP/2自动协商、生产推荐

auto::Builder 的魔法——读 TCP stream 的前几个字节——是 HTTP/2 preface (PRI * HTTP/2.0...) 就走 h2否则走 http1——透明协商、无需 TLS ALPN也能走 ALPN、两条路都有)。

生产选择——99% 选 auto::Builder——除非你有明确理由(内嵌场景 + 节省二进制体积)。

13.42 附录:错误处理的五层设计

hyper 1.0 的 Service 错误处理分五层——每层都由Service::Error + 协议层 + runtime 层共同决定

层 1——Service::Error——用户业务错误——变成 HTTP response 的 500 或具体 error body

层 2——hyper body 层——读 Request body 时的 I/O 错误(client 断线、超时)——hyper 提供 hyper::body::Incoming 的 error 类型

层 3——hyper 协议层——HTTP/2 frame 解析错HTTP/1 解析错——hyper 自动发 RST_STREAM 或关连接

层 4——hyper runtime adapter 层——TokioIo 封装的 I/O 错——kernel 级别的 ECONNRESET 等

层 5——连接 teardown 层——accept failure、TLS handshake failure——用户需要在 accept loop 里 catch

五层各有处理范式——混在一起就是生产事故——分清楚才能写出稳定 server

13.44 Service<Request<B>> 里的 B 是什么

本章各处 Service<Request<Incoming>> 的泛型 Incoming 常常被忽略——值得解释

  • Request<B> 是 http crate 的类型——B 是 body 类型
  • server 场景——B = hyper::body::Incoming——hyper 从 wire 解析出来的 body stream
  • client 场景——B 是用户的 Full<Bytes> / StreamBody<...>——要发出去的 body

为什么要泛型——body 的类型多样(空 body、固定字节、流式字节、websocket frames 等)——hyper 不写死让用户选

对新手——server 永远先写 Request<hyper::body::Incoming>——response 永远先写 Response<Full<Bytes>>——两边固定、学习曲线平缓

进阶——想让 body 流式——response body 用 StreamBody<...>BodyExt::map_frame(...) wrap——配合 http-body-util crate

13.45 http-body-util 的角色

http-body-util crate 提供 Body 的常用组合子——不是 hyper 主 crate、但几乎所有 hyper 服务都要用

  • Full<Bytes> —— 一次性全量 body200 OK 回个字符串)
  • Empty<Bytes> —— 空 body204 No Content
  • StreamBody<S> —— Stream<Item = Frame<Bytes>> 转成 Body(流式响应)
  • BodyExt::collect() —— 把 Body 收集成 Collected<Bytes>(请求完整读取)
  • BodyExt::map_frame() —— 对 body 里每个 frame 做变换

为什么要拆一个 crate——hyper 只负责 HTTP protocol + 连接管理——body 组合子是数据流工具”——分离关注点——升级 body 工具不用重建 hyper

5 个组合子够覆盖 90% 场景——剩下 10% 可以自己 impl Body

13.47 回顾:tower::Service 和 hyper::Service 的共生

本章开头说”两者都对、只是服务不同场景”——末尾要再强调一次它们不是竞争、是合作

  • 你的业务逻辑——继续写 tower::Service——获得 retry / rate-limit / timeout / trace 等 middleware 生态
  • 你的 HTTP 边界——在最外层包一层 TowerToHyperService——让 hyper 接住
  • 中间层——hyper_util::service::ServiceBuilderExt + tower 生态——链式组合

典型生产部署——

// tower 世界做业务组合
let tower_svc = ServiceBuilder::new()
    .layer(TraceLayer::new_for_http())
    .layer(TimeoutLayer::new(Duration::from_secs(30)))
    .layer(RateLimitLayer::new(100, Duration::from_secs(1)))
    .service(my_business_svc);

// 桥接到 hyper
let hyper_svc = TowerToHyperService::new(tower_svc);

// 让 hyper 跑
Builder::new(TokioExecutor::new())
    .serve_connection(TokioIo::new(stream), hyper_svc)
    .await;

5 行代码——你同时享受 tower middleware 生态 + hyper HTTP/2 多路复用——两全其美

本章所有讨论——最终落到如何让两者合作”——不是选哪个的单选题

13.48 Q&A 汇总

把散落在各节的常见疑问集中回答——

  • &mut tower Service 能直接给 hyper 用吗? 不能——必须通过 TowerToHyperService + S: Clone 约束。
  • hyper 不支持 poll_ready、怎么做限流?call 内部用 Arc<Semaphore> acquire(§13.23)。
  • Service 里状态为什么要 Arc<Mutex> &self 要求共享访问,不能 &mut self 直接改(§13.12)。
  • HTTP/2 多路复用的单机上限? 默认 100 个 stream,通过 http2_max_concurrent_streams 调。
  • 客户端也用 hyper::Service 吗? 是。hyper client 也基于 hyper::Service,只是方向相反(发 request 而不是回 response)。
  • sealed 的 HttpService 普通用户能实现吗? trait 声明可见、但无法手动实现——通过 blanket impl 自动获得。
  • service_fn 的性能开销? 近乎零。ServiceFn 只多一个 PhantomData
  • 不用 hyper-util、直接用 hyper 可以吗? 可以,但要自己写 Executor impl + IO 适配,麻烦。
  • hyper 1.0 会再次 breaking change 吗? 长期内不会。1.0 是 stable 承诺,API 冻结;未来演进走 hyper-util 或新增 method。

13.49 &self 设计与 Rust 异步未来

2024-2026 年 Rust 社区对 async trait 的讨论仍在演进——RPITIT、AFIT(async fn in trait)、dyn-compatible async trait 是三条主线。hyper::Service&self + 关联类型 Future 是当前 MSRV 窗口里最优方案,但未来可能变化:

  • Rust 1.85+ 普及后,Service::call 签名可能从手写 type Future 改为 async fn(&self, req: Request) -> Result<...>——内部仍是 &self,但签名更直观。
  • dyn-compatible async trait 解决后,Box<dyn Service<...>> 不再需要 type Future = BoxFuture<...>——类型擦除更简洁。

语法可能变、&self 的设计内核不变。

13.50 hyper Service 和 axum Handler 的层次化

axum 建在 hyper + tower 之上,但 axum 的 Handler 既不 extend hyper::Service 也不 extend tower::Service——而是自定义了 Handler<T, S> trait:

pub trait Handler<T, S>: Clone + Send + Sized + 'static {
    type Future: Future<Output = Response> + Send + 'static;
    fn call(self, req: Request, state: S) -> Self::Future;
}

注意 call(self, ...) own self——比 &self 更自由,因为 axum 用 “一次调用 + 大量 clone” 的模式。之所以不直接复用 Service,是因为 Service 的 Request 泛型和 axum 的 Extractor 模式不契合——Handler 通过 IntoService trait 转换到 Service。

三层分工:hyper Service(协议层) → tower Service(组合层) → axum Handler(人机交互层)。每一层解决不同问题、维持独立抽象,彼此可互转。第 22 章会完整读 axum 的 routing + extractor 实现。

下一章讲 HTTP/1 连接的生存线——keep-alive 的超时策略、半关闭、header read timeout——这些生产环境里 debug 最频繁的边界。