当前位置: 首页 > news >正文

Rust Web开发指南 第三章(Axum 请求体解析:处理 JSON、表单与文件上传)

在 HTTP 交互中,除了路径参数和查询参数,请求体(Request Body) 是传递复杂数据的主要方式(如表单提交、API 数据传输)。Axum 提供了多种灵活的提取器(Extractor)用于解析不同格式的请求体,本文将详细讲解如何处理 JSON、表单数据、文件上传等常见场景。

一、请求体解析基础:核心概念与依赖准备

1.1 什么是请求体?

请求体是 HTTP 请求中携带的实际数据,通常用于 POSTPUTPATCH 等方法,用于向服务器提交数据(如用户注册信息、订单数据)。常见的请求体格式包括:

  • JSON:API 交互的主流格式,轻量且易于解析;
  • 表单数据:分 application/x-www-form-urlencoded(普通表单)和 multipart/form-data(带文件上传的表单);
  • 原始字节:如二进制文件、自定义协议数据。

1.2 必备依赖

解析请求体需要额外依赖,在 Cargo.toml 中添加(serde和multer依赖项):

[package]
name = "axum-tutorial"
version = "0.1.0"
edition = "2024"[dependencies]
# Axum 核心框架(处理路由、请求/响应等)
axum = { version = "0.8.4", features = ["multipart"] } # 启用 multipart 特性
# 异步运行时(仅保留核心特性,减少冗余)
tokio = { version = "1.47.1", features = ["rt-multi-thread", "net", "macros","signal","fs", "io-util"] }
# 日志相关(初始化日志输出)
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }  # 添加env-filter特性# 数据序列化/反序列化(JSON 处理依赖,启用 derive 宏)
serde = { version = "1.0.219", features = ["derive"] }# 新增:处理multipart表单(文件上传)
multer = "3.1.0"  # Axum推荐的multipart解析库# 新增:处理http请求体大小的限制
tower-http = { version = "0.6.6", features = ["limit"] }# 新增以下两行(sha2 和 hex 依赖)
sha2 = "0.10"   # 提供 SHA-256 哈希算法
hex = "0.4"     # 将字节数组转为十六进制字符串

二、解析 JSON 请求体:Json 提取器

JSON 是前后端 API 交互的标准格式,Axum 提供 axum::extract::Json 提取器,结合 serde 可轻松将 JSON 请求体解析为 Rust 结构体。

2.1 基础用法:解析简单 JSON

步骤 1:定义数据结构

首先定义与 JSON 对应的 Rust 结构体,需派生 serde::Deserialize trait 以支持反序列化:

use serde::Deserialize;/// 用户注册数据(JSON格式)
#[derive(Debug, Deserialize)]
struct RegisterUser {username: String,email: String,age: Option<u8>, // 可选字段:允许为null或不存在
}

步骤 2:创建处理函数

使用 Json 提取器接收请求体,并返回解析结果:

use axum::extract::Json;/// 处理用户注册(JSON请求体)
async fn register_user(Json(user): Json<RegisterUser>) -> String {// 解析后的user是RegisterUser实例,可直接使用其字段format!("注册成功!用户信息:用户名={}, 邮箱={}, 年龄={:?}",user.username, user.email, user.age)
}

步骤 3:注册路由(POST 方法)

JSON 数据通常通过 POST 方法提交,需使用 routing::post 绑定路由:

// 在之前的say_hello_routes或新的路由组中添加
use axum::routing::post;fn user_routes() -> Router {Router::new().route("/register", post(register_user)) // POST /user/register
}// 在main函数的app中嵌套
let app = Router::new().route("/", get(root)).nest("/user", user_routes()); // 新增用户相关路由

程序的全部代码:

// 导入必要的组件
// Axum框架核心组件:用于创建路由和处理HTTP请求
use axum::{routing::{get,post},  // 导入GET、POST请求处理函数Router,        // 导入路由构建器,用于定义请求路由规则extract::Json, // 从Axum框架的extract模块中导入Json提取器,用于解析HTTP请求体中Content-Type为application/json的JSON数据
};
// 标准库中的网络地址类型,用于指定服务器绑定的IP和端口
use std::net::SocketAddr;
// 日志相关组件:用于记录信息日志和错误日志
use tracing::{info, error};
// 日志订阅器组件:用于配置和初始化日志系统
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
// 新增:导入信号处理模块
use tokio::signal;
// 导入serde库的Deserialize trait,用于实现数据反序列化
// 反序列化是指将外部数据格式(如JSON、表单数据)转换为Rust结构体的过程
// 在Axum中处理请求体(如JSON、表单)时,需要通过该trait将请求数据解析为定义的结构体
// 需配合#[derive(Deserialize)]属性使用,自动为结构体生成反序列化代码
use serde::Deserialize;/// 用户注册数据(JSON格式)
#[derive(Debug, Deserialize)]
struct RegisterUser {username: String,email: String,age: Option<u8>, // 可选字段:允许为null或不存在
}// 异步主函数:Axum基于Tokio运行时,必须使用#[tokio::main]宏标记
// 该宏会自动设置Tokio异步运行时环境
#[tokio::main]
async fn main() {// 初始化日志系统:配置日志的过滤规则和输出格式tracing_subscriber::registry()  // 创建日志订阅器注册表.with(// 设置日志过滤规则:优先从环境变量读取,若未设置则使用默认规则// 默认规则:本程序(axum_tutorial)和tower_http库输出debug级别及以上日志tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "axum_tutorial=debug,tower_http=debug".into())).with(tracing_subscriber::fmt::layer())  // 添加日志格式化输出层.init();  // 初始化日志系统,使其生效// 记录信息日志:提示服务器开始启动info!("Starting axum server...");// 创建路由表:定义不同路径的请求由哪个函数处理let app = Router::new()// 注册根路径("/")的处理规则:GET请求由root函数处理.route("/", get(root)).nest("/user", user_routes()); // 新增用户相关路由// 定义服务器绑定的地址:0.0.0.0表示监听所有可用网络接口,端口为8080let addr = SocketAddr::from(([0, 0, 0, 0], 8080));// 记录信息日志:提示正在绑定的地址info!("Binding to address: {}", addr);// 创建TCP监听器并绑定到指定地址// 使用match表达式处理可能的绑定结果(成功/失败)let listener = match tokio::net::TcpListener::bind(addr).await {Ok(listener) => listener,  // 绑定成功:获取监听器对象Err(e) => {  // 绑定失败:记录错误并优雅退出error!("Failed to bind to address {}: {}", addr, e);return;  // 退出程序,不再继续执行}};// 启动Axum服务器:使用监听器接收请求,并通过路由表处理// 记录信息日志:提示服务器已启动并监听指定地址info!("Server started, listening on {}", addr);// 新增:创建服务器并配置优雅关闭let server = axum::serve(listener, app);// 新增:添加优雅关闭处理if let Err(e) = server.with_graceful_shutdown(shutdown_signal()).await {// 服务器运行出错时记录错误信息error!("Server error: {}", e);}// 记录信息日志:提示服务器已正常关闭info!("Server shutdown");
}/// 根路径("/")的请求处理函数
/// 异步函数:返回静态字符串作为HTTP响应体
async fn root() -> &'static str {"Hello world!"  // 响应内容:简单的"Hello world!"字符串
}// 新增:处理关闭信号的函数
async fn shutdown_signal() {// 等待Ctrl+C信号let ctrl_c = async {signal::ctrl_c().await.expect("Failed to install Ctrl+C handler");};// 在Unix系统上额外监听终止信号#[cfg(unix)]let terminate = async {signal::unix::signal(signal::unix::SignalKind::terminate()).expect("Failed to install SIGTERM handler").recv().await;};// 等待任一信号#[cfg(unix)]tokio::select! {_ = ctrl_c => {},_ = terminate => {},}#[cfg(not(unix))]ctrl_c.await;info!("Received shutdown signal, starting graceful shutdown...");
}fn user_routes() -> Router {Router::new().route("/register", post(register_user)) // POST /user/register
}/// 处理用户注册(JSON请求体)
async fn register_user(Json(user): Json<RegisterUser>) -> String {// 解析后的user是RegisterUser实例,可直接使用其字段format!("注册成功!用户信息:用户名={}, 邮箱={}, 年龄={:?}",user.username, user.email, user.age)
}

