使用 Axum 构建高性能异步 Web 服务

文章目录

使用 Axum 构建高性能异步 Web 服务

在 Rust Web 生态中,Axum 凭借其简洁的 API、强大的类型安全、完善的异步支持以及与 Tokio 生态的无缝集成,成为近年来最受欢迎的 Web 框架之一。与 Actix-web、Warp 等框架相比,Axum 以无宏入侵、模块化设计、易上手为核心优势,既适合新手快速入门,也能满足生产级项目的高性能需求。

第一个 Axum 服务

项目初始化与依赖配置

首先创建一个新的 Rust 项目,并添加 Axum 及相关依赖:

shell 复制代码
cargo new axum-demo
cd axum-demo

修改 Cargo.toml 文件,添加核心依赖:

toml 复制代码
[dependencies]
axum = { version = "0.8", features = ["full"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tracing = { version = "0.1", features = ["log"] }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

编写 Hello World 服务

修改 src/main.rs 中的内容,实现一个简单的 HTTP 服务,监听 3000 端口,根路由返回 "Hello, Axum!":

rust 复制代码
use anyhow::Result;
use axum::{Router, routing::get};

// 根路由处理函数:异步函数,返回字符串(自动实现 IntoResponse)
async fn root_handler() -> &'static str {
    "Hello, Axum!"
}

#[tokio::main]
async fn main() -> Result<()> {
    tracing_subscriber::fmt::init();

    // 构建路由:将 GET 请求 / 映射到 root_handler
    let app = Router::new().route("/", get(root_handler));

    // 启动服务器
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
    axum::serve(listener, app).await?;

    Ok(())
}

运行与测试

在终端执行以下命令运行项目:

shell 复制代码
cargo run

运行成功后,打开浏览器访问 http://localhost:3000,即可看到 "Hello, Axum!" 的响应。也可以使用 curl 命令测试:

shell 复制代码
curl http://localhost:3000

Axum 核心组件

Axum 的核心设计围绕路由(Router)、提取器(Extractor)、响应(Response)、状态(State)、中间件(Middleware)这五大组件展开的,这五大组件协同工作,完成从请求接收、参数解析到响应返回的全流程。

路由(Router):请求分发的核心

Router 是 Axum 的路由管理器,负责将不同 HTTP 方法(GET、POST、PUT、DELETE 等)和路径映射到对应的处理函数,支持路由嵌套、路径参数、路由合并等功能。

基本路由与 HTTP 方法

Axum 支持所有常见的 HTTP 方法,通过 routing 模块中的函数(get、post、put、delete 等)绑定处理函数:

rust 复制代码
use anyhow::Result;
use axum::{
    Router,
    routing::{delete, get, post, put},
};

// 不同 HTTP 方法的处理函数
async fn get_handler() -> &'static str {
    "这是 GET 请求"
}

async fn post_handler() -> &'static str {
    "这是 POST 请求"
}

async fn put_handler() -> &'static str {
    "这是 PUT 请求"
}

async fn delete_handler() -> &'static str {
    "这是 DELETE 请求"
}

#[tokio::main]
async fn main() -> Result<()> {
    tracing_subscriber::fmt::init();

    let app = Router::new()
        .route("/get", get(get_handler)) // GET /get
        .route("/post", post(post_handler)) // POST /post
        .route("/put", put(put_handler)) // PUT /put
        .route("/delete", delete(delete_handler)); // DELETE /delete

    // 启动服务器
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
    axum::serve(listener, app).await?;

    Ok(())
}
路径参数

通过 /:param 语法定义路径参数,处理函数通过提取器获取参数值,支持 Stringu32i64 等多种基础类型,无需手动转换:

rust 复制代码
use anyhow::Result;
use axum::{Router, extract::Path, routing::get};

// 提取单个路径参数(用户 ID)
async fn user_handler(Path(user_id): Path<u32>) -> String {
    format!("用户 ID:{}", user_id)
}

// 提取多个路径参数(用户 ID + 文章 ID)
async fn article_handler(Path((user_id, article_id)): Path<(u32, String)>) -> String {
    format!("用户 {} 的文章:{}", user_id, article_id)
}

#[tokio::main]
async fn main() -> Result<()> {
    tracing_subscriber::fmt::init();

    let app = Router::new()
        .route("/user/{user_id}", get(user_handler)) // GET /user/123
        .route("/user/{user_id}/article/{article_id}", get(article_handler)); // GET /user/123/article/hello-axum

    // 启动服务器
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
    axum::serve(listener, app).await?;

    Ok(())
}
路由嵌套

