Hyper 与 Tower:工业级 HTTP 栈
第13章 hyper 的 Service trait:为什么 1.0 不复用 tower::Service
第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;
}
只有两处差别:
&mut selfvs&self(call 方法)- 有
poll_readyvs 没有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 侧收到
DATAframe 时,要立刻路由到对应 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:让 &S、Box<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:
- 公开 trait
A,要求A: SealedA。 - SealedA 在 private module,用户看不见。
- 给所有”合法实现 A 的类型” blanket impl SealedA——因此用户看不到也写不出自己的实现。
- 这意味着 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: A、B: 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
})
}
}
逻辑:
- 进入
call(&self, req)——hyper 的 trait 要求&self。 - 因为要调 tower 的
&mut self+poll_ready,clone 一份 service。 - 在返回的 future 里,先
poll_ready再call。
代价很明显:每次请求 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 写同样的逻辑,不得不:
- 定义一个 struct + Service impl(不能直接用闭包)。
- 在 poll_ready 里 return Ready。
- Service 必须 Clone——每个 stream 持有 clone。
&mut self约束下,可变状态必须通过&mut self访问——要么 Mutex、要么 atomic。
两套做法在运行时行为上几乎一样——但 hyper::Service 的代码 ergonomics 明显更顺滑,尤其对”service_fn + async fn”这种闭包式写法。
13.11 落到你键盘上
这一章的结论:
- hyper::Service 和 tower::Service 差一个字符,但背后是三年讨论。不是随便的风格分歧——是 HTTP/2 多路复用和异步 fn 两个刚需推动的架构决定。
- tower 适合业务组合层,hyper 适合协议连接层。中间由
TowerToHyperService桥接。 - HttpService 是 hyper 的 sealed trait——基于 Service + Request/Response 约束自动 blanket 实现。sealed 给 hyper 保留了未来添加 HttpService 方法的权力。
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 objectBox<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.rs 的 service_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_ready、call(&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”
- 中级——“两者差在
&selfvs&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() 返回 Permit、Permit 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 commenthyper-util-0.1.20/src/service/glue.rs—— 72 行 TowerToHyperServicehyper-util-0.1.20/src/service/oneshot.rs—— Oneshot 状态机hyper-1.9.0/src/service/util.rs—— 42 行 service_fnhyper-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
- 根据
ServiceFnimpl 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 selfauto::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::Servicetrait
所以本章的设计——不只影响 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 TokioIo 和 TokioExecutor 的 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 个知识点——每一点都能独立考试:
- hyper::Service 和 tower::Service 只差两处(§13.1)
&selfvs&mut self是 HTTP/2 多路复用的分水岭(§13.3)- 没有
poll_ready不是缺陷、是协议层面已解决(§13.32) - TowerToHyperService 适配器要求 Service: Clone(§13.14)
- Oneshot 状态机把 tower 两步压缩成一步(§13.15)
- sealed trait HttpService 保留前向兼容权(§13.16)
- hyper 内部调 HttpService、用户实现 Service、两层分离(§13.17)
- service_fn 42 行是 99% 用户的第一行 Service 代码(§13.18)
- hyper 1.0 vs 0.14 的 API 差异(§13.19)
- axum/tonic/reqwest 跟进
&self设计(§13.21) - Arc<AtomicU64> / Arc<Mutex> 是共享状态的范式(§13.23)
- 类型推断”只标 Error type”(§13.27)
- HTTP/2 flow-control 是内置背压(§13.32)
- TokioIo / TokioExecutor 是 runtime 适配层(§13.33-13.34)
- Executor trait 只有 1 个方法(§13.34)
- HTTP/3 会继承
&self设计(§13.30) - 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 通常需要 Unpin 或 Pin<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/1 | HTTP/1.1 | 轻量、适合内嵌 |
hyper::server::conn::http2::Builder | 只要 HTTP/2 | HTTP/2 | TLS 友好、多路复用 |
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>—— 一次性全量 body(200 OK回个字符串)Empty<Bytes>—— 空 body(204 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 汇总
把散落在各节的常见疑问集中回答——
&muttower 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 可以吗? 可以,但要自己写
Executorimpl + 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 最频繁的边界。