测试接口

使用 curl 发送 JSON 请求:

D:\>curl -X POST http://localhost:8080/user/register -H "Content-Type: application/json" -d "{\"username\":\"alice\", \"email\":\"alice@example.com\", \"age\":25}"
注册成功!用户信息:用户名=alice, 邮箱=alice@example.com, 年龄=Some(25)

预期响应正确:注册成功!用户信息:用户名=alice, 邮箱=alice@example.com, 年龄=Some(25)

2.2 关键知识点

  • Json 提取器:将请求体按 application/json 格式解析,若请求头 Content-Type 不匹配或格式错误,Axum 会自动返回 400 Bad Request
  • 可选字段:使用 Option<T> 标记可选字段,JSON 中可省略或设为 null
  • 反序列化失败:若 JSON 字段与结构体不匹配(如类型错误),Axum 会返回详细错误信息(需在日志中查看)。

三、解析表单数据:Form 提取器

对于 HTML 表单提交或 application/x-www-form-urlencoded 格式的请求,需使用 axum::extract::Form 提取器。

3.1 处理普通表单(x-www-form-urlencoded

步骤 1:定义表单结构体

同样需要派生 serde::Deserialize

#[derive(Debug, Deserialize)]
struct LoginForm {username: String,password: String,remember_me: bool, // 复选框类型,表单中会传递"true"或"false"
}

步骤 2:创建表单处理函数

use axum::extract::Form;/// 处理登录表单(x-www-form-urlencoded)
/// 密码不能明文显示(要加密)
use sha2::{Sha256, Digest}; // SHA-256哈希算法
use hex::encode;            // 字节转十六进制字符串
async fn login(Form(form): Form<LoginForm>) -> String {// 1. 计算密码的SHA-256哈希(将明文转为字节数组后处理)let password_hash = Sha256::digest(form.password.as_bytes());// 2. 将哈希结果(字节数组)转为十六进制字符串(便于显示)let password_hash_hex = encode(password_hash);format!("登录信息:用户名={}, 记住登录={}, 密码哈希(SHA-256)={}",form.username, form.remember_me, password_hash_hex)
}

步骤 3:注册路由

// 在user_routes中添加
fn user_routes() -> Router {Router::new().route("/register", post(register_user)).route("/login", post(login)) // POST /user/login
}

程序全部代码:

// 导入必要的组件
// Axum框架核心组件:用于创建路由和处理HTTP请求
use axum::{routing::{get,post},  // 导入GET、POST请求处理函数Router,        // 导入路由构建器,用于定义请求路由规则extract::{Json,   // 从Axum框架的extract模块中导入Json提取器,用于解析HTTP请求体中Content-Type为application/json的JSON数据Form    // 从Axum框架的extract模块中导入Form提取器},
};
// 标准库中的网络地址类型,用于指定服务器绑定的IP和端口
use std::net::SocketAddr;
// 日志相关组件:用于记录信息日志和错误日志
use tracing::{info, error};
// 日志订阅器组件:用于配置和初始化日志系统
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
// 新增:导入信号处理模块
use tokio::signal;
// 导入serde库的Deserialize trait,用于实现数据反序列化
// 反序列化是指将外部数据格式(如JSON、表单数据)转换为Rust结构体的过程
// 在Axum中处理请求体(如JSON、表单)时,需要通过该trait将请求数据解析为定义的结构体
// 需配合#[derive(Deserialize)]属性使用,自动为结构体生成反序列化代码
use serde::Deserialize;// 用户注册数据(JSON格式)
#[derive(Debug, Deserialize)]
struct RegisterUser {username: String,email: String,age: Option<u8>, // 可选字段:允许为null或不存在
}// Form的数据结构体
#[derive(Debug, Deserialize)]
struct LoginForm {username: String,password: String,remember_me: bool, // 复选框类型,表单中会传递"true"或"false"
}// 异步主函数:Axum基于Tokio运行时,必须使用#[tokio::main]宏标记
// 该宏会自动设置Tokio异步运行时环境
#[tokio::main]
async fn main() {// 初始化日志系统:配置日志的过滤规则和输出格式tracing_subscriber::registry()  // 创建日志订阅器注册表.with(// 设置日志过滤规则:优先从环境变量读取,若未设置则使用默认规则// 默认规则:本程序(axum_tutorial)和tower_http库输出debug级别及以上日志tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "axum_tutorial=debug,tower_http=debug".into())).with(tracing_subscriber::fmt::layer())  // 添加日志格式化输出层.init();  // 初始化日志系统,使其生效// 记录信息日志:提示服务器开始启动info!("Starting axum server...");// 创建路由表:定义不同路径的请求由哪个函数处理let app = Router::new()// 注册根路径("/")的处理规则:GET请求由root函数处理.route("/", get(root)).nest("/user", user_routes()); // 新增用户相关路由// 定义服务器绑定的地址:0.0.0.0表示监听所有可用网络接口,端口为8080let addr = SocketAddr::from(([0, 0, 0, 0], 8080));// 记录信息日志:提示正在绑定的地址info!("Binding to address: {}", addr);// 创建TCP监听器并绑定到指定地址// 使用match表达式处理可能的绑定结果(成功/失败)let listener = match tokio::net::TcpListener::bind(addr).await {Ok(listener) => listener,  // 绑定成功:获取监听器对象Err(e) => {  // 绑定失败:记录错误并优雅退出error!("Failed to bind to address {}: {}", addr, e);return;  // 退出程序,不再继续执行}};// 启动Axum服务器:使用监听器接收请求,并通过路由表处理// 记录信息日志:提示服务器已启动并监听指定地址info!("Server started, listening on {}", addr);// 新增:创建服务器并配置优雅关闭let server = axum::serve(listener, app);// 新增:添加优雅关闭处理if let Err(e) = server.with_graceful_shutdown(shutdown_signal()).await {// 服务器运行出错时记录错误信息error!("Server error: {}", e);}// 记录信息日志:提示服务器已正常关闭info!("Server shutdown");
}/// 根路径("/")的请求处理函数
/// 异步函数:返回静态字符串作为HTTP响应体
async fn root() -> &'static str {"Hello world!"  // 响应内容:简单的"Hello world!"字符串
}// 新增:处理关闭信号的函数
async fn shutdown_signal() {// 等待Ctrl+C信号let ctrl_c = async {signal::ctrl_c().await.expect("Failed to install Ctrl+C handler");};// 在Unix系统上额外监听终止信号#[cfg(unix)]let terminate = async {signal::unix::signal(signal::unix::SignalKind::terminate()).expect("Failed to install SIGTERM handler").recv().await;};// 等待任一信号#[cfg(unix)]tokio::select! {_ = ctrl_c => {},_ = terminate => {},}#[cfg(not(unix))]ctrl_c.await;info!("Received shutdown signal, starting graceful shutdown...");
}fn user_routes() -> Router {Router::new().route("/register", post(register_user)) // POST /user/register.route("/login", post(login)) // POST /user/login
}/// 处理用户注册(JSON请求体)
async fn register_user(Json(user): Json<RegisterUser>) -> String {// 解析后的user是RegisterUser实例,可直接使用其字段format!("注册成功!用户信息:用户名={}, 邮箱={}, 年龄={:?}",user.username, user.email, user.age)
}/// 处理登录表单(x-www-form-urlencoded)
use sha2::{Sha256, Digest}; // SHA-256哈希算法
use hex::encode;            // 字节转十六进制字符串
async fn login(Form(form): Form<LoginForm>) -> String {// 1. 计算密码的SHA-256哈希(将明文转为字节数组后处理)let password_hash = Sha256::digest(form.password.as_bytes());// 2. 将哈希结果(字节数组)转为十六进制字符串(便于显示)let password_hash_hex = encode(password_hash);format!("登录信息:用户名={}, 记住登录={}, 密码哈希(SHA-256)={}",form.username, form.remember_me, password_hash_hex)
}