对于复杂项目,可通过 nest 方法实现路由嵌套,将相关路由分组管理,提升代码可维护性:

rust 复制代码
use anyhow::Result;
use axum::{Router, extract::Path, routing::get};

// 用户相关路由
async fn user_list() -> &'static str {
    "所有用户列表"
}

async fn user_detail(Path(user_id): Path<u32>) -> String {
    format!("用户 {} 详情", user_id)
}

// 文章相关路由
async fn article_list() -> &'static str {
    "所有文章列表"
}

#[tokio::main]
async fn main() -> Result<()> {
    tracing_subscriber::fmt::init();

    // 嵌套用户路由:所有 /user 开头的请求,都会匹配到 user_router
    let user_router = Router::new()
        .route("/", get(user_list)) // GET /user
        .route("/{user_id}", get(user_detail)); // GET /user/123

    // 嵌套文章路由:所有 /article 开头的请求,都会匹配到 article_router
    let article_router = Router::new().route("/", get(article_list)); // GET /article

    // 根路由:合并所有嵌套路由
    let app = Router::new()
        .nest("/user", user_router)
        .nest("/article", article_router);

    // 启动服务器
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
    axum::serve(listener, app).await?;

    Ok(())
}

提取器(Extractor):请求参数解析的利器

提取器是 Axum 最强大的特性之一,它允许我们提取请求中的各种信息,无需手动解析请求体、请求头或参数,且所有提取器都具备类型安全特性,编译期就能校验参数类型。

Path:提取路径参数

前文已介绍,用于提取路径中的参数,支持单个、多个参数,以及复杂类型。

Query:提取查询参数

用于提取 URL 中的查询参数,比如 /users?page=1&limit=10,通常结合 serde::Deserialize 特征使用,自动将查询参数映射为结构体:

rust 复制代码
use anyhow::Result;
use axum::{Router, extract::Query, routing::get};
use serde::Deserialize;

// 定义查询参数结构体
#[derive(Deserialize, Debug)]
struct Pagination {
    page: u64,
    limit: u64,
}

// 提取查询参数
async fn user_list(Query(pagination): Query<Pagination>) -> String {
    let offset = (pagination.page - 1) * pagination.limit;
    format!(
        "分页查询:页码 {},每页 {} 条,偏移量 {}",
        pagination.page, pagination.limit, offset
    )
}

#[tokio::main]
async fn main() -> Result<()> {
    tracing_subscriber::fmt::init();

    let app = Router::new().route("/users", get(user_list)); // GET /users?page=1&limit=10

    // 启动服务器
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
    axum::serve(listener, app).await?;

    Ok(())
}
Json:提取 JSON 请求体

用于提取 JSON 格式的请求体,结合 serde::Deserialize 特征使用,自动将 JSON 字符串反序列化为结构体,同时支持自动校验 JSON 格式:

rust 复制代码
use anyhow::Result;
use axum::{Json, Router, routing::post};
use serde::Deserialize;

// 定义 JSON 请求体结构体
#[derive(Deserialize, Debug)]
struct CreateUserRequest {
    username: String,
    email: String,
    age: Option<u8>, // 可选参数
}

// 提取 JSON 请求体并处理
async fn create_user(Json(req): Json<CreateUserRequest>) -> String {
    format!(
        "创建用户:用户名={},邮箱={},年龄={:?}",
        req.username, req.email, req.age
    )
}

#[tokio::main]
async fn main() -> Result<()> {
    tracing_subscriber::fmt::init();

    let app = Router::new().route("/users", post(create_user)); // POST /users,请求体为 JSON

    // 启动服务器
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
    axum::serve(listener, app).await?;

    Ok(())
}
State:提取应用全局状态

在实际项目中,我们常常需要共享全局资源,比如数据库连接池、配置信息、缓存等,Axum 通过 State 提取器实现全局状态共享,步骤如下:

  1. 定义状态结构体,需要实现 Clone,因为状态会被多个请求处理函数共享;
  2. 通过 Router::with_state 将状态挂载到路由上;
  3. 在处理函数中通过 State 提取器获取状态。
rust 复制代码
use anyhow::Result;
use axum::{Router, extract::State, routing::get};
use std::sync::{Arc, Mutex};

// 定义全局状态结构体
#[derive(Clone)]
struct AppState {
    // Arc + Mutex 实现线程安全的可变状态
    counter: Arc<Mutex<u64>>,
}

