跳转到正文
zeno's blog
返回

hyper(三):与 axum、tower 的架构关系

专题: hyper

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, 事件循环              │
└─────────────────────────────────────────────────┘

每层只做自己的事, 不越界:

职责不做什么
hyperHTTP 协议实现不做路由、不做中间件、不做类型提取
tower中间件组合框架不知道 HTTP 是什么、不做协议解析
tower-httpHTTP 专用中间件不做路由、不绑定具体框架
axumWeb 框架 (路由 + 提取)不实现 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 内部做了:

  1. Router 转换为 tower Service
  2. 为每个连接创建 service 实例
  3. 用 hyper 的 serve_connection 处理 HTTP 协议
  4. 自动处理 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 的内部实现:

5.3 axum::serve 隐藏了这个复杂性

当使用 axum::serve(listener, router) 时, axum 内部自动处理了:

  1. Router → tower::Service
  2. tower::Service → hyper::service::Service (通过适配)
  3. 绑定到 hyper 的 connection handler

开发者通常不需要接触 TowerToHyperService, 除非直接使用 hyper.

6. 不同场景下的技术选择

场景直接用 hyperhyper + toweraxum
极致性能 (代理/网关)适合适合可以
REST API太底层可以最佳
gRPC不直接用 (tonic)不直接用不直接用 (tonic)
WebSocket可以可以最佳 (axum 内置)
自定义协议适合适合不适合
学习 HTTP 原理最佳-太高层

6.1 什么时候直接用 hyper

6.2 什么时候用 axum

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 版本变化):

axumhypertowerhttphttp-body
0.6.x0.140.40.20.4
0.7.x1.x0.4-0.51.x1.x
0.8.x1.x0.51.x1.x

8. Pitfalls

8.1 hyper 版本与 axum 版本不匹配

axum 0.7+ 依赖 hyper 1.x. 如果项目中同时有依赖 hyper 0.14 的 crate, 会出现两个不兼容的 http crate 版本 (0.2 vs 1.0), 导致 RequestResponse 类型不兼容.

诊断方法:

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::IncomingSend, 但如果在 handler 中将其转换为 Stream 并跨 .await 持有 borrow, 可能触发 Future is not Send 错误. 解决: 在 .await 前 collect body 或用 tokio::spawn 前确保所有 borrow 释放.


分享这篇文章:

上一篇
tower(一):Service trait-异步函数抽象与背压信号
下一篇
hyper(二):Body trait 与请求响应体