测试接口

# 模拟表单提交
D:\>curl -X POST http://localhost:8080/user/login -H "Content-Type: application/x-www-form-urlencoded" -d "username=alice&password=123456&remember_me=true"
登录信息:用户名=alice, 记住登录=true, 密码哈希(SHA-256)=8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92

预期响应正确:登录信息:用户名=alice, 记住登录=true, 密码哈希(SHA-256)=8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92

3.2 关键知识点

  • Form 提取器:仅处理 application/x-www-form-urlencoded 格式,与 Json 提取器互斥(同一路由不能同时使用);

  • 布尔值处理:表单中布尔值通过字符串 "true"/"false" 传递,serde 会自动转换为 Rust 的 bool

  • 字段编码:表单数据会自动处理 URL 编码(如空格转为 +),提取器会自动解码。


四、处理文件上传:Multipart 提取器

对于包含文件的表单(如头像上传),需使用 multipart/form-data 格式,Axum 推荐结合 multer 库处理这类请求。

4.1 基础文件上传实现

步骤 1:创建文件上传处理函数

use axum::extract::Multipart;/// 处理文件上传(multipart/form-data)
async fn upload_file(mut multipart: Multipart) -> String {// 遍历multipart表单中的所有字段while let Some(field) = multipart.next_field().await.unwrap() {// 1. 先保存所有需要的元数据(此时仅借用field,不移动)let name = field.name().unwrap_or("未知字段").to_string();let filename = field.file_name().unwrap_or("未知文件名").to_string();let content_type = field.content_type().unwrap_or("未知类型").to_string();// 2. 消费field获取字节数据(可以处理文件数据,也可以保存到硬盘)let data = field.bytes().await.unwrap();// 3. 使用保存的元数据和数据长度构建响应return format!("文件上传成功!字段名:{}, 文件名:{}, 类型:{}, 大小:{}字节",name, filename, content_type, data.len());}"未找到文件字段".to_string()
}

步骤 2:注册路由

// 在user_routes中添加
fn user_routes() -> Router {Router::new().route("/register", post(register_user)).route("/login", post(login)).route("/upload", post(upload_file)) // POST /user/upload
}

程序全部代码:

// 导入必要的组件
// Axum框架核心组件:用于创建路由和处理HTTP请求
use axum::{routing::{get,post},  // 导入GET、POST请求处理函数Router,        // 导入路由构建器,用于定义请求路由规则extract::{Json,   // 从Axum框架的extract模块中导入Json提取器,用于解析HTTP请求体中Content-Type为application/json的JSON数据Form,   // 从Axum框架的extract模块中导入Form提取器Multipart   // 从Axum框架的extract模块中导入Multipart提取器,用于传输文件},
};
// 标准库中的网络地址类型,用于指定服务器绑定的IP和端口
use std::net::SocketAddr;
// 日志相关组件:用于记录信息日志和错误日志
use tracing::{info, error};
// 日志订阅器组件:用于配置和初始化日志系统
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
// 新增:导入信号处理模块
use tokio::signal;
// 导入serde库的Deserialize trait,用于实现数据反序列化
// 反序列化是指将外部数据格式(如JSON、表单数据)转换为Rust结构体的过程
// 在Axum中处理请求体(如JSON、表单)时,需要通过该trait将请求数据解析为定义的结构体
// 需配合#[derive(Deserialize)]属性使用,自动为结构体生成反序列化代码
use serde::Deserialize;// 用户注册数据(JSON格式)
#[derive(Debug, Deserialize)]
struct RegisterUser {username: String,email: String,age: Option<u8>, // 可选字段:允许为null或不存在
}// Form的数据结构体
#[derive(Debug, Deserialize)]
struct LoginForm {username: String,password: String,remember_me: bool, // 复选框类型,表单中会传递"true"或"false"
}// 异步主函数:Axum基于Tokio运行时,必须使用#[tokio::main]宏标记
// 该宏会自动设置Tokio异步运行时环境
#[tokio::main]
async fn main() {// 初始化日志系统:配置日志的过滤规则和输出格式tracing_subscriber::registry()  // 创建日志订阅器注册表.with(// 设置日志过滤规则:优先从环境变量读取,若未设置则使用默认规则// 默认规则:本程序(axum_tutorial)和tower_http库输出debug级别及以上日志tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "axum_tutorial=debug,tower_http=debug".into())).with(tracing_subscriber::fmt::layer())  // 添加日志格式化输出层.init();  // 初始化日志系统,使其生效// 记录信息日志:提示服务器开始启动info!("Starting axum server...");// 创建路由表:定义不同路径的请求由哪个函数处理let app = Router::new()// 注册根路径("/")的处理规则:GET请求由root函数处理.route("/", get(root)).nest("/user", user_routes()); // 新增用户相关路由// 定义服务器绑定的地址:0.0.0.0表示监听所有可用网络接口,端口为8080let addr = SocketAddr::from(([0, 0, 0, 0], 8080));// 记录信息日志:提示正在绑定的地址info!("Binding to address: {}", addr);// 创建TCP监听器并绑定到指定地址// 使用match表达式处理可能的绑定结果(成功/失败)let listener = match tokio::net::TcpListener::bind(addr).await {Ok(listener) => listener,  // 绑定成功:获取监听器对象Err(e) => {  // 绑定失败:记录错误并优雅退出error!("Failed to bind to address {}: {}", addr, e);return;  // 退出程序,不再继续执行}};// 启动Axum服务器:使用监听器接收请求,并通过路由表处理// 记录信息日志:提示服务器已启动并监听指定地址info!("Server started, listening on {}", addr);// 新增:创建服务器并配置优雅关闭let server = axum::serve(listener, app);// 新增:添加优雅关闭处理if let Err(e) = server.with_graceful_shutdown(shutdown_signal()).await {// 服务器运行出错时记录错误信息error!("Server error: {}", e);}// 记录信息日志:提示服务器已正常关闭info!("Server shutdown");
}/// 根路径("/")的请求处理函数
/// 异步函数:返回静态字符串作为HTTP响应体
async fn root() -> &'static str {"Hello world!"  // 响应内容:简单的"Hello world!"字符串
}// 新增:处理关闭信号的函数
async fn shutdown_signal() {// 等待Ctrl+C信号let ctrl_c = async {signal::ctrl_c().await.expect("Failed to install Ctrl+C handler");};// 在Unix系统上额外监听终止信号#[cfg(unix)]let terminate = async {signal::unix::signal(signal::unix::SignalKind::terminate()).expect("Failed to install SIGTERM handler").recv().await;};// 等待任一信号#[cfg(unix)]tokio::select! {_ = ctrl_c => {},_ = terminate => {},}#[cfg(not(unix))]ctrl_c.await;info!("Received shutdown signal, starting graceful shutdown...");
}fn user_routes() -> Router {Router::new().route("/register", post(register_user)) // POST /user/register.route("/login", post(login)) // POST /user/login.route("/upload", post(upload_file)) // POST /user/upload
}/// 处理用户注册(JSON请求体)
async fn register_user(Json(user): Json<RegisterUser>) -> String {// 解析后的user是RegisterUser实例,可直接使用其字段format!("注册成功!用户信息:用户名={}, 邮箱={}, 年龄={:?}",user.username, user.email, user.age)
}/// 处理登录表单(x-www-form-urlencoded)
use sha2::{Sha256, Digest}; // SHA-256哈希算法
use hex::encode;            // 字节转十六进制字符串
async fn login(Form(form): Form<LoginForm>) -> String {// 1. 计算密码的SHA-256哈希(将明文转为字节数组后处理)let password_hash = Sha256::digest(form.password.as_bytes());// 2. 将哈希结果(字节数组)转为十六进制字符串(便于显示)let password_hash_hex = encode(password_hash);format!("登录信息:用户名={}, 记住登录={}, 密码哈希(SHA-256)={}",form.username, form.remember_me, password_hash_hex)
}/// 处理文件上传(multipart/form-data)
async fn upload_file(mut multipart: Multipart) -> String {// 遍历multipart表单中的所有字段while let Some(field) = multipart.next_field().await.unwrap() {// 1. 先保存所有需要的元数据(此时仅借用field,不移动)let name = field.name().unwrap_or("未知字段").to_string();let filename = field.file_name().unwrap_or("未知文件名").to_string();let content_type = field.content_type().unwrap_or("未知类型").to_string();// 2. 消费field获取字节数据(可以处理文件数据,也可以保存到硬盘)let data = field.bytes().await.unwrap();// 3. 使用保存的元数据和数据长度构建响应return format!("文件上传成功!字段名:{}, 文件名:{}, 类型:{}, 大小:{}字节",name, filename, content_type, data.len());}"未找到文件字段".to_string()
}

