Rust Web开发指南 第二章(Axum 路由与参数处理)

Axum 作为 Rust 生态中基于 Tokio 的低资源消耗、高性能 HTTP 框架,其核心优势之一是灵活的路由系统简洁的参数提取机制。本教程将从 Axum 基础代码出发,循序渐进地讲解路由扩展、路由嵌套、路径参数、查询参数等核心功能,最终实现一个支持多场景参数处理的模块化 HTTP 服务。

一、基础回顾:Axum 服务的核心结构

在扩展路由与参数功能前,我们先回顾上一章的内容。其核心结构分为 4 部分:

  1. 依赖导入:Axum 核心组件、日志、网络地址、异步信号处理;
  2. 日志初始化:配置日志过滤规则与输出格式,便于调试;
  3. 服务器启动:创建 TCP 监听器、绑定地址、启动服务;
  4. 优雅关闭:监听 Ctrl+C/SIGTERM 信号,确保当前请求处理完成后再停止服务。

基础代码中仅包含根路由(/),处理函数返回固定的 Hello world!。接下来我们将在此基础上逐步扩展路由与参数能力。


二、第一步:路由扩展基础 —— 添加简单路由

路由是 HTTP 服务的 “导航系统”,用于将不同 URL 路径与对应的处理函数绑定。Axum 中通过 Router::route 方法实现路由配置,语法为:

Router::new().route("路径", HTTP方法绑定(处理函数))

1.1 新增 “问候路由”(/say)

我们先添加一个无参数的 /say 路由,返回通用问候语。需要修改两部分代码:

步骤 1:导入依赖(无需新增,基础代码已包含核心依赖)

use axum::{routing::get, Router}; // 已存在于基础代码

步骤 2:新增路由处理函数

在 root 函数后添加 say_hello 函数,负责处理 /say 路径的 GET 请求:

/// 处理"/say"路径的GET请求:无参数的通用问候
async fn say_hello() -> &'static str {
    "Hello guts!" // 口语化问候,返回静态字符串(编译期确定,无内存分配)
}

步骤 3:在主函数中注册路由

修改 main 函数中 app 的路由配置,新增 /say 路由:

// 基础代码中的路由配置
let app = Router::new()
    .route("/", get(root)) // 原有根路由
    .route("/say", get(say_hello)); // 新增/say路由

1.2 关键知识点解释

  • routing::get:将异步函数绑定为 GET 请求的处理器,类似地还有 post/put/delete 等方法,对应不同 HTTP 方法;
  • 处理函数返回值:此处返回 &'static str(静态字符串引用),Axum 会自动将其封装为 HTTP 响应(状态码 200 OK,Content-Type 为 text/plain);
  • 路径匹配规则/say 仅匹配精确路径,不匹配 /say/abc 这类子路径(后续将通过嵌套路由解决)。

三、第二步:路由模块化 —— 使用 nest 实现路由嵌套

当路由数量增多时,直接在主函数中注册所有路由会导致代码混乱。Axum 提供 Router::nest 方法支持路由嵌套,可按业务模块拆分路由(如 “问候模块”“用户模块”),提升代码可维护性。

3.1 实现 “问候模块” 路由组

我们将所有以 /say 为前缀的路由统一放在 say_hello_routes 函数中,形成独立的路由模块:

步骤 1:新增路由组函数

/// 创建"问候相关"的路由组:统一管理以"/say"为前缀的所有路由
fn say_hello_routes() -> Router {
    Router::new()
        // 子路由1:/(嵌套后完整路径为 /say)
        .route("/", get(say_hello))
        // 后续将在此处添加更多子路由(如带参数的/say/{name})
}

步骤 2:在主函数中嵌套路由

修改 main 函数的 app 配置,用 nest 替代直接的 route

let app = Router::new()
    .route("/", get(root)) // 原有根路由
    // 嵌套路由:所有以"/say"开头的请求,委托给say_hello_routes()的子路由处理
    .nest("/say", say_hello_routes());