// 提取状态并修改计数器
async fn incr_counter(State(state): State<AppState>) -> String {
    // 锁定计数器,修改值
    let mut counter = state.counter.lock().unwrap();
    *counter += 1;
    format!("当前计数器值:{}", counter)
}

#[tokio::main]
async fn main() -> Result<()> {
    tracing_subscriber::fmt::init();

    // 初始化全局状态
    let app_state = AppState {
        counter: Arc::new(Mutex::new(0)),
    };

    let app = Router::new()
        .route("/incr", get(incr_counter))
        .with_state(app_state);

    // 启动服务器
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
    axum::serve(listener, app).await?;

    Ok(())
}
提取器组合与常见错误

多个提取器可以组合使用,只需在处理函数参数中依次声明即可,但需注意提取器顺序:元数据提取器(State、Path、Query、Header 等)放在前面,请求体提取器(Json、Form 等)放在最后,因为请求体只能读取一次。

当提取器无法正确解析请求数据时,如 JSON 格式错误、参数类型不匹配等错误,会返回提取器拒绝(Rejection)。Axum 提供 debug_handler 宏,可帮助我们快速排查提取器相关的类型错误,但需要引入 axum-macros 这个依赖库:

rust 复制代码
use axum::extract::{State, Json};
use serde::Deserialize;

#[derive(Clone)]
struct AppState {
    app_name: String,
}

#[derive(Deserialize)]
struct LoginRequest {
    username: String,
    password: String,
}

// 添加 debug_handler 宏,排查提取器和处理函数的类型错误
#[axum_macros::debug_handler]
async fn login(
    State(state): State<AppState>, // 元数据提取器(前)
    Json(req): Json<LoginRequest>  // 请求体提取器(后)
) -> String {
    format!("{}:用户 {} 登录成功", state.app_name, req.username)
}

响应(Response):灵活构建返回结果

Axum 中,任何实现 IntoResponse 特征的类型都可以作为处理函数的返回值,无需手动构建 HTTP 响应对象,简化了响应编写流程。

基础响应类型

Axum 内置了多种基础类型的 IntoResponse 实现,可直接作为返回值:

rust 复制代码
use anyhow::Result;
use axum::{Router, routing::get};

// 字符串响应
async fn str_response() -> &'static str {
    "字符串响应"
}

// 字符串切片响应(带状态码)
async fn str_with_status() -> (axum::http::StatusCode, &'static str) {
    (axum::http::StatusCode::OK, "成功响应")
}

// JSON 响应(serde_json::Value 或自定义结构体)
async fn json_response() -> axum::Json<serde_json::Value> {
    axum::Json(serde_json::json!({
        "code": 200,
        "message": "success",
        "data": { "username": "axum" }
    }))
}

#[tokio::main]
async fn main() -> Result<()> {
    tracing_subscriber::fmt::init();

    let app = Router::new()
        .route("/str", get(str_response))
        .route("/str-status", get(str_with_status))
        .route("/json", get(json_response));

    // 启动服务器
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
    axum::serve(listener, app).await?;

    Ok(())
}
自定义响应

对于复杂场景,比如统一响应格式、自定义响应头,可以通过实现 IntoResponse 特征自定义响应类型:

rust 复制代码
use anyhow::Result;
use axum::{
    Router,
    http::{StatusCode, header::CONTENT_TYPE},
    response::{IntoResponse, Response},
    routing::get,
};
use serde::Serialize;

// 自定义统一响应结构体
#[derive(Serialize)]
struct ApiResponse<T> {
    code: u16,
    message: &'static str,
    data: Option<T>,
}

// 实现 IntoResponse 特征
impl<T: Serialize> IntoResponse for ApiResponse<T> {
    fn into_response(self) -> Response {
        // 序列化 JSON
        let json = serde_json::to_string(&self).unwrap();
        // 构建响应:状态码 + 响应头 + 响应体
        (
            StatusCode::OK,
            [(CONTENT_TYPE, "application/json; charset=utf-8")],
            json,
        )
            .into_response()
    }
}

// 成功响应(带数据)
async fn success_response() -> ApiResponse<String> {
    ApiResponse {
        code: 200,
        message: "success",
        data: Some("操作成功".to_string()),
    }
}

// 失败响应(无数据)
async fn error_response() -> ApiResponse<()> {
    ApiResponse {
        code: 400,
        message: "参数错误",
        data: None,
    }
}

#[tokio::main]
async fn main() -> Result<()> {
    tracing_subscriber::fmt::init();

    let app = Router::new()
        .route("/success", get(success_response))
        .route("/error", get(error_response));

    // 启动服务器
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
    axum::serve(listener, app).await?;

    Ok(())
}