测试接口

使用 curl 上传文件

D:\>curl -X POST http://localhost:8080/user/upload -F "avatar=@./rust_logo.jpeg"
文件上传成功!字段名:avatar, 文件名:rust_logo.jpeg, 类型:image/jpeg, 大小:14695字节

预期响应正确:文件上传成功!字段名:avatar, 文件名:rust_logo.jpeg, 类型:image/jpeg, 大小:???????字节

4.2 进阶:保存文件到磁盘

步骤 1:创建文件上传和保存函数

// 添加文件操作所需的额外导入
use std::path::Path;
use tokio::fs::{self, File};
use tokio::io::AsyncWriteExt;/// 处理文件上传(multipart/form-data)并保存到本地目录
async fn upload_save_file(mut multipart: Multipart) -> String {// 1. 定义上传目录(项目根目录下的 uploads 文件夹)let upload_dir = Path::new("./uploads");// 2. 确保上传目录存在(不存在则创建,包括父目录)match fs::create_dir_all(upload_dir).await {Ok(_) => {}, // 目录创建成功,继续Err(e) => return format!("创建上传目录失败:{}", e), // 错误提示}// 3. 遍历 Multipart 表单字段(处理外层 Result 类型)while let Ok(Some(field)) = multipart.next_field().await {// 3.1 获取文件元信息(转为 String,释放 field 借用)let field_name = field.name().unwrap_or("未知字段").to_string();let original_filename = field.file_name().unwrap_or("未知文件名").to_string(); // 拥有所有权的 Stringlet content_type = field.content_type().unwrap_or("未知类型").to_string();// 3.2 读取文件内容(此时 field 无借用,可安全移动)let file_data = match field.bytes().await {Ok(data) => data,Err(e) => return format!("读取文件内容失败(字段:{}):{}", field_name, e),};// 3.3 构建文件保存路径:用 &original_filename 借用,不转移所有权(关键修复!)let save_path = upload_dir.join(&original_filename); // 加 & 符号,借用而非移动// 3.4 检查文件是否已存在(避免覆盖)if save_path.exists() {return format!("文件已存在:{}", save_path.display());}// 3.5 创建文件并写入内容let mut file = match File::create(&save_path).await {Ok(f) => f,Err(e) => return format!("创建文件失败(路径:{}):{}", save_path.display(), e),};// 3.6 写入文件内容并同步磁盘if let Err(e) = file.write_all(&file_data).await {return format!("写入文件内容失败(路径:{}):{}", save_path.display(), e);}if let Err(e) = file.sync_all().await {return format!("同步文件到磁盘失败(路径:{}):{}", save_path.display(), e);}// 3.7 上传成功,返回详细信息(此时 original_filename 所有权仍在,可正常使用)return format!("文件上传成功!\n字段名:{}\n原始文件名:{}\n文件类型:{}\n文件大小:{} 字节\n保存路径:{}",field_name,original_filename, // 所有权未转移,可正常使用content_type,file_data.len(),save_path.display());}// 4. 遍历结束仍未找到文件字段"未在表单中找到文件字段(请确保使用 multipart/form-data 格式上传)".to_string()
}

步骤 2:注册路由

fn user_routes() -> Router {Router::new().route("/register", post(register_user)) // POST /user/register.route("/login", post(login)) // POST /user/login.route("/upload", post(upload_file)) // POST /user/upload.route("/upload_and_save", post(upload_save_file)) // POST /user/upload
}

程序全部代码:

