Hyper 与 Tower:工业级 HTTP 栈
第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-0.5.3/tower-service/src/lib.rs:322(省略文档注释)
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-1.9.0/src/service/service.rs:32-57(省略文档注释)
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 团队经过两轮讨论:先是 issue #2853,由 PR #2920(2022-09-09 合入,随 1.0.0-rc.1 发布)引入自己的 Service 并去掉 poll_ready;再是 issue #3040,由 PR #3223(2023-05-15 合入,随 1.0.0-rc.4 发布)把 call 改成 &self。最终决定不在 hyper 1.0 里用 tower::Service——而是定义了自己的版本。
这一章我们把这个决定的背后完整展开——不是"谁对谁错",而是"为什么两者都对,只是服务不同场景"。读完你对 Rust 类型系统和协议设计的理解会更深一层。
13.2 先看结论:两个 trait 的分工
一句话:
- tower::Service 面向业务组合层——它要支持中间件堆叠、背压传递、容量控制。
poll_ready是它的灵魂。- hyper::Service 面向协议连接层——它只管把请求交出去、拿回 future,并为将来的 async fn 铺路。
&self是它的灵魂。
中间由 hyper_util::service::TowerToHyperService 适配器桥接。任何实现了 Clone 的 tower Service<Request> 都能变成 hyper 能接的 hyper::Service<Request>——只是在桥接点,tower 的 poll_ready 被延后到每个请求的 future 里等待(第 4 章 §4.11)。
要理解这个分工为什么是对的,我们先从一个常见误解讲起。
13.3 常见误解:&mut self 会让 HTTP/2 串行吗
想象一个 HTTP/2 server 的场景:
- 一条 TCP 连接上复用着 50 个 stream(hyper server 的
max_concurrent_streams默认 200,见hyper-1.9.0/src/proto/h2/server.rs:68)。 - 每个 stream 是一个独立的 HTTP 请求-响应。
- server 侧收到
DATAframe 时,要立刻路由到对应 stream 的 Request 处理器。 - 这些处理器同时进行——一个 stream 在处理 100KB 的 body upload,另一个 stream 在等数据库查询,它们应该并发。
所以 server 需要让同一个 Service 同时有多个请求在处理中——这就是 HTTP/2 多路复用的本质。
13.3.1 &mut self 并不强制串行
现在看 tower::Service::call(&mut self, req)。&mut self 意味着"独占访问"——但独占只持续到 call 这次同步调用返回为止。返回值是关联类型 Self::Future,签名里不带 self 的生命周期,future 并不借用 self。call 一返回,下一个请求就可以接着 call,两个 future 各自独立推进。
实测:一个 &mut self 的 tower Service,每次 call 返回一个 sleep 100ms 的 future;连续 call 两次、再用 tokio::join! 同时等两个 future,总耗时约 101ms,而不是 200ms。
hyper v0.14 就是现成的例子:它的 Service 直接复用 tower_service::Service(&mut self + poll_ready),HTTP/2 server 在连接任务里逐个接收新 stream,service.call(req) 拿到 future 后交给 executor 另起任务(exec.execute_h2stream(fut),见 git show v0.14.32:src/proto/h2/server.rs 第 290-336 行),各 stream 照样并发。
所以"&mut self 让 HTTP/2 退化成串行、server 一次只能处理一个请求"并不成立。串行的只是"调用 call 那一瞬间",而这一步本来就在连接任务里逐个发生。
13.3.2 &mut self 真正的别扭之处
既然 &mut self 不妨碍并发,hyper 为什么还要改?hyper-1.9.0/src/service/service.rs:48-55 的注释给了三条理由:
- 为 async fn 铺路:如果将来
call写成async fn,返回的 future 会借用self。借的若是&mut self,这个 future 完成前谁都不能再call,那时才会真正串行;借的是&self,多个未完成的 future 可以同时借用同一个 Service。 - Service 通常可以 Clone:
&self让这一点更清楚。 - 跨 clone 共享状态本来就要
Arc<Mutex<_>>:既然可变性由Mutex自己管,&mut self实际没被用上,&self就够了。
§13.12 会逐字读这段注释。
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 能否并发的前提(v0.14 的 &mut self 照样并发),而是为 async fn 和共享状态做的接口选择。
13.4 poll_ready 在连接层能回答什么
Tower 的 poll_ready 是整个 backpressure 协议的灵魂(第 4 章完整讲过)。但把它放到 HTTP 连接层,能回答的问题很有限。
13.4.1 "这个连接能不能接新请求?"
Tower Service 的 poll_ready 在问"这个服务能不能接新请求"。把它套到 hyper 的 Connection 上,问题变成:"这条 HTTP 连接能不能接新请求"?
HTTP/1 上这个问题的答案首先由协议决定——一条连接一次只处理一个请求,上一个没处理完就不读下一个。
HTTP/2 上每个 stream 独立,连接能接多少个 stream 由 MAX_CONCURRENT_STREAMS 限定,那是 h2 层面的事,不是 Service 层面。
hyper v0.14 确实在这两处问过 poll_ready(下一节看代码),但它能表达的只是"这条连接暂时别再收新请求"。
用户真正关心的背压——"CPU 满了、数据库连接池满了、上游服务满了"——这些全是业务层问题,应该由业务代码里的 ConcurrencyLimit / Buffer 等中间件处理,不应该由连接层的 Service::poll_ready 回答。
13.4.2 v0.14 的连接层 poll_ready 做了什么
hyper v0.14 的 HTTP/2 server 在每次接收新 stream 之前问一次 Service:
// git show v0.14.32:src/proto/h2/server.rs 第 258-290 行(省略错误分支细节)
// Check that the service is ready to accept a new request.
//
// - If not, just drive the connection some.
// - If ready, try to accept a new request from the connection.
match service.poll_ready(cx) {
Poll::Ready(Ok(())) => (),
Poll::Pending => {
// use `poll_closed` instead of `poll_accept`,
// in order to avoid accepting a request.
ready!(self.conn.poll_closed(cx).map_err(crate::Error::new_h2))?;
trace!("incoming connection complete");
return Poll::Ready(Ok(()));
}
Poll::Ready(Err(err)) => { /* 按错误原因 graceful_shutdown 或 abrupt_shutdown */ }
}
// When the service is ready, accepts an incoming request.
match ready!(self.conn.poll_accept(cx)) {
HTTP/1 dispatcher 也类似:读下一个请求头之前先问 dispatch.poll_ready,而 server 侧的实现在没有未完成请求时转去问 self.service.poll_ready(cx)(git show v0.14.32:src/proto/h1/dispatch.rs 第 242 行、第 533-541 行)。
也就是说,v0.14 的连接层并不是"没法用" poll_ready,而是只把它当成一个"要不要再收新请求"的开关:不 ready 时只推进连接、不 accept 新 stream。已经接进来的请求照常处理,业务容量仍要靠业务层的中间件管。
去掉这个开关的是 PR #2920(commit fee7d361,2022-09-09,关闭 issue #2853)。提交说明写的是 "This removes the dependency on tower-service, and simplifies the Service trait to be used by hyper's server connections.",BREAKING CHANGE 一栏写 "The poll_ready method has been removed.";diff 删掉的正是上面这段 h2 代码,h1 的 poll_ready 也改成只看自己手上有没有未完成的请求。
至于 hyperium/hyper#3040,它讨论的不是 poll_ready,而是 call 该取 &self 还是 &mut self(§13.12)。
13.4.3 hyper 的决定:干脆不要
hyper 团队最终决定:把 poll_ready 从 hyper::Service 里彻底去掉。
不是不需要背压——而是把背压责任留给业务 Service。hyper 连接层只是把请求原样搬到 Service,Service 自己要不要做背压、怎么做、跟中间件怎么配合——hyper 不管。
这个决定让 hyper 的协议层代码简化——协议层不再调用用户 Service 的 poll_ready:h1 dispatcher 内部还有一个同名的 Dispatch::poll_ready(hyper-1.9.0/src/proto/h1/dispatch.rs:41),但 server 侧实现只看自己是否还有未完成的请求(:569-575);h2 server 接到新 stream 就直接 call。hyper 只做协议,业务用 tower 在 hyper 外面做背压——责任分离得干净。
13.5 源码对比:hyper::Service 的极简
// hyper-1.9.0/src/service/service.rs:32-57(省略部分文档注释)
pub trait Service<Request> {
type Response;
/// Errors produced by the service.
///
/// Note: Returning an `Error` to a hyper server, the behavior depends on the
/// protocol. In most cases, hyper will cause the connection to be abruptly aborted.
/// It will abort the request however the protocol allows, either with some sort of RST_STREAM,
/// or killing the connection if that doesn't exist.
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-1.9.0/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-1.9.0/src/service/http.rs:20-38(省略文档注释)
pub trait HttpService<ReqBody>: sealed::Sealed<ReqBody> {
type ResBody: Body;
// 注:源码 :26-28 的文档给了一条很实在的建议:往 hyper server 返 Error,多数情况下
// 连接会被直接掐断——更好的做法是返回一个带 4xx/5xx 状态码的 Response。
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)
}
}
// :56-65
impl<T, B1, B2> sealed::Sealed<B1> for T
where T: Service<Request<B1>, Response = Response<B2>>, B2: Body,
{
}
mod sealed {
pub trait Sealed<T> {}
}
这段代码演示了一个绝妙的设计技巧: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-1.9.0/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_fn 要求 F: Fn:call 手里只有 &self,只能通过 &F 调用闭包。
这又一次印证 &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 逆变:如果 B 是 A 的子类型(Rust 里的子类型关系来自生命周期,例如 &'static str 是 &'a str 的子类型),那么消费 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 仓库 src/service/glue.rs:20-46(省略文档注释与 new)
pub struct TowerToHyperService<S> {
service: S,
}
impl<S, R> hyper::service::Service<R> for TowerToHyperService<S>
where
S: tower_service::Service<R> + Clone,
{
type Response = S::Response;
type Error = S::Error;
type Future = TowerToHyperServiceFuture<S, R>;
fn call(&self, req: R) -> Self::Future {
TowerToHyperServiceFuture {
future: Oneshot::new(self.service.clone(), req),
}
}
}
逻辑:
- 进入
call(&self, req)——hyper 的 trait 要求&self。 - 因为要调 tower 的
&mut self+poll_ready,clone 一份 service。 - 返回的 future 是一个
Oneshot(§13.15)——被 poll 时先poll_ready再call。
代价很明显:每次请求 clone 一次 tower Service。对 Axum 这种 Service 本质是 Arc<Inner> 的场景,clone 是 Arc::clone——一次原子计数、无分配。对其他场景——视具体 Service 而定。
背压被延后:tower 栈的 poll_ready 没有被丢掉,而是延后到每个请求的 Oneshot future 里等待——任何 Pending 变成这个请求 future 内部的异步等待,对 hyper 连接层不可见。hyper 在接收请求之前没法提前知道 tower 栈是否 ready——这是 hyper::Service 不带 poll_ready 的自然结果(第 4 章 §4.11)。
这是 hyper 1.0 分裂的一个已知代价(另一个是上面说的每请求一次 clone)——但它只发生在桥接点(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,契约允许实现 panic)。
hyper 1.0 代表的哲学是 "宽松 + 易用":
&self允许多个未完成请求共享同一个 Service。- 没有 poll_ready——协议层不管业务容量。
- 为 async fn 铺路(但当前 trait 有关联类型
Future,impl 里还不能直接写async fn call,会报 E0053)。 - 错了也不会 panic,因为 trait 没有这个协议预期。
两者都对——只是在不同的抽象层级应该不同的哲学。
一个程序应该同时受益于两者:
- 业务中间件层用 tower——享受背压协议、享受组合能力。
- 连接层用 hyper::Service——接口最小、
&self便于共享状态。 - 桥接点(
TowerToHyperService)用 clone +Oneshot把 tower 的poll_ready延后到每个请求的 future 里——这是跨过两套 trait 时避不开的代价。
理解这一层之后你再读 Axum、Tonic 的代码,会知道为什么它们都要在某一处做 "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,要么用
tower::service_fn(闭包是FnMut)。 - 手写时 poll_ready 里 return Ready(
tower::service_fn的 poll_ready 恒 Ready)。 - 要接进 hyper 1.x,Service 必须 Clone,并经
TowerToHyperService桥接——每个请求 clone 一份。 &mut self下虽然能直接改字段,但每个 clone 各改各的——要跨请求共享,仍然要 Mutex 或 atomic。
两套做法在运行时行为上几乎一样——但 hyper::Service 的代码 ergonomics 明显更顺滑,尤其对"service_fn + async fn"这种闭包式写法。
13.11 落到你键盘上
这一章的结论:
- hyper::Service 和 tower::Service 只差两处,但背后是 #2853、#3040 两轮讨论。不是随便的风格分歧——去掉
poll_ready是为了简化连接层,改成&self是为 async 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:47-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 从签名里移除。
这三条是 hyperium/hyper#3040 issue 讨论后凝练的、由 PR #3223(commit d894439e)写进源码、随 1.0.0-rc.4 发布(CHANGELOG:"change Service::call to take &self")——读者可以去翻那个 issue、看 Sean McArthur 如何反复澄清。
13.13 五个 blanket impl——&S / &mut S / Box<S> / Rc<S> / Arc<S>
hyper-1.9.0/src/service/service.rs:59-112 还有一组 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<...>>能当具体 ServiceRc<S>——单线程场景共享 Service(比如 wasm runtime)Arc<S>——多线程共享 Service(最常见)
五个 blanket impl 合起来——让用户写 Arc<dyn Service<Request, Response=R, Error=E, Future=F>> 这种复杂类型也能直接工作——不需要再包一层 wrapper。
对比 tower-service 0.3——只有 &mut S 和 Box<S> 两个 blanket impl(tower-0.5.3/tower-service/src/lib.rs:369-401)——因为 call(&mut self) 无法透过 &S / Rc<S> / Arc<S> 转发。
13.14 TowerToHyperService:72 行的适配器
hyper 团队不用 tower::Service、但提供官方适配器让两者无缝桥接——hyper-util 仓库的 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 内部的 Oneshot(hyper-util 仓库 src/service/oneshot.rs,只以 pub(crate) 导出)——封装了"一次性调 poll_ready 到 Ready 再 call"的状态机——每个请求独立一个 Oneshot——互不干扰。
所以 tower→hyper 的转换代价——每次 call 都 clone 一次 Service + 跑一次 Oneshot 状态机——如果 S::Clone 便宜就无所谓、贵就要权衡。
这呼应本章§13.8 说的代价——适配器能工作、但不是免费的。
13.15 Oneshot 状态机:单次调用的 poll_ready → call 生命周期
hyper-util 仓库 src/service/oneshot.rs 里的 Oneshot<S, Req> 把 tower 的两步(poll_ready + call)压缩成一个 Future:
// hyper-util 仓库 src/service/oneshot.rs:13-23(pin_project! 内,省略属性)
pub enum Oneshot<S: Service<Req>, Req> {
NotReady { svc: S, req: Option<Req> }, // 等 poll_ready 完成
Called { #[pin] fut: S::Future }, // call 已发出、在等 response
Done,
}
Future 的 poll 逻辑——
- 先调
svc.poll_ready(cx)——Pending 则返回 Pending、Ready 则跳下一步 svc.call(req)拿 future、切到Oneshot::Called- 以后每次 poll 都 poll 内部 future——Ready 返回结果
这个状态机——把 tower "两步曲" 包装成 hyper "一步曲"——hyper 只需要 call → future 一步、内部细节由 Oneshot 封装。
Oneshot 的代码来自 tower::util——源码注释写明 "Vendored from tower::util to reduce dependencies"——hyper-util 只是抄过来一份、避免整个依赖 tower。
13.16 Sealed trait HttpService:基于 Service 的 HTTP 专用约束
hyper-1.9.0/src/service/http.rs 65 行——定义了一个 seal 过的 trait HttpService:
// 简化示意(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;
}
注意这里的 call 是 &mut self、还带 #[doc(hidden)]——它只给 hyper 内部用,blanket impl 里转发到 Service::call(&self, ..)。
sealed::Sealed 是私有模块里的 trait——外部 crate 写不出它的 impl(它只由 hyper 自己的 blanket impl 覆盖所有满足约束的 Service)——外部用户无法自行 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 代码(hyper-1.9.0/src/proto/h2/server.rs:306)——内部派发请求的地方写的是 service.call(req),但 S 的 bound 是 HttpService<IncomingBody>,走的是 HttpService::call——不直接约束 Service。
为什么——
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 源码:33 行的奇迹
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(Request<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,
ReqBody: Body,
Ret: Future<Output = Result<Response<ResBody>, E>>,
E: Into<Box<dyn StdError + Send + Sync>>,
ResBody: Body,
{
type Response = Response<ResBody>;
type Error = E;
type Future = Ret;
fn call(&self, req: Request<ReqBody>) -> Self::Future {
(self.f)(req)
}
}
33 行代码(util.rs:30-62)——却是绝大多数 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 的核心——没有它,每个 handler 都得手写一个实现 Service 的结构体。
13.19 hyper 1.0 vs 0.14 的 Service 差异
对照 hyper 0.14 系列(git tag v0.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 去掉了 poll_ready、call 改为 &self,接口更小。
13.20 本章第二次小结
读者读完本章再加本节——对 hyper / tower 关系的理解层次:
- 初级——"hyper 不用 tower::Service、有自己的 Service"
- 中级——"两者差在
&selfvs&mut self、支不支持poll_ready" - 高级——"
&self是为了 async fn + 并发 + 共享状态的Arc<Mutex<_>>范式" - 专家——"hyper 作者在
hyper-1.9.0/src/service/service.rs:47-55亲笔写的 3 条理由、加 sealed trait HttpService 的前向兼容设计"
§13.12-13.19 补的就是"专家"这一层。
13.21 &self 设计在其他库里的影响
hyper 1.0 这一设计对下游的实际影响、比"大家都改成 &self"要克制得多:
- axum——
Router仍然实现tower::Service;接到 hyper 1.x 上时用 hyper-util 的TowerToHyperService(或等价的 clone + oneshot 逻辑)桥接。它自己的Handler<T, S>::call是self按值(§13.45)。 - tonic——
Server/ 生成的 gRPC service 仍是tower::Service,同样经TowerToHyperService进 hyper(第 22 章 §22.4.1 读 tonic 的 import、§22.4.2 读这座桥)。 - hyper-util 的 legacy
Client——实现的是tower_service::Service<Request<B>>(hyper-util 仓库src/client/legacy/client.rs:669),不是hyper::Service。
也就是说 &self 目前只在 hyper 的协议边界上生效——tower 世界照旧、桥在 hyper-util。读者下次接一个 tower Service 到 hyper 时——记住要 Clone、记住 poll_ready 会在 Oneshot 里被 await。
13.22 &self 带来的隐性约束
&self 的自由不是没代价的——三个隐性约束:
约束 1——Service 里的可变状态只能靠内部可变性(Mutex、原子类型等)——比如"内部维护一个 state machine 每次 call 转一次状态"必须上锁——锁可能成为争用点。
约束 2——连接层拿不到提前的背压信号——tower 的 poll_ready 能在请求进来之前告诉上游"我暂时满了、别喂"——hyper::Service 没有——背压只能延后到每个请求的 future 里(TowerToHyperService 的 Oneshot,或自己用 semaphore + async 实现,§13.23)。
约束 3——没有容量控制原语——tower 的 Load、Limit、Balance middleware"依赖 poll_ready"——hyper 层面用不上——要用 tower middleware 就必须TowerToHyperService 桥接——业务代码两端都得懂。
这三条约束——不是 hyper 的 bug、是它的设计权衡——懂得了权衡、你写 HTTP service 就不会掉坑。
13.23 实战:给 service_fn 加请求限流
本章所有抽象——落到一个具体场景:如何给 hyper service_fn 加并发上限?
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 // handle:你自己的 async fn(Request<Incoming>) -> Result<Response<_>, _>
}
});
拆解——
- 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 路径不同、效果接近——区别是请求已经被 hyper 接收,等待发生在请求 future 里,这和 TowerToHyperService 把 poll_ready 延后到 Oneshot 里是同一回事。
13.24 跨专栏呼应:hyper Service ↔ MCP Client ↔ LangChain Runnable
这三个"跨专栏主题"在本章汇合——
- 本章 hyper::Service——
call(&self, Request) -> Self::Future—— HTTP 层的计算原语 - 《MCP 协议设计与实现》专栏第 9 章的 TypeScript Client——
async connect + listTools + callTool—— 协议层的计算原语 - 《LangChain 设计与实现》专栏第 2 章的 Runnable——
invoke / batch / stream—— AI chain 层的计算原语
三者都是"异步计算原语"的不同层次——都把一次调用建模成可以并发发起的异步操作,状态靠共享而不是独占来管理(Rust 里落到 &self + Arc,TypeScript / Python 没有借用规则,靠对象引用共享)。
读完这三章——你会发现异步生态的底层哲学是通用的——不是 Rust 独有、也不是 Python 独有、是"async-first 计算"这件事情本身的特性。
13.25 资源速查
- hyperium/hyper#3040 issue —— 本章引用的辩论原文
hyper-1.9.0/src/service/service.rs:47-55—— hyper 作者亲笔 doc comment- hyper-util 仓库
src/service/glue.rs—— 72 行 TowerToHyperService - hyper-util 仓库
src/service/oneshot.rs—— Oneshot 状态机 hyper-1.9.0/src/service/util.rs—— service_fn(30-62 行)hyper-1.9.0/src/service/http.rs—— sealed HttpService trait
这 6 份材料都是一手来源,配合本章可以把 hyper 1.0 Service 的设计来龙去脉对照着读完。
13.26 深入: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
- async block 的输出类型由块内的表达式决定,再与
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 类型系统的精华。
写 hyper service 时——你能用最少的类型标注获得最强的类型安全——这是"ergonomics + safety"的双赢。
13.27 编译器错误的幽默侧面
写 hyper::Service 时常见一条错误——漏标 Error type:
error[E0282]: type annotations needed
|
5 | let svc = service_fn(|req: Request<Incoming>| async { Ok(Response::new(Full::new(Bytes::from("hi")))) });
| ^^ cannot infer type of the type parameter `E` declared on the enum `Result`
编译器的要求——你必须告诉我 Ok 里的 E 是什么类型(因为 Result 有两个泛型、E 没出现在值里、推不出来)。
两种修法——
// 方法 1:在 Ok 上标
|req| async move { Ok::<_, hyper::Error>(Response::new(...)) }
// 方法 2:在 Result 上标
|req| async move { Result::<_, hyper::Error>::Ok(Response::new(...)) }
新手遇到这个错误常被吓到——其实只是"告诉编译器 Error 是啥"一行——知道了就 5 秒搞定。
13.28 给"入门者"的一次完整的 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 适配
三十几行能跑——这就是 hyper 1.0 的 ergonomics。
13.29 HTTP/3 的未来——本章设计能否被继承
hyper 目前主攻 HTTP/1.1 + HTTP/2——HTTP/3(QUIC)正在演进——h3 crate (hyperium/h3) 是独立仓库。
关键问题——HTTP/3 接入 hyper 时会继承本章的 &self 设计吗?
- h3 crate 本身不依赖 hyper、也没有 Service trait——它只提供连接 / stream 级 API
- h3 同样面临"多 stream 并发"问题(QUIC 天然多路复用、比 HTTP/2 更甚)——
&self+ async fn + Arc<Mutex> 的范式在那里同样适用 - hyper 若在 1.x 内集成 HTTP/3,能复用的正是
hyper::service::Service这层抽象——协议换了、Service 不用换
所以本章的设计——不只服务 HTTP/1 与 HTTP/2、也给 HTTP/3 留好了位置。
13.30 hyper HTTP/2 server 的 stream dispatcher 内部
读者好奇"hyper 如何把 hyper::Service 转成真正的 HTTP/2 dispatcher"——打开 hyper-1.9.0/src/proto/h2/server.rs(550 行)、关键结构是 Server<T, S, B, E> 与它内部的 Serving:
- 每个 HTTP/2 connection 对应一个
Server<T, S, B, E>——持有用户的S: HttpService<IncomingBody> - 每个 incoming stream 触发
service.call(req)(server.rs:306;经HttpService::call转到Service::call(&self, ..)——多 stream 能并发调) - call 返回的 Future 被
exec.execute_h2stream(fut)交给Executorspawn 出去独立 poll(tokio 下就是tokio::spawn)——stream 之间完全并发 - Future 完成后生成的 Response 再通过 h2 stream 发回——frame 级的 flush
并发从哪来——来自每个 stream 的 future 被单独 spawn,而不是 &self 本身——call 在连接任务里逐个同步调用、立刻返回 future;v0.14 用 &mut self 走的是同一条路,各 stream 一样并发(§13.3.1)。&self 的意义在于为 async fn 和共享状态铺路(§13.3.2)。
这 550 行的 h2 server 是"协议层只管 call、并发交给 executor"的最重样本——读者可以挑 100 行精读、再回头看 service.rs 的 doc comment——两端印证。
13.31 背压问题:hyper 层面的替代方案
本章§13.22 讲 &self 失去了 tower 的 poll_ready 背压——但 hyper 并没有"无背压"——它在协议层自己做了:
HTTP/2 层——
- 每个 stream 有自己的 flow-control window(WINDOW_UPDATE frame)
- 每个 connection 有总 flow-control window
- 接收方没处理完、不更新 window、发送方自动停——这是协议层背压
hyper 的参数——
http2::Builder::initial_stream_window_size(hyper 默认 1 MB,hyper-1.9.0/src/proto/h2/server.rs:37;RFC 9113 的协议默认才是 65,535 字节)http2::Builder::initial_connection_window_size(hyper 默认 1 MB,hyper-1.9.0/src/proto/h2/server.rs:36)http2::Builder::max_concurrent_streams(hyper 默认 200,hyper-1.9.0/src/proto/h2/server.rs:68)
用户通过 Builder 配置——不需要 tower middleware 干预——HTTP/2 协议本身就带一层背压机制。
但它管的是"字节"和"单连接上的 stream 数"——flow-control window 限制在途的 body 字节,max_concurrent_streams 限制一条连接上同时打开的请求数;它回答不了"数据库连接池满了"这类业务容量问题,也管不到多条连接加起来的总量。HTTP/1 更没有这层机制。
所以业务容量仍要在业务层管——用 tower 的 ConcurrencyLimit / Buffer 等中间件(经 TowerToHyperService 接入),或像 §13.23 那样在 call 里用 semaphore——hyper 只负责协议层的那一部分。
13.32 TokioIo 和 TokioExecutor 的 runtime 适配
§13.28 示例里用了 TokioIo::new(stream) 和 TokioExecutor::new()——这是 hyper-util 的 runtime 适配层:
- hyper 内部用
std::future+ 自己的hyper::rt::Read/hyper::rt::Writetrait - tokio 提供的是它自己的
AsyncRead/AsyncWrite - 两者签名不同——直接传 tokio 的 stream 给 hyper 编译失败
TokioIo<T>是适配 wrapper——为TokioIo<T>实现hyper::rt::Read(当T: tokio::io::AsyncRead)、反向也实现tokio::io::AsyncRead(当T: hyper::rt::Read)
为什么 hyper 要自己定义 Read/Write——因为 std 没统一 async I/O trait——hyper 想保持 runtime 无关——不锁定 tokio。
结果——tokio 用户需要 TokioIo wrap、其他 runtime 的用户需要自己写同样的 wrapper——几十行适配代码。
代价是"wrap 一次"——收益是"任意 runtime 都能跑 hyper"——值得。
13.33 Builder::new(executor) 的 executor 抽象
hyper_util::server::conn::auto::Builder::new(executor) 接受的 executor 是 hyper::rt::Executor trait:
// hyper-1.9.0/src/rt/mod.rs:45-48(省略文档注释)
pub trait Executor<Fut> {
fn execute(&self, fut: Fut);
}
简洁到极致的 trait——只要求能把一个 Future spawn 出去——不要求返回 JoinHandle、不要求 cancel。
为什么这么简——hyper 只需要"把 task 扔出去"——其他细节 runtime 自己处理——最小 coupling。
TokioExecutor impl——
// hyper-util 仓库 src/rt/tokio.rs:105-117(略去 tracing cfg 分支)
impl<Fut> Executor<Fut> for TokioExecutor
where
Fut: Future + Send + 'static,
Fut::Output: Send + 'static,
{
fn execute(&self, fut: Fut) {
tokio::spawn(fut);
}
}
十来行代码——让 hyper 能用 tokio——再换 runtime 也只需要这么多——API 设计的典范。
13.34 小结:本章覆盖的 17 个知识点
把本章前面各节压缩成 17 个知识点——每一点都能独立考试:
- hyper::Service 和 tower::Service 只差两处(§13.1)
&mut self并不让 HTTP/2 串行;&self是为 async fn 和共享状态铺路(§13.3)- 没有
poll_ready:协议层只管协议,业务容量交给业务层(§13.4、§13.31) - TowerToHyperService 适配器要求 Service: Clone(§13.14)
- Oneshot 状态机把 tower 两步压缩成一步(§13.15)
- sealed trait HttpService 保留前向兼容权(§13.16)
- hyper 内部调 HttpService、用户实现 Service、两层分离(§13.17)
- service_fn 33 行是绝大多数用户的第一行 Service 代码(§13.18)
- hyper 1.0 vs 0.14 的 API 差异(§13.19)
- axum/tonic 仍是 tower::Service、经 TowerToHyperService 接入 hyper(§13.21)
- Arc<AtomicU64> / Arc<Mutex> 是共享状态的范式(§13.10、§13.12)
- 类型推断"只标 Error type"(§13.26)
- HTTP/2 flow-control 是协议层内置背压,但管不到业务容量(§13.31)
- TokioIo / TokioExecutor 是 runtime 适配层(§13.32-13.33)
- Executor trait 只有 1 个方法(§13.33)
- HTTP/3 若接入 hyper,可以复用同一个 Service 抽象(§13.29)
- 三十几行能跑一个 hyper server(§13.28)
能把这 17 个知识点的来源 + 工程意义讲出来——你是 hyper 1.0 API 设计的"内行"。
13.35 深度 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——直接上新语法会把 1.63–1.74 这一年多的版本用户甩开。
理由 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 友好"三重职责——在当前 MSRV 窗口里没有换掉它的动力(§13.44 会说:等 MSRV 抬高、dyn-compatible async trait 落地之后,这个签名是可能变的——变的是语法,不变的是 &self 的设计内核)。
13.36 拓展: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 0.1.20 的 service 模块只有 TowerToHyperService(及其 TowerToHyperServiceFuture)和内部的 Oneshot(hyper-util 仓库 src/service/mod.rs:24-32)。要擦除 hyper::Service,可以利用它对 Box<S> 的 blanket impl 自己写 Box<dyn Service<Req, Response = R, Error = E, Future = Pin<Box<dyn Future<Output = ...> + Send>>> + Send + Sync>。
为什么 hyper 场景下不常用——
- hyper 的 Service 往往包在 connection dispatcher 里——dispatcher 持有具体类型、不需要 erase
- 性能敏感——
dyn trait + Pin<Box<dyn Future>>有额外开销 - hyper 用户偏"一个 Service 跑到底"——不太需要动态切换
tower 场景更常见——router 要 dispatch 到不同 handler、每个 handler 是不同 Service 类型、统一 erase 才能放一起——axum 的 Route 就是一层 BoxCloneSyncService(BoxCloneService 的 Sync 版本,axum-full/axum/src/routing/route.rs:31)。
13.37 Pin + Future 的隐性约束
type Future: Future<...> 背后还有一个被许多读者忽略的细节:Pin 和 Send。
原因——
std::future::Future::poll(self: Pin<&mut Self>, cx)的签名要求 future 是Pin的- 但
Service::Future不需要Unpin——pin 由 hyper 负责:h1 dispatcher 把在途 future 放进Pin<Box<Option<S::Future>>>(hyper-1.9.0/src/proto/h1/dispatch.rs:49),h2 则把它交给 executor;所以service_fn直接返回 async block(!Unpin)也没问题 Send + 'static来自 executor:HTTP/2 要把每个 stream 的 future 交给 executor spawn,用TokioExecutor时它要求Fut: Send + 'static(hyper-util 仓库src/rt/tokio.rs:105-109)——跨线程 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 用户最常撞到的编译错误之一——"为什么我的 Service 不能 spawn"——多半是 Send 约束挂了。
13.38 附录: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。auto::Builder 本身不看 ALPN;如果 TLS 层已经用 ALPN 协商好了协议,可以直接用 http1_only() / http2_only() 固定版本(hyper-util 仓库 src/server/conn/auto/mod.rs:124-136)。
生产选择——多数场景选 auto::Builder——除非你有明确理由(内嵌场景 + 节省二进制体积)。
13.39 附录:错误处理的五层设计
hyper 1.0 的 Service 错误处理分五层——每层都由"Service::Error + 协议层 + runtime 层"共同决定:
层 1——Service::Error——用户业务错误——hyper 不会把它变成 500:HTTP/1 直接结束连接(hyper-1.9.0/src/proto/h1/dispatch.rs:337),HTTP/2 reset 这个 stream(hyper-1.9.0/src/proto/h2/server.rs:459-462)——想返回 500 或具体 error body,要在 Service 里自己构造 Response
层 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.40 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、固定字节、流式字节等)——hyper 不写死、让用户选。
对新手——server 永远先写 Request<hyper::body::Incoming>——response 永远先写 Response<Full<Bytes>>——两边固定、学习曲线平缓。
进阶——想让 body 流式——response body 用 StreamBody<...> 或 BodyExt::map_frame(...) wrap——配合 http-body-util crate。
13.41 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。
这几个组合子覆盖了大多数场景——剩下的可以自己 impl Body。
13.42 回顾:tower::Service 和 hyper::Service 的共生
本章开头说"两者都对、只是服务不同场景"——末尾要再强调一次:它们不是竞争、是合作:
- 你的业务逻辑——继续写 tower::Service——获得 retry / rate-limit / timeout / trace 等 middleware 生态
- 你的 HTTP 边界——在最外层包一层
TowerToHyperService——让 hyper 接住 - 中间层——
tower::ServiceBuilder+ tower-http 的ServiceBuilderExt——链式组合
典型生产部署——
// 依赖:tower 开 buffer / limit 特性,tower-http 开 trace / timeout 特性
use std::time::Duration;
use http::StatusCode;
use hyper_util::rt::{TokioExecutor, TokioIo};
use hyper_util::server::conn::auto::Builder;
use hyper_util::service::TowerToHyperService;
use tower::{buffer::BufferLayer, limit::RateLimitLayer, ServiceBuilder};
use tower_http::{timeout::TimeoutLayer, trace::TraceLayer};
// tower 世界做业务组合(my_business_svc: Service<Request<Incoming>>)
let tower_svc = ServiceBuilder::new()
.layer(TraceLayer::new_for_http())
.layer(TimeoutLayer::with_status_code(StatusCode::REQUEST_TIMEOUT, Duration::from_secs(30)))
.layer(BufferLayer::new(1024)) // RateLimit 不是 Clone,前面垫一层 Buffer
.layer(RateLimitLayer::new(100, Duration::from_secs(1)))
.service(my_business_svc);
// 桥接到 hyper
let hyper_svc = TowerToHyperService::new(tower_svc);
// 让 hyper 跑(stream 来自 accept 循环)
Builder::new(TokioExecutor::new())
.serve_connection(TokioIo::new(stream), hyper_svc)
.await?;
注意中间那层 BufferLayer:TowerToHyperService 要求整个栈 Clone(§13.14),而 tower 的 RateLimit 没有实现 Clone(tower-0.5.3/tower/src/limit/rate/service.rs:12 只 derive 了 Debug),直接拼会报 E0277。垫一层 Buffer 之后,每个请求 clone 的只是 Buffer 的发送端,所有请求共用同一个 RateLimit,限速才是全局的。tower-http 的 TimeoutLayer::new 从 0.6.7 起已标记 deprecated,这里用 with_status_code。
几行代码——你同时享受 tower middleware 生态 + hyper HTTP/2 多路复用——两全其美。
本章所有讨论——最终落到"如何让两者合作"——不是"选哪个"的单选题。
13.43 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 多路复用的单连接上限? hyper server 默认 200 个 stream,通过
http2::Builder::max_concurrent_streams调(auto Builder 是.http2().max_concurrent_streams(..))。 - 客户端也用
hyper::Service吗? 不。hyper 1.x 的client::conn::http1::SendRequest走的是自己的一组固有方法(poll_ready/ready/is_ready/is_closed/send_request/try_send_request,见hyper-1.9.0/src/client/conn/http1.rs:143-221),没有impl Service for SendRequest;hyper-util 的 legacyClient实现的是tower_service::Service<Request<B>>。 - sealed 的
HttpService普通用户能实现吗? trait 声明可见、但无法手动实现——通过 blanket impl 自动获得。 service_fn的性能开销? 近乎零。ServiceFn只多一个PhantomData。- 不用 hyper-util、直接用 hyper 可以吗? 可以,但要自己写
Executorimpl + IO 适配,麻烦。 - hyper 1.0 会再次 breaking change 吗? 1.x 内不会——这是 1.0 的 semver 承诺;新能力走 hyper-util 或新增 method。
13.44 &self 设计与 Rust 异步未来
Rust 的 async trait 仍在演进——RPITIT 和 AFIT(async fn in trait)已在 Rust 1.75 稳定,dyn-compatible async trait 还在推进。hyper::Service 的 &self + 关联类型 Future 是当前 MSRV 窗口里最优方案,但未来可能变化:
- 若将来发布 hyper 2.0、MSRV 抬过 1.75,
Service::call签名可能从手写type Future改为async fn(&self, req: Request) -> Result<...>——内部仍是&self,但签名更直观。 - dyn-compatible async trait 解决后,
Box<dyn Service<...>>不再需要type Future = BoxFuture<...>——类型擦除更简洁。
语法可能变、&self 的设计内核不变。
13.45 hyper Service 和 axum Handler 的层次化
axum 建在 hyper + tower 之上,但 axum 的 Handler 既不 extend hyper::Service 也不 extend tower::Service——而是自定义了 Handler<T, S> trait:
// axum-full/axum/src/handler/mod.rs:148-153(省略文档注释与 layer / with_state 等方法;见《Axum 设计与实现》专栏第 5 章)
pub trait Handler<T, S>: Clone + Send + Sync + 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 通过 Handler::with_state / into_service 得到的 HandlerService 转换到 tower::Service。
三层分工:hyper Service(协议层) → tower Service(组合层) → axum Handler(人机交互层)。每一层解决不同问题、维持独立抽象,彼此可互转。第 22 章会完整读 axum 的 routing + extractor 实现。
下一章讲 HTTP/1 连接的生存线——keep-alive 的超时策略、半关闭、header read timeout——这些生产环境里 debug 最频繁的边界。