3.2 关键知识点解释

  • nest 方法:参数为 “前缀路径” 和 “子 Router”,嵌套后子路由的路径会与前缀拼接。例如:
    • 子路由 / → 完整路径 /say
    • 子路由 /abc → 完整路径 /say/abc
  • 模块化优势:后续新增 “问候模块” 的路由(如 /say/search),只需在 say_hello_routes 中添加,无需修改主函数;
  • 路由合并:子 Router 可独立配置中间件、错误处理,实现模块内的功能隔离。

四、第三步:路径参数处理 —— 从 URL 路径提取数据

实际开发中,我们常需要从 URL 路径中获取动态数据(如 /user/123 中的用户 ID 123)。Axum 提供 Path 提取器(axum::extract::Path),可轻松从路径中提取参数。

4.1 单段路径参数(/{param}

先实现 /say/{name} 路由,根据路径中的 name 动态返回问候语(如 /say/Alice 返回 Hello Alice)。

步骤 1:导入 Path 提取器

在 axum 依赖导入中添加 extract::Path

use axum::{
    routing::get,
    Router,
    extract::Path, // 新增:导入路径参数提取器
};

步骤 2:新增带路径参数的处理函数

/// 处理"/say/{name}"路径的GET请求:带单路径参数的问候
/// Path(name):从路径中提取{name}参数,类型为String(拥有所有权,避免生命周期问题)
async fn say_hello_to(Path(name): Path<String>) -> String {
    format!("Hello {}", name) // 动态生成响应(返回String而非&'static str)
}

步骤 3:在路由组中注册路由

在 say_hello_routes 函数中添加新路由:

fn say_hello_routes() -> Router {
    Router::new()
        .route("/{name}", get(say_hello_to)) // 新增:/say/{name}
        .route("/", get(say_hello))
}

4.2 通配符路径参数(/{*param}

单段参数仅能匹配一个路径段(如 /say/Alice 中的 Alice),若需匹配剩余所有路径段(如 /say/love/you/forever 中的 you/forever),需使用通配符路径参数 {*param}

步骤 1:新增通配符路由与处理函数

在 say_hello_routes 中添加 /love/{*rest} 路由(注意:原代码中 /love{*rest} 缺少 /,需修正为 /love/{*rest} 以匹配预期路径):

// 新增处理函数
/// 处理"/say/love/{*rest}"路径的GET请求:带通配符参数的表白
async fn say_love_to(Path(rest): Path<String>) -> String {
    format!("I love {}", rest) // 如/rest=you → "I love you"
}

// 注册路由(在say_hello_routes中)
fn say_hello_routes() -> Router {
    Router::new()
        .route("/{name}", get(say_hello_to))
        .route("/", get(say_hello))
        .route("/love/{*rest}", get(say_love_to)) // 新增:通配符路由
}

4.3 路径参数关键知识点

  1. 提取器语法:处理函数的参数需写成 Path(param): Path<Type>,其中:

    • param 是参数名,必须与路径中的 {param} 或 {*param} 一致;
    • Type 是参数类型,需实现 axum::extract::FromUriParts trait(常见类型如 Stringu32i64 已默认实现);
  2. 单段 vs 通配符

    • 单段参数 {name}:匹配一个路径段(不包含 /),如 /say/Alice/Bob 中 {name} 仅匹配 Alice
    • 通配符参数 {*rest}:匹配剩余所有路径段(包含 /),如 /say/love/you 中 {*rest} 匹配 you/say/love/you/forever 中匹配 you/forever
  3. 返回值类型:当响应内容动态生成时(如 format! 拼接),需返回 String(而非 &'static str),因为静态字符串无法存储运行时生成的内容。


五、第四步:查询参数处理 —— 从 URL 查询字符串提取数据

除了路径参数,HTTP 请求还常通过查询字符串(URL 中 ? 后的部分,如 /search?query=rust&page=1)传递参数。Axum 提供 Query 提取器(axum::extract::Query)处理这类参数。

5.1 用 HashMap 处理动态查询参数

若查询参数的键名或数量不固定(如通用搜索接口,支持动态筛选条件),可使用 std::collections::HashMap 存储查询参数。

步骤 1:导入依赖

添加 Query 提取器和 HashMap

// Axum依赖中添加Query
use axum::{
    routing::get,
    Router,
    extract::Path, Query, // 新增Query
};

// 标准库中导入HashMap
use std::collections::HashMap; // 新增:存储查询参数键值对

步骤 2:新增查询参数处理函数

实现 /say/search 路由,返回所有查询参数:

/// 处理"/say/search"路径的GET请求:用HashMap接收动态查询参数
/// Query(params):从查询字符串中提取键值对,存储为HashMap<String, String>
async fn search(Query(params): Query<HashMap<String, String>>) -> String {
    format!("Search query: {:?}", params) // 格式化输出所有参数
}

步骤 3:注册路由

在 say_hello_routes 中添加路由:

fn say_hello_routes() -> Router {
    Router::new()
        .route("/{name}", get(say_hello_to))
        .route("/", get(say_hello))
        .route("/love/{*rest}", get(say_love_to))
        .route("/search", get(search)) // 新增:查询参数路由
}

5.2 用自定义结构体处理固定结构参数

若查询参数的键名和类型固定(如分页接口 ?page=1&size=10),推荐使用自定义结构体替代 HashMap,可获得类型安全和自动校验。

例如,实现固定的分页查询参数:

// 1. 定义结构体(需派生Deserialize trait,需在Cargo.toml中添加serde依赖)
use serde::Deserialize; // 需在Cargo.toml添加serde = { version = "1.0", features = ["derive"] }

#[derive(Deserialize, Debug)]
struct PageParams {
    page: u32,    // 页码(必须为非负整数)
    size: u32,    // 每页条数
    query: String,// 搜索关键词
}

// 2. 处理函数:用结构体接收查询参数
async fn paginated_search(Query(params): Query<PageParams>) -> String {
    format!(
        "Search: query={}, page={}, size={}",
        params.query, params.page, params.size
    )
}

// 3. 注册路由(在say_hello_routes中)
// .route("/paginated-search", get(paginated_search))

5.3 查询参数关键知识点

  1. Query 提取器:原理与 Path 类似,通过 Query(params): Query<Type> 提取查询参数,Type 需实现 serde::Deserialize trait(HashMap 已实现,自定义结构体需派生 Deserialize);
  2. HashMap 适用场景:参数键名动态(如用户自定义筛选条件)、参数数量不固定;
  3. 自定义结构体适用场景:参数结构固定(如分页、固定筛选),可避免键名拼写错误,且自动处理类型转换(如将字符串 page=1 转为 u32);
  4. 依赖说明:使用自定义结构体时,需在 Cargo.toml 中添加 serde 依赖(serde = { version = "1.0", features = ["derive"] }),因为 Query 提取器依赖 serde 反序列化查询参数。

六、完整代码整合与解析

将上述所有功能整合后,最终代码如下(与本文开头提供的 “带路由和参数处理的代码” 一致,此处逐段解析关键部分):

// 导入必要的组件:按功能分类整理,明确各依赖的核心用途
// Axum框架核心:用于构建HTTP路由和处理请求
use axum::{
    routing::get,          // 导入GET请求处理器构造函数,用于将函数绑定为GET路由的处理逻辑
    Router,                // 路由构建器:用于定义请求路径与处理函数的映射关系,支持嵌套和合并
    extract::{Path, Query}, // 导入参数提取器:
                           // Path:从URL路径中提取参数(如 /{name} 中的name)
                           // Query:从URL查询参数中提取数据(如 ?key=value 中的键值对)
};
// 标准库组件:网络地址与数据结构
use std::net::SocketAddr;       // 定义网络地址格式(IP + 端口),用于指定服务器绑定的地址
use std::collections::HashMap;  // 哈希表结构:用于存储查询参数的键值对(适合参数数量/键名不固定的场景)
// 日志系统:用于记录运行时信息、错误,便于调试和监控
use tracing::{info, error};     // 日志宏:info!记录普通信息,error!记录错误信息
use tracing_subscriber::{       // 日志订阅器:配置日志的过滤规则、输出格式并初始化
    layer::SubscriberExt,       // 为订阅器添加额外功能(如多层日志处理)的扩展 trait
    util::SubscriberInitExt     // 提供订阅器初始化方法的扩展 trait
};
// 异步信号处理:用于实现服务器的优雅关闭(响应系统终止信号)
use tokio::signal;              // Tokio的信号处理模块:监听系统信号(如Ctrl+C、SIGTERM)


// 异步主函数:Axum基于Tokio异步运行时,必须用#[tokio::main]宏标记
// 该宏会自动初始化Tokio的多线程运行时,处理异步任务调度(如HTTP请求、IO操作)
#[tokio::main]
async fn main() {
    // -------------------------- 1. 初始化日志系统 --------------------------
    // 创建日志订阅器注册表:管理日志的过滤、格式化等多层处理逻辑
    tracing_subscriber::registry()
        // 1.1 配置日志过滤规则:控制哪些日志会被输出
        .with(
            tracing_subscriber::EnvFilter::try_from_default_env()
                // 优先从环境变量(如 RUST_LOG)读取过滤规则,若未设置则使用默认规则
                .unwrap_or_else(|_| "axum_tutorial=debug,tower_http=debug".into())
                // 默认规则说明:
                // - axum_tutorial=debug:当前程序(crate名)输出debug及以上级别日志
                // - tower_http=debug:axum依赖的tower_http库输出debug及以上级别日志
        )
        // 1.2 配置日志输出格式:添加格式化层,默认输出包含时间、日志级别、模块名、日志内容
        .with(tracing_subscriber::fmt::layer())
        // 1.3 初始化日志系统:将配置应用到全局,后续可通过info!、error!等宏输出日志
        .init();

    // 记录服务器启动的信息日志
    info!("Starting axum server...");

    // -------------------------- 2. 创建路由表 --------------------------
    // 初始化根路由,并添加具体的路由规则
    let app = Router::new()
        // 根路径("/")的GET请求:由root函数处理
        .route("/", get(root))
        // 嵌套路由组:将所有以"/say"开头的请求,委托给say_hello_routes()返回的路由处理
        // 作用:按业务模块拆分路由,提升代码可维护性(如"/say"下的所有接口都属于问候相关)
        .nest("/say", say_hello_routes());

    // -------------------------- 3. 绑定服务器地址 --------------------------
    // 定义服务器绑定的地址:
    // - [0, 0, 0, 0]:监听所有可用的网络接口(允许外部设备访问,而非仅本地)
    // - 8080:绑定的端口号
    let addr = SocketAddr::from(([0, 0, 0, 0], 8080));
    // 记录绑定地址的信息日志
    info!("Binding to address: {}", addr);

    // 创建TCP监听器:绑定到指定地址,用于接收客户端的TCP连接
    // 使用match处理绑定结果(异步操作,需await):
    let listener = match tokio::net::TcpListener::bind(addr).await {
        Ok(listener) => listener,  // 绑定成功:获取监听器对象,用于后续接收请求
        Err(e) => {                // 绑定失败(如端口被占用):
            error!("Failed to bind to address {}: {}", addr, e); // 记录错误日志
            return;                // 退出程序,避免后续无效执行
        }
    };

    // -------------------------- 4. 启动服务器并处理优雅关闭 --------------------------
    // 记录服务器启动成功的信息日志
    info!("Server started, listening on {}", addr);

    // 创建Axum服务器:
    // - listener:用于接收TCP连接的监听器
    // - app:定义好的路由表,用于处理接收到的HTTP请求
    let server = axum::serve(listener, app);

    // 启动服务器并配置优雅关闭:
    // - with_graceful_shutdown:接收一个异步函数(shutdown_signal),当该函数完成时触发优雅关闭
    // - 优雅关闭:等待当前正在处理的请求完成后再停止服务器,避免请求中断
    if let Err(e) = server.with_graceful_shutdown(shutdown_signal()).await {
        // 若服务器运行过程中出错(如端口被强制释放),记录错误日志
        error!("Server error: {}", e);
    }

    // 服务器优雅关闭后,记录关闭完成的信息日志
    info!("Server shutdown");
}


/// 根路径("/")的GET请求处理函数
/// 功能:返回静态字符串作为HTTP响应(状态码默认200 OK,Content-Type默认text/plain)
/// 返回值:&'static str(静态字符串引用,编译期确定,无需动态分配内存)
async fn root() -> &'static str {
    "Hello world!"  // 响应体内容:简单的问候字符串
}


/// 处理系统关闭信号的异步函数:用于触发服务器优雅关闭
/// 功能:监听系统终止信号(Windows的Ctrl+C、Unix的Ctrl+C/SIGTERM),信号触发后通知服务器关闭
async fn shutdown_signal() {
    // 分支1:监听Ctrl+C信号(所有系统通用)
    let ctrl_c = async {
        signal::ctrl_c()                  // 注册Ctrl+C信号处理器
            .await                        // 等待Ctrl+C信号触发
            .expect("Failed to install Ctrl+C handler"); // 若注册失败, panic(生产环境可优化为优雅处理)
    };

    // 分支2:在Unix系统(如Linux、macOS)上额外监听SIGTERM信号(系统终止信号,如kill命令触发)
    #[cfg(unix)]
    let terminate = async {
        signal::unix::signal(signal::unix::SignalKind::terminate()) // 注册SIGTERM信号处理器
            .expect("Failed to install SIGTERM handler")            // 注册失败时panic
            .recv()                                                 // 等待SIGTERM信号触发
            .await;
    };

    // 等待任一信号触发(根据系统类型分支处理):
    #[cfg(unix)]
    tokio::select! {
        // Unix系统:等待Ctrl+C或SIGTERM,任一信号触发则继续执行(通知服务器关闭)
        _ = ctrl_c => {},
        _ = terminate => {},
    }

    // 非Unix系统(如Windows):仅等待Ctrl+C信号
    #[cfg(not(unix))]
    ctrl_c.await;

    // 记录信号接收日志:提示开始优雅关闭流程
    info!("Received shutdown signal, starting graceful shutdown...");
}


/// 创建"问候相关"的路由组:统一管理以"/say"为前缀的所有路由
/// 返回值:Router实例,包含该模块下的所有路由规则
fn say_hello_routes()->Router{
    Router::new()
        // 1. 路径:"/say/{name}"(嵌套后完整路径),GET请求由say_hello_to处理
        // {name}:单路径参数,匹配"/say/"后紧跟的单个路径段(如/say/Alice中的Alice)
        .route("/{name}", get(say_hello_to))
        // 2. 路径:"/say/"(嵌套后完整路径),GET请求由say_hello处理
        // 无参数,返回固定问候语
        .route("/", get(say_hello))
        // 3. 路径:"/say/love{*rest}"(嵌套后完整路径),GET请求由say_love_to处理
        // {*rest}:通配符参数,匹配"/say/love"后所有剩余路径段(如/say/lovesome中的some,注意:语法需补"/"为/love/{*rest}才符合预期)
        .route("/love{*rest}", get(say_love_to))
        // 4. 路径:"/say/search"(嵌套后完整路径),GET请求由search处理
        // 用于接收查询参数(如/say/search?query=rust&page=1)
        .route("/search", get(search))
}


/// 处理"/say/{name}"路径的GET请求:带单路径参数的问候
/// 参数:Path(name):通过Path提取器获取路径中的{name}参数,类型为String(拥有所有权,避免生命周期问题)
/// 返回值:格式化后的问候字符串(动态生成,类型为String)
async fn say_hello_to(Path(name): Path<String>) -> String {
    format!("Hello {}", name)  // 拼接问候语(如name=Alice时返回"Hello Alice")
}


/// 处理"/say/"路径的GET请求:无参数的通用问候
/// 返回值:静态字符串(无需动态分配,直接返回固定内容)
async fn say_hello() -> &'static str {
    "Hello guts!"  // 响应内容:口语化的"嘿,大伙儿!"(guts为口语中对同伴的称呼)
}


/// 处理"/say/love{*rest}"路径的GET请求:带通配符参数的表白
/// 参数:Path(name):通过Path提取器获取通配符{*rest}匹配的路径内容(注意:参数名需与通配符名一致,当前代码参数名name与通配符rest不匹配,运行时需修正为Path(rest))
/// 返回值:格式化后的表白字符串(动态生成,类型为String)
async fn say_love_to(Path(name): Path<String>) -> String {
    format!("I love {}", name)  // 拼接表白语(如rest=you时返回"I love you")
}


/// 处理"/say/search"路径的GET请求:通过HashMap接收查询参数
/// 参数:Query(params):通过Query提取器获取URL中的查询参数(如?key1=val1&key2=val2),存储为HashMap<String, String>
/// 适用场景:查询参数的数量或键名不固定(如通用搜索接口,支持动态筛选条件)
/// 返回值:格式化后的查询参数信息(展示所有键值对)
async fn search(Query(params): Query<HashMap<String, String>>)->String{
    format!("Search query: {:?}", params)  // 以调试格式输出所有查询参数(如{"query":"rust", "page":"1"})
}

七、功能测试与验证

运行代码前,需确保 Cargo.toml 包含以下依赖:

[package]
name = "axum-tutorial"
version = "0.1.0"
edition = "2024"

[dependencies]
# Axum 核心框架(处理路由、请求/响应等)
axum = "0.8.4"
# 异步运行时(仅保留核心特性,减少冗余)
tokio = { version = "1.47.1", features = ["rt-multi-thread", "net", "macros","signal"] }
# 日志相关(初始化日志输出)
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }  # 添加env-filter特性