// 导入必要的组件
// Axum框架核心组件:用于创建路由和处理HTTP请求
use axum::{routing::{get,post},  // 导入GET、POST请求处理函数Router,        // 导入路由构建器,用于定义请求路由规则extract::{Json,   // 从Axum框架的extract模块中导入Json提取器,用于解析HTTP请求体中Content-Type为application/json的JSON数据Form,   // 从Axum框架的extract模块中导入Form提取器Multipart   // 从Axum框架的extract模块中导入Multipart提取器,用于传输文件},
};
// 标准库中的网络地址类型,用于指定服务器绑定的IP和端口
use std::net::SocketAddr;
// 日志相关组件:用于记录信息日志和错误日志
use tracing::{info, error};
// 日志订阅器组件:用于配置和初始化日志系统
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
// 新增:导入信号处理模块
use tokio::signal;
// 导入serde库的Deserialize trait,用于实现数据反序列化
// 反序列化是指将外部数据格式(如JSON、表单数据)转换为Rust结构体的过程
// 在Axum中处理请求体(如JSON、表单)时,需要通过该trait将请求数据解析为定义的结构体
// 需配合#[derive(Deserialize)]属性使用,自动为结构体生成反序列化代码
use serde::Deserialize;// 用户注册数据(JSON格式)
#[derive(Debug, Deserialize)]
struct RegisterUser {username: String,email: String,age: Option<u8>, // 可选字段:允许为null或不存在
}// Form的数据结构体
#[derive(Debug, Deserialize)]
struct LoginForm {username: String,password: String,remember_me: bool, // 复选框类型,表单中会传递"true"或"false"
}// 异步主函数:Axum基于Tokio运行时,必须使用#[tokio::main]宏标记
// 该宏会自动设置Tokio异步运行时环境
#[tokio::main]
async fn main() {// 初始化日志系统:配置日志的过滤规则和输出格式tracing_subscriber::registry()  // 创建日志订阅器注册表.with(// 设置日志过滤规则:优先从环境变量读取,若未设置则使用默认规则// 默认规则:本程序(axum_tutorial)和tower_http库输出debug级别及以上日志tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "axum_tutorial=debug,tower_http=debug".into())).with(tracing_subscriber::fmt::layer())  // 添加日志格式化输出层.init();  // 初始化日志系统,使其生效// 记录信息日志:提示服务器开始启动info!("Starting axum server...");// 创建路由表:定义不同路径的请求由哪个函数处理let app = Router::new()// 注册根路径("/")的处理规则:GET请求由root函数处理.route("/", get(root)).nest("/user", user_routes()); // 新增用户相关路由// 定义服务器绑定的地址:0.0.0.0表示监听所有可用网络接口,端口为8080let addr = SocketAddr::from(([0, 0, 0, 0], 8080));// 记录信息日志:提示正在绑定的地址info!("Binding to address: {}", addr);// 创建TCP监听器并绑定到指定地址// 使用match表达式处理可能的绑定结果(成功/失败)let listener = match tokio::net::TcpListener::bind(addr).await {Ok(listener) => listener,  // 绑定成功:获取监听器对象Err(e) => {  // 绑定失败:记录错误并优雅退出error!("Failed to bind to address {}: {}", addr, e);return;  // 退出程序,不再继续执行}};// 启动Axum服务器:使用监听器接收请求,并通过路由表处理// 记录信息日志:提示服务器已启动并监听指定地址info!("Server started, listening on {}", addr);// 新增:创建服务器并配置优雅关闭let server = axum::serve(listener, app);// 新增:添加优雅关闭处理if let Err(e) = server.with_graceful_shutdown(shutdown_signal()).await {// 服务器运行出错时记录错误信息error!("Server error: {}", e);}// 记录信息日志:提示服务器已正常关闭info!("Server shutdown");
}/// 根路径("/")的请求处理函数
/// 异步函数:返回静态字符串作为HTTP响应体
async fn root() -> &'static str {"Hello world!"  // 响应内容:简单的"Hello world!"字符串
}// 新增:处理关闭信号的函数
async fn shutdown_signal() {// 等待Ctrl+C信号let ctrl_c = async {signal::ctrl_c().await.expect("Failed to install Ctrl+C handler");};// 在Unix系统上额外监听终止信号#[cfg(unix)]let terminate = async {signal::unix::signal(signal::unix::SignalKind::terminate()).expect("Failed to install SIGTERM handler").recv().await;};// 等待任一信号#[cfg(unix)]tokio::select! {_ = ctrl_c => {},_ = terminate => {},}#[cfg(not(unix))]ctrl_c.await;info!("Received shutdown signal, starting graceful shutdown...");
}fn user_routes() -> Router {Router::new().route("/register", post(register_user)) // POST /user/register.route("/login", post(login)) // POST /user/login.route("/upload", post(upload_file)) // POST /user/upload.route("/upload_and_save", post(upload_save_file)) // POST /user/upload
}/// 处理用户注册(JSON请求体)
async fn register_user(Json(user): Json<RegisterUser>) -> String {// 解析后的user是RegisterUser实例,可直接使用其字段format!("注册成功!用户信息:用户名={}, 邮箱={}, 年龄={:?}",user.username, user.email, user.age)
}/// 处理登录表单(x-www-form-urlencoded)
use sha2::{Sha256, Digest}; // SHA-256哈希算法
use hex::encode;            // 字节转十六进制字符串
async fn login(Form(form): Form<LoginForm>) -> String {// 1. 计算密码的SHA-256哈希(将明文转为字节数组后处理)let password_hash = Sha256::digest(form.password.as_bytes());// 2. 将哈希结果(字节数组)转为十六进制字符串(便于显示)let password_hash_hex = encode(password_hash);format!("登录信息:用户名={}, 记住登录={}, 密码哈希(SHA-256)={}",form.username, form.remember_me, password_hash_hex)
}/// 处理文件上传(multipart/form-data)
async fn upload_file(mut multipart: Multipart) -> String {// 遍历multipart表单中的所有字段while let Some(field) = multipart.next_field().await.unwrap() {// 1. 先保存所有需要的元数据(此时仅借用field,不移动)let name = field.name().unwrap_or("未知字段").to_string();let filename = field.file_name().unwrap_or("未知文件名").to_string();let content_type = field.content_type().unwrap_or("未知类型").to_string();// 2. 消费field获取字节数据(可以处理文件数据,也可以保存到硬盘)let data = field.bytes().await.unwrap();// 3. 使用保存的元数据和数据长度构建响应return format!("文件上传成功!字段名:{}, 文件名:{}, 类型:{}, 大小:{}字节",name, filename, content_type, data.len());}"未找到文件字段".to_string()
}// 添加文件操作所需的额外导入
use std::path::Path;
use tokio::fs::{self, File};
use tokio::io::AsyncWriteExt;/// 处理文件上传(multipart/form-data)并保存到本地目录
async fn upload_save_file(mut multipart: Multipart) -> String {// 1. 定义上传目录(项目根目录下的 uploads 文件夹)let upload_dir = Path::new("./uploads");// 2. 确保上传目录存在(不存在则创建,包括父目录)match fs::create_dir_all(upload_dir).await {Ok(_) => {}, // 目录创建成功,继续Err(e) => return format!("创建上传目录失败:{}", e), // 错误提示}// 3. 遍历 Multipart 表单字段(处理外层 Result 类型)while let Ok(Some(field)) = multipart.next_field().await {// 3.1 获取文件元信息(转为 String,释放 field 借用)let field_name = field.name().unwrap_or("未知字段").to_string();let original_filename = field.file_name().unwrap_or("未知文件名").to_string(); // 拥有所有权的 Stringlet content_type = field.content_type().unwrap_or("未知类型").to_string();// 3.2 读取文件内容(此时 field 无借用,可安全移动)let file_data = match field.bytes().await {Ok(data) => data,Err(e) => return format!("读取文件内容失败(字段:{}):{}", field_name, e),};// 3.3 构建文件保存路径:用 &original_filename 借用,不转移所有权(关键修复!)let save_path = upload_dir.join(&original_filename); // 加 & 符号,借用而非移动// 3.4 检查文件是否已存在(避免覆盖)if save_path.exists() {return format!("文件已存在:{}", save_path.display());}// 3.5 创建文件并写入内容let mut file = match File::create(&save_path).await {Ok(f) => f,Err(e) => return format!("创建文件失败(路径:{}):{}", save_path.display(), e),};// 3.6 写入文件内容并同步磁盘if let Err(e) = file.write_all(&file_data).await {return format!("写入文件内容失败(路径:{}):{}", save_path.display(), e);}if let Err(e) = file.sync_all().await {return format!("同步文件到磁盘失败(路径:{}):{}", save_path.display(), e);}// 3.7 上传成功,返回详细信息(此时 original_filename 所有权仍在,可正常使用)return format!("文件上传成功!\n字段名:{}\n原始文件名:{}\n文件类型:{}\n文件大小:{} 字节\n保存路径:{}",field_name,original_filename, // 所有权未转移,可正常使用content_type,file_data.len(),save_path.display());}// 4. 遍历结束仍未找到文件字段"未在表单中找到文件字段(请确保使用 multipart/form-data 格式上传)".to_string()
}

测试接口

使用 curl 上传文件

D:\>curl -X POST http://localhost:8080/user/upload_and_save -F "avatar=@./rust_logo.jpeg"
文件上传成功!
字段名:avatar
原始文件名:rust_logo.jpeg
文件类型:image/jpeg
文件大小:14695 字节
保存路径:./uploads\rust_logo.jpeg

与预测的结果一致。

4.3 关键知识点

  • Multipart 提取器:用于处理 multipart/form-data 格式,需在 Cargo.toml 中添加 multer 依赖;
  • 字段遍历:通过 multipart.next_field().await 逐个获取表单字段(包括普通字段和文件字段);
  • 文件安全:实际应用中需验证文件类型、限制大小,并对文件名进行 sanitize(如去除特殊字符),避免路径遍历攻击。

