Table of contents
Open Table of contents
TL;DR
hyper 处理 HTTP 协议 (字节流 ↔ Request/Response), tower 提供中间件组合框架 (Service + Layer), axum 在两者之上添加路由和类型安全的请求提取. 三者的分工是: hyper 管”线路上的字节”, tower 管”请求的处理流水线”, axum 管”开发者体验”. 理解这个三层架构是在 Rust 中写 HTTP 服务的基础.
1. 三层架构全景
┌─────────────────────────────────────────────────┐
│ 应用代码 │
│ handlers, extractors, state │
├─────────────────────────────────────────────────┤
│ axum │
│ Router, Handler trait, IntoResponse, │
│ Extract (Path, Query, Json, State...) │
├─────────────────────────────────────────────────┤
│ tower + tower-http │
│ Service trait, Layer, ServiceBuilder, │
│ Compression, CORS, Trace, Timeout... │
├─────────────────────────────────────────────────┤
│ hyper │
│ HTTP/1.1 + HTTP/2 编解码, Connection, │
│ Body (Incoming), serve_connection │
├─────────────────────────────────────────────────┤
│ tokio + TCP │
│ TcpListener, TcpStream, 事件循环 │
└─────────────────────────────────────────────────┘
每层只做自己的事, 不越界:
| 层 | 职责 | 不做什么 |
|---|---|---|
| hyper | HTTP 协议实现 | 不做路由、不做中间件、不做类型提取 |
| tower | 中间件组合框架 | 不知道 HTTP 是什么、不做协议解析 |
| tower-http | HTTP 专用中间件 | 不做路由、不绑定具体框架 |
| axum | Web 框架 (路由 + 提取) | 不实现 HTTP 协议、不重复 tower 的中间件 |
2. hyper → axum: 如何组装
2.1 不用 axum 的纯 hyper + tower 服务
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::body::Incoming;
use hyper::{Request, Response};
use http_body_util::Full;
use bytes::Bytes;
use hyper_util::rt::TokioIo;
use std::convert::Infallible;
use std::net::SocketAddr;
async fn handler(
req: Request<Incoming>,
) -> Result<Response<Full<Bytes>>, Infallible> {
// 手动路由
match (req.method(), req.uri().path()) {
(&hyper::Method::GET, "/") => {
Ok(Response::new(Full::new(Bytes::from("Hello!"))))
}
(&hyper::Method::GET, "/health") => {
Ok(Response::new(Full::new(Bytes::from("OK"))))
}
_ => {
let mut resp = Response::new(Full::new(Bytes::from("Not Found")));
*resp.status_mut() = hyper::StatusCode::NOT_FOUND;
Ok(resp)
}
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
let listener = tokio::net::TcpListener::bind(addr).await?;
loop {
let (stream, _) = listener.accept().await?;
let io = TokioIo::new(stream);
tokio::spawn(async move {
if let Err(err) = http1::Builder::new()
.serve_connection(io, service_fn(handler))
.await
{
eprintln!("Error: {err:?}");
}
});
}
}
问题: 路由要手写 match, 没有参数提取, 没有中间件组合. 这就是”用汇编写程序”的感觉.
2.2 用 axum 的等价代码
use axum::{Router, routing::get};
async fn hello() -> &'static str {
"Hello!"
}
async fn health() -> &'static str {
"OK"
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(hello))
.route("/health", get(health));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
.await
.unwrap();
axum::serve(listener, app).await.unwrap();
}
axum 内部做了:
- 将
Router转换为 towerService - 为每个连接创建 service 实例
- 用 hyper 的
serve_connection处理 HTTP 协议 - 自动处理 HTTP/1 和 HTTP/2 协商
2.3 axum::serve 的内部实现 (概念)
// axum::serve 的概念性实现 (简化)
pub async fn serve(listener: TcpListener, app: Router) -> Result<(), Error> {
loop {
let (stream, _addr) = listener.accept().await?;
let io = TokioIo::new(stream);
let service = app.clone();
tokio::spawn(async move {
// axum 使用 hyper-util 的 auto Builder
hyper_util::server::conn::auto::Builder::new(TokioExecutor::new())
.serve_connection(
io,
TowerToHyperService::new(service),
)
.await
});
}
}
3. axum 如何使用 tower 中间件
3.1 axum 没有自己的中间件系统
axum 文档明确说:
axum doesn’t have its own middleware system but instead uses
tower::Service. This means axum gets timeouts, tracing, compression, authorization, and more, for free.
这意味着任何 tower 或 tower-http 的中间件都直接可用:
use axum::{Router, routing::get};
use tower::ServiceBuilder;
use tower_http::{
compression::CompressionLayer,
cors::CorsLayer,
trace::TraceLayer,
timeout::TimeoutLayer,
};
use std::time::Duration;
let app = Router::new()
.route("/", get(handler))
.layer(
ServiceBuilder::new()
.layer(TraceLayer::new_for_http())
.layer(CorsLayer::permissive())
.layer(TimeoutLayer::new(Duration::from_secs(30)))
.layer(CompressionLayer::new())
);
3.2 Router 本身是 Service
axum::Router 实现了 tower::Service<Request>, 这就是为什么 tower 中间件能直接包装它:
// Router 的核心 trait 实现 (概念)
impl Service<Request<Body>> for Router {
type Response = Response<Body>;
type Error = Infallible;
type Future = /* ... */;
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(())) // Router 永远就绪
}
fn call(&mut self, req: Request<Body>) -> Self::Future {
// 路由匹配 → 调用 handler → 返回 Response
}
}
3.3 中间件作用域
axum 支持将中间件应用到特定路由:
let api_routes = Router::new()
.route("/users", get(list_users))
.route("/users/:id", get(get_user))
.layer(AuthLayer::new()); // 只对 API 路由生效
let app = Router::new()
.route("/", get(home)) // 不需要认证
.nest("/api", api_routes) // /api/* 需要认证
.layer(TraceLayer::new_for_http()); // 全局生效
4. Body 类型在三层中的流转
4.1 请求方向 (入站)
TCP bytes
↓ hyper 解析
Request<hyper::body::Incoming>
↓ axum 内部适配
Request<axum::body::Body>
↓ Extractor 消费
handler(Json<T>, Path<P>, ...)
axum 的 Extractor (如 Json<T>) 在内部消费 body 来提取数据.
4.2 响应方向 (出站)
handler 返回 impl IntoResponse
↓ axum 的 IntoResponse trait
Response<axum::body::Body>
↓ hyper 发送
TCP bytes
4.3 axum 的 Body 类型
axum 使用自己的 body 类型别名, 底层是 http-body-util 的 BoxBody:
// axum::body::Body 的核心设计:
// 类型擦除, 统一所有 handler 返回的不同 body 类型
// axum handler 可以返回 String, &str, Json<T>, Vec<u8> 等
// 它们都通过 IntoResponse trait 转换为统一的 Body 类型
这种类型擦除是 axum 的设计选择: 牺牲一点性能 (vtable 调用) 换取极大的 API 易用性. 开发者不需要关心具体的 body 类型.
5. 两种 Service trait 的桥接
5.1 问题
hyper 1.0 使用 hyper::service::Service (&self, 无 poll_ready), tower 使用 tower::Service (&mut self, 有 poll_ready). axum 的 Router 实现的是 tower 的 Service. 要将它传给 hyper 的 serve_connection, 需要适配.
5.2 TowerToHyperService 适配器
use hyper_util::service::TowerToHyperService;
// tower::Service → hyper::service::Service
let tower_service: MyTowerService = /* ... */;
let hyper_service = TowerToHyperService::new(tower_service);
// 现在可以传给 hyper 的 serve_connection
http1::Builder::new()
.serve_connection(io, hyper_service)
.await?;
TowerToHyperService 的内部实现:
- 在
call(&self, req)中 clone 内部的 tower service - 对 clone 出的实例调用
poll_ready+call - 这要求 tower service 实现
Clone
5.3 axum::serve 隐藏了这个复杂性
当使用 axum::serve(listener, router) 时, axum 内部自动处理了:
- Router → tower::Service
- tower::Service → hyper::service::Service (通过适配)
- 绑定到 hyper 的 connection handler
开发者通常不需要接触 TowerToHyperService, 除非直接使用 hyper.
6. 不同场景下的技术选择
| 场景 | 直接用 hyper | hyper + tower | axum |
|---|---|---|---|
| 极致性能 (代理/网关) | 适合 | 适合 | 可以 |
| REST API | 太底层 | 可以 | 最佳 |
| gRPC | 不直接用 (tonic) | 不直接用 | 不直接用 (tonic) |
| WebSocket | 可以 | 可以 | 最佳 (axum 内置) |
| 自定义协议 | 适合 | 适合 | 不适合 |
| 学习 HTTP 原理 | 最佳 | - | 太高层 |
6.1 什么时候直接用 hyper
- 构建 HTTP 代理/负载均衡器 (需要连接级控制)
- 实现非标准 HTTP 行为 (如 CONNECT 隧道)
- 性能极致优化 (避免框架抽象开销)
- 嵌入到其他系统 (如 WASM 环境)
6.2 什么时候用 axum
- 几乎所有 Web 应用
- REST API
- 需要路由、参数提取、中间件的场景
- 团队协作 (axum 的类型安全减少 bug)
7. Crate 依赖关系图
axum
├── hyper (HTTP 协议)
├── tower (Service + Layer)
├── tower-http (HTTP 中间件)
├── http (Request, Response, StatusCode...)
├── http-body (Body trait)
├── http-body-util (Full, Empty, BoxBody...)
├── hyper-util (TokioIo, TowerToHyperService...)
├── tokio (异步运行时)
├── bytes (Bytes, Buf)
└── matchit (路由匹配)
版本对应关系 (需验证, 随 axum 版本变化):
| axum | hyper | tower | http | http-body |
|---|---|---|---|---|
| 0.6.x | 0.14 | 0.4 | 0.2 | 0.4 |
| 0.7.x | 1.x | 0.4-0.5 | 1.x | 1.x |
| 0.8.x | 1.x | 0.5 | 1.x | 1.x |
8. Pitfalls
8.1 hyper 版本与 axum 版本不匹配
axum 0.7+ 依赖 hyper 1.x. 如果项目中同时有依赖 hyper 0.14 的 crate, 会出现两个不兼容的 http crate 版本 (0.2 vs 1.0), 导致 Request 和 Response 类型不兼容.
诊断方法:
cargo tree -d # 查看重复依赖
cargo tree -i http # 查看 http crate 被谁依赖
8.2 tower::Service 的 Clone 要求
当通过 TowerToHyperService 桥接时, tower Service 必须实现 Clone, 因为 hyper 对每个请求 clone 一份. 如果 Service 持有非 Clone 的状态, 需要用 Arc 包装或使用 Buffer.
8.3 axum handler 的 body 消费
axum 的 extractor 消费 request body. 如果多个 extractor 都需要 body (如 Json<T> 和 String), 只有最后一个 extractor 能消费 body, 其他会得到空 body.
// 错误: body 被 Json 消费后, raw_body 拿到空内容
async fn handler(Json(data): Json<Value>, raw_body: String) { ... }
// 正确: 只使用一个消费 body 的 extractor
async fn handler(Json(data): Json<Value>) { ... }
8.4 中间件顺序在 axum 中的反直觉行为
axum 的 .layer() 遵循 tower 的洋葱模型, 但在嵌套路由 (.nest()) 中, 中间件的应用范围可能不符合直觉:
// 注意: 嵌套路由上的 layer 先于外部 layer 执行
let app = Router::new()
.nest("/api", api_routes.layer(inner_middleware)) // inner 先执行
.layer(outer_middleware); // outer 后执行
8.5 hyper Incoming body 的 Send 问题
hyper::body::Incoming 是 Send, 但如果在 handler 中将其转换为 Stream 并跨 .await 持有 borrow, 可能触发 Future is not Send 错误. 解决: 在 .await 前 collect body 或用 tokio::spawn 前确保所有 borrow 释放.