中间件(Middleware):统一拦截与处理

中间件用于在请求到达处理函数之前、响应返回客户端之前,执行统一的逻辑,比如日志记录、鉴权、限流、CORS 跨域等。Axum 本身不实现中间件系统,而是直接复用 TowerTower-HTTP 生态的中间件,无需重复开发。

常用内置中间件(Tower-HTTP)

Tower-HTTP 提供了大量常用中间件,在 Cargo.toml 中添加依赖:

toml 复制代码
[dependencies]
tower = "0.5"
tower-http = { version = "0.6", features = ["full"] }
http = "1.0"

以日志中间件和 CORS 中间件为例:

rust 复制代码
use anyhow::Result;
use axum::{Router, routing::get};
use tower_http::{cors::CorsLayer, trace::TraceLayer};

async fn root_handler() -> &'static str {
    "Hello, Axum Middleware!"
}

#[tokio::main]
async fn main() -> Result<()> {
    tracing_subscriber::fmt::init();

    // 构建中间件
    let cors_layer = CorsLayer::permissive(); // 允许所有跨域请求,生产环境需谨慎配置
    let trace_layer = TraceLayer::new_for_http(); // 请求日志中间件

    let app = Router::new()
        .route("/", get(root_handler))
        .layer(cors_layer) // CORS 中间件
        .layer(trace_layer); // 日志中间件

    // 启动服务器
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
    axum::serve(listener, app).await?;

    Ok(())
}
自定义中间件

如果内置中间件无法满足需求,我们可以通过实现 Tower 的 Service 特征来自定义中间件。以下是一个简单的自定义日志中间件示例,记录请求耗时:

rust 复制代码
use std::{
    pin::Pin,
    task::{Context, Poll},
    time::Instant,
};

use anyhow::Result;
use axum::{Router, routing::get};
use tower::{Layer, Service};

// 自定义中间件服务
#[derive(Clone)]
struct TimingService<S> {
    inner: S,
}

// 实现 Service 特征
impl<S, Req> Service<Req> for TimingService<S>
where
    S: Service<Req>,
    S::Future: Send + 'static,
    Req: Send + 'static,
{
    type Response = S::Response;
    type Error = S::Error;
    type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;

    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        self.inner.poll_ready(cx)
    }

    fn call(&mut self, req: Req) -> Self::Future {
        let start = Instant::now();
        let future = self.inner.call(req);
        Box::pin(async move {
            let result = future.await;
            let duration = start.elapsed();
            println!("请求耗时:{:?}", duration);
            result
        })
    }
}

// 中间件层,用于挂载到路由
#[derive(Clone)]
struct TimingLayer;

impl<S> Layer<S> for TimingLayer {
    type Service = TimingService<S>;

    fn layer(&self, inner: S) -> Self::Service {
        TimingService { inner }
    }
}

async fn root_handler() -> &'static str {
    "Hello, Custom Middleware!"
}

#[tokio::main]
async fn main() -> Result<()> {
    tracing_subscriber::fmt::init();

    let app = Router::new()
        .route("/", get(root_handler))
        .layer(TimingLayer); // 挂载自定义中间件

    // 启动服务器
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
    axum::serve(listener, app).await?;

    Ok(())
}

总结

Axum 作为 Rust Web 生态的后起之秀,正在快速发展,社区活跃度不断提升。如果你想使用 Rust 构建高性能、高安全性的 Web 服务,Axum 绝对是一个值得深入学习和使用的框架。

相关推荐
灰子学技术1 小时前
Envoy HTTP 流量层面的 Metric 指标分析
网络·网络协议·http
0xDevNull1 小时前
Spring Boot 自动装配:从原理到实践
java·spring boot·后端
上海云盾-小余1 小时前
海外恶意 UDP 攻击溯源:分层封禁策略与业务兼容平衡方案
网络·网络协议·udp
C澒1 小时前
AI 生码 - API2Code:接口智能匹配与 API 自动化生码全链路设计
前端·低代码·ai编程
智慧光迅AINOPOL2 小时前
校园全光网建设指南:从架构到调优,打造稳定高体验校园网络
网络·全光网解决方案·全光网·酒店全光解决方案·泛住宿全光网解决方案
浔川python社2 小时前
HTML头部元信息避坑指南技术文章大纲
前端·html
被摘下的星星2 小时前
Internet 的域名系统:从“名字”到“地址”的翻译官
网络
IT_陈寒2 小时前
SpringBoot配置加载顺序把我坑惨了
前端·人工智能·后端