五、解析原始请求体:Bytes 与 String 提取器

对于非 JSON / 表单格式的请求体(如纯文本、二进制数据),可直接提取为原始字节或字符串。这时非标Web服务的常见场景。

5.1 提取为字符串(String

/// 处理纯文本请求体
async fn handle_text(body: String) -> String {format!("收到文本:{}(长度:{}字符)", body, body.len())
}

注册路由:

    .route("/text", post(handle_text))

全部代码:

// 导入必要的组件
// Axum框架核心组件:用于创建路由和处理HTTP请求
use axum::{routing::{get,post},  // 导入GET、POST请求处理函数Router,        // 导入路由构建器,用于定义请求路由规则extract::{Json,   // 从Axum框架的extract模块中导入Json提取器,用于解析HTTP请求体中Content-Type为application/json的JSON数据Form,   // 从Axum框架的extract模块中导入Form提取器Multipart   // 从Axum框架的extract模块中导入Multipart提取器,用于传输文件},
};
// 标准库中的网络地址类型,用于指定服务器绑定的IP和端口
use std::net::SocketAddr;
// 日志相关组件:用于记录信息日志和错误日志
use tracing::{info, error};
// 日志订阅器组件:用于配置和初始化日志系统
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
// 新增:导入信号处理模块
use tokio::signal;
// 导入serde库的Deserialize trait,用于实现数据反序列化
// 反序列化是指将外部数据格式(如JSON、表单数据)转换为Rust结构体的过程
// 在Axum中处理请求体(如JSON、表单)时,需要通过该trait将请求数据解析为定义的结构体
// 需配合#[derive(Deserialize)]属性使用,自动为结构体生成反序列化代码
use serde::Deserialize;// 用户注册数据(JSON格式)
#[derive(Debug, Deserialize)]
struct RegisterUser {username: String,email: String,age: Option<u8>, // 可选字段:允许为null或不存在
}// Form的数据结构体
#[derive(Debug, Deserialize)]
struct LoginForm {username: String,password: String,remember_me: bool, // 复选框类型,表单中会传递"true"或"false"
}// 异步主函数:Axum基于Tokio运行时,必须使用#[tokio::main]宏标记
// 该宏会自动设置Tokio异步运行时环境
#[tokio::main]
async fn main() {// 初始化日志系统:配置日志的过滤规则和输出格式tracing_subscriber::registry()  // 创建日志订阅器注册表.with(// 设置日志过滤规则:优先从环境变量读取,若未设置则使用默认规则// 默认规则:本程序(axum_tutorial)和tower_http库输出debug级别及以上日志tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "axum_tutorial=debug,tower_http=debug".into())).with(tracing_subscriber::fmt::layer())  // 添加日志格式化输出层.init();  // 初始化日志系统,使其生效// 记录信息日志:提示服务器开始启动info!("Starting axum server...");// 创建路由表:定义不同路径的请求由哪个函数处理let app = Router::new()// 注册根路径("/")的处理规则:GET请求由root函数处理.route("/", get(root)).nest("/user", user_routes()); // 新增用户相关路由// 定义服务器绑定的地址:0.0.0.0表示监听所有可用网络接口,端口为8080let addr = SocketAddr::from(([0, 0, 0, 0], 8080));// 记录信息日志:提示正在绑定的地址info!("Binding to address: {}", addr);// 创建TCP监听器并绑定到指定地址// 使用match表达式处理可能的绑定结果(成功/失败)let listener = match tokio::net::TcpListener::bind(addr).await {Ok(listener) => listener,  // 绑定成功:获取监听器对象Err(e) => {  // 绑定失败:记录错误并优雅退出error!("Failed to bind to address {}: {}", addr, e);return;  // 退出程序,不再继续执行}};// 启动Axum服务器:使用监听器接收请求,并通过路由表处理// 记录信息日志:提示服务器已启动并监听指定地址info!("Server started, listening on {}", addr);// 新增:创建服务器并配置优雅关闭let server = axum::serve(listener, app);// 新增:添加优雅关闭处理if let Err(e) = server.with_graceful_shutdown(shutdown_signal()).await {// 服务器运行出错时记录错误信息error!("Server error: {}", e);}// 记录信息日志:提示服务器已正常关闭info!("Server shutdown");
}/// 根路径("/")的请求处理函数
/// 异步函数:返回静态字符串作为HTTP响应体
async fn root() -> &'static str {"Hello world!"  // 响应内容:简单的"Hello world!"字符串
}// 新增:处理关闭信号的函数
async fn shutdown_signal() {// 等待Ctrl+C信号let ctrl_c = async {signal::ctrl_c().await.expect("Failed to install Ctrl+C handler");};// 在Unix系统上额外监听终止信号#[cfg(unix)]let terminate = async {signal::unix::signal(signal::unix::SignalKind::terminate()).expect("Failed to install SIGTERM handler").recv().await;};// 等待任一信号#[cfg(unix)]tokio::select! {_ = ctrl_c => {},_ = terminate => {},}#[cfg(not(unix))]ctrl_c.await;info!("Received shutdown signal, starting graceful shutdown...");
}fn user_routes() -> Router {Router::new().route("/register", post(register_user)) // POST /user/register.route("/login", post(login)) // POST /user/login.route("/upload", post(upload_file)) // POST /user/upload.route("/upload_and_save", post(upload_save_file)) // POST /user/upload.route("/text", post(handle_text))
}/// 处理用户注册(JSON请求体)
async fn register_user(Json(user): Json<RegisterUser>) -> String {// 解析后的user是RegisterUser实例,可直接使用其字段format!("注册成功!用户信息:用户名={}, 邮箱={}, 年龄={:?}",user.username, user.email, user.age)
}/// 处理登录表单(x-www-form-urlencoded)
use sha2::{Sha256, Digest}; // SHA-256哈希算法
use hex::encode;            // 字节转十六进制字符串
async fn login(Form(form): Form<LoginForm>) -> String {// 1. 计算密码的SHA-256哈希(将明文转为字节数组后处理)let password_hash = Sha256::digest(form.password.as_bytes());// 2. 将哈希结果(字节数组)转为十六进制字符串(便于显示)let password_hash_hex = encode(password_hash);format!("登录信息:用户名={}, 记住登录={}, 密码哈希(SHA-256)={}",form.username, form.remember_me, password_hash_hex)
}/// 处理文件上传(multipart/form-data)
async fn upload_file(mut multipart: Multipart) -> String {// 遍历multipart表单中的所有字段while let Some(field) = multipart.next_field().await.unwrap() {// 1. 先保存所有需要的元数据(此时仅借用field,不移动)let name = field.name().unwrap_or("未知字段").to_string();let filename = field.file_name().unwrap_or("未知文件名").to_string();let content_type = field.content_type().unwrap_or("未知类型").to_string();// 2. 消费field获取字节数据(可以处理文件数据,也可以保存到硬盘)let data = field.bytes().await.unwrap();// 3. 使用保存的元数据和数据长度构建响应return format!("文件上传成功!字段名:{}, 文件名:{}, 类型:{}, 大小:{}字节",name, filename, content_type, data.len());}"未找到文件字段".to_string()
}// 添加文件操作所需的额外导入
use std::path::Path;
use tokio::fs::{self, File};
use tokio::io::AsyncWriteExt;/// 处理文件上传(multipart/form-data)并保存到本地目录
async fn upload_save_file(mut multipart: Multipart) -> String {// 1. 定义上传目录(项目根目录下的 uploads 文件夹)let upload_dir = Path::new("./uploads");// 2. 确保上传目录存在(不存在则创建,包括父目录)match fs::create_dir_all(upload_dir).await {Ok(_) => {}, // 目录创建成功,继续Err(e) => return format!("创建上传目录失败:{}", e), // 错误提示}// 3. 遍历 Multipart 表单字段(处理外层 Result 类型)while let Ok(Some(field)) = multipart.next_field().await {// 3.1 获取文件元信息(转为 String,释放 field 借用)let field_name = field.name().unwrap_or("未知字段").to_string();let original_filename = field.file_name().unwrap_or("未知文件名").to_string(); // 拥有所有权的 Stringlet content_type = field.content_type().unwrap_or("未知类型").to_string();// 3.2 读取文件内容(此时 field 无借用,可安全移动)let file_data = match field.bytes().await {Ok(data) => data,Err(e) => return format!("读取文件内容失败(字段:{}):{}", field_name, e),};// 3.3 构建文件保存路径:用 &original_filename 借用,不转移所有权(关键修复!)let save_path = upload_dir.join(&original_filename); // 加 & 符号,借用而非移动// 3.4 检查文件是否已存在(避免覆盖)if save_path.exists() {return format!("文件已存在:{}", save_path.display());}// 3.5 创建文件并写入内容let mut file = match File::create(&save_path).await {Ok(f) => f,Err(e) => return format!("创建文件失败(路径:{}):{}", save_path.display(), e),};// 3.6 写入文件内容并同步磁盘if let Err(e) = file.write_all(&file_data).await {return format!("写入文件内容失败(路径:{}):{}", save_path.display(), e);}if let Err(e) = file.sync_all().await {return format!("同步文件到磁盘失败(路径:{}):{}", save_path.display(), e);}// 3.7 上传成功,返回详细信息(此时 original_filename 所有权仍在,可正常使用)return format!("文件上传成功!\n字段名:{}\n原始文件名:{}\n文件类型:{}\n文件大小:{} 字节\n保存路径:{}",field_name,original_filename, // 所有权未转移,可正常使用content_type,file_data.len(),save_path.display());}// 4. 遍历结束仍未找到文件字段"未在表单中找到文件字段(请确保使用 multipart/form-data 格式上传)".to_string()
}/// 处理纯文本请求体
async fn handle_text(body: String) -> String {format!("收到文本:{}(长度:{}字符)", body, body.len())
}