启动服务后,可通过 curl 或 浏览器 测试以下路由:

测试 URL预期响应功能说明
http://localhost:8080Hello world!根路由(无参数)
http://localhost:8080/sayHello guts!嵌套路由(无参数)
http://localhost:8080/say/AliceHello Alice单段路径参数

http://localhost:8080/say/love/You

I love You通配符路径参数
http://localhost:8080/say/search?query=axum&page=1Search query: {"query": "axum", "page": "1"}HashMap 处理查询参数

以下为Windows下crul测试的结果:

D:\>curl "http://localhost:8080"
Hello world!
D:\>curl "http://localhost:8080/say"
Hello guts!
D:\>curl "http://localhost:8080/say/Tom"
Hello Tom
D:\>curl "http://localhost:8080/say/loveTom"
I love Tom
D:\>curl "http://localhost:8080/say/search?name=Tom&age=30"
Search query: {"age": "30", "name": "Tom"}

八、总结:Axum 路由与参数处理核心要点

  1. 路由基础

    • 用 Router::route 绑定 “路径 + HTTP 方法 + 处理函数”;
    • 用 Router::nest 实现路由嵌套,按业务模块拆分代码。
  2. 参数提取

    • 路径参数Path 提取器,支持单段({param})和通配符({*param});
    • 查询参数Query 提取器,支持 HashMap(动态参数)和自定义结构体(固定参数)。
  3. 处理函数

    • 提取器作为函数参数,需显式标注类型(如 Path(name): Path<String>);
    • 返回值支持 &'static str(静态内容)和 String(动态内容),Axum 自动封装为 HTTP 响应。
  4. 模块化与可维护性

    • 路由嵌套是实现模块化的关键,避免主函数中路由配置臃肿;
    • 固定结构参数优先用自定义结构体,兼顾类型安全与代码可读性。

通过本教程的循序渐进扩展,你已掌握 Axum 路由与参数处理的核心能力。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值