测试:

D:\>curl -X POST http://localhost:8080/user/text -H "Content-Type: text/plain" -d "hello raw text"
收到文本:hello raw text(长度:14字符)

与预测结果一致。

5.2 提取为字节(Bytes

/// 处理二进制请求体
use axum::body::Bytes;
async fn handle_binary(body: Bytes) -> String {format!("收到二进制数据,长度:{}字节", body.len())
}

注册路由:

// 注册路由.route("/binary", post(handle_binary))

完整代码:

// 导入必要的组件
// Axum框架核心组件:用于创建路由和处理HTTP请求
use axum::{routing::{get,post},  // 导入GET、POST请求处理函数Router,        // 导入路由构建器,用于定义请求路由规则extract::{Json,   // 从Axum框架的extract模块中导入Json提取器,用于解析HTTP请求体中Content-Type为application/json的JSON数据Form,   // 从Axum框架的extract模块中导入Form提取器Multipart   // 从Axum框架的extract模块中导入Multipart提取器,用于传输文件},
};
// 标准库中的网络地址类型,用于指定服务器绑定的IP和端口
use std::net::SocketAddr;
// 日志相关组件:用于记录信息日志和错误日志
use tracing::{info, error};
// 日志订阅器组件:用于配置和初始化日志系统
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
// 新增:导入信号处理模块
use tokio::signal;
// 导入serde库的Deserialize trait,用于实现数据反序列化
// 反序列化是指将外部数据格式(如JSON、表单数据)转换为Rust结构体的过程
// 在Axum中处理请求体(如JSON、表单)时,需要通过该trait将请求数据解析为定义的结构体
// 需配合#[derive(Deserialize)]属性使用,自动为结构体生成反序列化代码
use serde::Deserialize;// 用户注册数据(JSON格式)
#[derive(Debug, Deserialize)]
struct RegisterUser {username: String,email: String,age: Option<u8>, // 可选字段:允许为null或不存在
}// Form的数据结构体
#[derive(Debug, Deserialize)]
struct LoginForm {username: String,password: String,remember_me: bool, // 复选框类型,表单中会传递"true"或"false"
}// 异步主函数:Axum基于Tokio运行时,必须使用#[tokio::main]宏标记
// 该宏会自动设置Tokio异步运行时环境
#[tokio::main]
async fn main() {// 初始化日志系统:配置日志的过滤规则和输出格式tracing_subscriber::registry()  // 创建日志订阅器注册表.with(// 设置日志过滤规则:优先从环境变量读取,若未设置则使用默认规则// 默认规则:本程序(axum_tutorial)和tower_http库输出debug级别及以上日志tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "axum_tutorial=debug,tower_http=debug".into())).with(tracing_subscriber::fmt::layer())  // 添加日志格式化输出层.init();  // 初始化日志系统,使其生效// 记录信息日志:提示服务器开始启动info!("Starting axum server...");// 创建路由表:定义不同路径的请求由哪个函数处理let app = Router::new()// 注册根路径("/")的处理规则:GET请求由root函数处理.route("/", get(root)).nest("/user", user_routes()); // 新增用户相关路由// 定义服务器绑定的地址:0.0.0.0表示监听所有可用网络接口,端口为8080let addr = SocketAddr::from(([0, 0, 0, 0], 8080));// 记录信息日志:提示正在绑定的地址info!("Binding to address: {}", addr);// 创建TCP监听器并绑定到指定地址// 使用match表达式处理可能的绑定结果(成功/失败)let listener = match tokio::net::TcpListener::bind(addr).await {Ok(listener) => listener,  // 绑定成功:获取监听器对象Err(e) => {  // 绑定失败:记录错误并优雅退出error!("Failed to bind to address {}: {}", addr, e);return;  // 退出程序,不再继续执行}};// 启动Axum服务器:使用监听器接收请求,并通过路由表处理// 记录信息日志:提示服务器已启动并监听指定地址info!("Server started, listening on {}", addr);// 新增:创建服务器并配置优雅关闭let server = axum::serve(listener, app);// 新增:添加优雅关闭处理if let Err(e) = server.with_graceful_shutdown(shutdown_signal()).await {// 服务器运行出错时记录错误信息error!("Server error: {}", e);}// 记录信息日志:提示服务器已正常关闭info!("Server shutdown");
}/// 根路径("/")的请求处理函数
/// 异步函数:返回静态字符串作为HTTP响应体
async fn root() -> &'static str {"Hello world!"  // 响应内容:简单的"Hello world!"字符串
}// 新增:处理关闭信号的函数
async fn shutdown_signal() {// 等待Ctrl+C信号let ctrl_c = async {signal::ctrl_c().await.expect("Failed to install Ctrl+C handler");};// 在Unix系统上额外监听终止信号#[cfg(unix)]let terminate = async {signal::unix::signal(signal::unix::SignalKind::terminate()).expect("Failed to install SIGTERM handler").recv().await;};// 等待任一信号#[cfg(unix)]tokio::select! {_ = ctrl_c => {},_ = terminate => {},}#[cfg(not(unix))]ctrl_c.await;info!("Received shutdown signal, starting graceful shutdown...");
}fn user_routes() -> Router {Router::new().route("/register", post(register_user)) // POST /user/register.route("/login", post(login)) // POST /user/login.route("/upload", post(upload_file)) // POST /user/upload.route("/upload_and_save", post(upload_save_file)) // POST /user/upload.route("/text", post(handle_text)).route("/binary", post(handle_binary))
}/// 处理用户注册(JSON请求体)
async fn register_user(Json(user): Json<RegisterUser>) -> String {// 解析后的user是RegisterUser实例,可直接使用其字段format!("注册成功!用户信息:用户名={}, 邮箱={}, 年龄={:?}",user.username, user.email, user.age)
}/// 处理登录表单(x-www-form-urlencoded)
use sha2::{Sha256, Digest}; // SHA-256哈希算法
use hex::encode;            // 字节转十六进制字符串
async fn login(Form(form): Form<LoginForm>) -> String {// 1. 计算密码的SHA-256哈希(将明文转为字节数组后处理)let password_hash = Sha256::digest(form.password.as_bytes());// 2. 将哈希结果(字节数组)转为十六进制字符串(便于显示)let password_hash_hex = encode(password_hash);format!("登录信息:用户名={}, 记住登录={}, 密码哈希(SHA-256)={}",form.username, form.remember_me, password_hash_hex)
}/// 处理文件上传(multipart/form-data)
async fn upload_file(mut multipart: Multipart) -> String {// 遍历multipart表单中的所有字段while let Some(field) = multipart.next_field().await.unwrap() {// 1. 先保存所有需要的元数据(此时仅借用field,不移动)let name = field.name().unwrap_or("未知字段").to_string();let filename = field.file_name().unwrap_or("未知文件名").to_string();let content_type = field.content_type().unwrap_or("未知类型").to_string();// 2. 消费field获取字节数据(可以处理文件数据,也可以保存到硬盘)let data = field.bytes().await.unwrap();// 3. 使用保存的元数据和数据长度构建响应return format!("文件上传成功!字段名:{}, 文件名:{}, 类型:{}, 大小:{}字节",name, filename, content_type, data.len());}"未找到文件字段".to_string()
}// 添加文件操作所需的额外导入
use std::path::Path;
use tokio::fs::{self, File};
use tokio::io::AsyncWriteExt;/// 处理文件上传(multipart/form-data)并保存到本地目录
async fn upload_save_file(mut multipart: Multipart) -> String {// 1. 定义上传目录(项目根目录下的 uploads 文件夹)let upload_dir = Path::new("./uploads");// 2. 确保上传目录存在(不存在则创建,包括父目录)match fs::create_dir_all(upload_dir).await {Ok(_) => {}, // 目录创建成功,继续Err(e) => return format!("创建上传目录失败:{}", e), // 错误提示}// 3. 遍历 Multipart 表单字段(处理外层 Result 类型)while let Ok(Some(field)) = multipart.next_field().await {// 3.1 获取文件元信息(转为 String,释放 field 借用)let field_name = field.name().unwrap_or("未知字段").to_string();let original_filename = field.file_name().unwrap_or("未知文件名").to_string(); // 拥有所有权的 Stringlet content_type = field.content_type().unwrap_or("未知类型").to_string();// 3.2 读取文件内容(此时 field 无借用,可安全移动)let file_data = match field.bytes().await {Ok(data) => data,Err(e) => return format!("读取文件内容失败(字段:{}):{}", field_name, e),};// 3.3 构建文件保存路径:用 &original_filename 借用,不转移所有权(关键修复!)let save_path = upload_dir.join(&original_filename); // 加 & 符号,借用而非移动// 3.4 检查文件是否已存在(避免覆盖)if save_path.exists() {return format!("文件已存在:{}", save_path.display());}// 3.5 创建文件并写入内容let mut file = match File::create(&save_path).await {Ok(f) => f,Err(e) => return format!("创建文件失败(路径:{}):{}", save_path.display(), e),};// 3.6 写入文件内容并同步磁盘if let Err(e) = file.write_all(&file_data).await {return format!("写入文件内容失败(路径:{}):{}", save_path.display(), e);}if let Err(e) = file.sync_all().await {return format!("同步文件到磁盘失败(路径:{}):{}", save_path.display(), e);}// 3.7 上传成功,返回详细信息(此时 original_filename 所有权仍在,可正常使用)return format!("文件上传成功!\n字段名:{}\n原始文件名:{}\n文件类型:{}\n文件大小:{} 字节\n保存路径:{}",field_name,original_filename, // 所有权未转移,可正常使用content_type,file_data.len(),save_path.display());}// 4. 遍历结束仍未找到文件字段"未在表单中找到文件字段(请确保使用 multipart/form-data 格式上传)".to_string()
}/// 处理纯文本请求体
async fn handle_text(body: String) -> String {format!("收到文本:{}(长度:{}字符)", body, body.len())
}/// 处理二进制请求体
use axum::body::Bytes;
async fn handle_binary(body: Bytes) -> String {format!("收到二进制数据,长度:{}字节", body.len())
}

测试:

D:\>curl -X POST http://localhost:8080/user/binary -H "Content-Type: application/octet-stream" --data-binary @./test.bin
收到二进制数据,长度:10字节

与预期结果相同。

5.3 关键知识点

  • String 提取器:自动将请求体按 UTF-8 编码解析为字符串,若编码错误返回 400
  • Bytes 提取器:直接获取原始字节数据,适用于二进制文件、自定义协议等场景;
  • Content-Type:这两种提取器不验证 Content-Type,需自行处理不同格式的数据。

http://www.dtcms.com/a/348475.html

相关文章:

  • 【Python NTLK自然语言处理库】
  • 数学建模-线性规划(LP)
  • GPT-5国内免费体验
  • 【Android】从一个AndroidRuntime看类的加载
  • Unreal Engine 下载与安装全指南:从入门到配置详解
  • 淘宝API实战应用:数据驱动商品信息实时监控与增长策略
  • 13种常见机器学习算法面试总结(含问题与优质回答)
  • 【209页PPT】P2ITSP新奥IT战略规划架构设计报告(附下载方式)
  • Python基础之运算符
  • Vue3 学习教程,从入门到精通,基于 Vue3 + Element Plus + ECharts + JavaScript 开发图片素材库网站(46)
  • 塔能科技物联精准节能如何构建智慧路灯免疫系统
  • 【软考选择】系分和架构哪个好考?适合什么样的人?
  • 简历书写指南
  • [创业之路-560]:机械、电气、自控、电子、软件、信息、通信、大数据、人工智能,上述技术演进过程
  • Linux shell脚本数值计算与条件执行
  • 基于php的萌宠社区网站的设计与实现、基于php的宠物社区论坛的设计与实现
  • 手写MyBatis第32弹-设计模式实战:Builder模式在MyBatis框架中的精妙应用
  • Wagtail CRX 的 Latest Pages Block 高级设置 模版v3.0 以后被阉割了
  • 基于深度学习的阿尔茨海默症MRI图像分类系统
  • CVPR2025丨遥感领域,全模态与秒超高清遥感建模重大突破,性能提升创新点
  • 人工智能-python-深度学习-自动微分
  • MySQL數據庫開發教學(二) 核心概念、重要指令
  • Run-Command:高效便捷的命令行工具
  • 46.【.NET8 实战--孢子记账--从单体到微服务--转向微服务】--扩展功能--集成网关--网关集成日志
  • ArticulateX:通过发音器官空间实现端到端单语语音翻译的突破
  • Vue vs React:前端框架的差异与选择
  • LabVIEW调用MATLAB 的分形生成
  • AMD KFD驱动分析系列0:HSA(异构系统架构)驱动概览
  • 海盗王3.0客户端从32位升级64位之路
  • Redis如何高效安全的遍历所有key?