找回密码
立即注册
搜索
发回帖 发新帖

5100

积分

0

好友

652

主题
发表于 昨天 23:08 | 查看: 9| 回复: 0

平时闲暇时,不少程序员喜欢写点小项目:网页、桌面应用、App、小程序……写完以后总想部署到服务器上,给别人看看效果,或者提供对外服务。

对外服务通常的做法,是把应用后端的核心部分部署到一台服务器上。可国内服务器资源并不便宜。我个人以前习惯用 java + springboot 快速写业务,但 Java 本身比较占内存,SpringBoot 作为其中的 starter 框架更是吃内存大户,哪怕只是一个很简单的 demo。

两只狗在雨天撑伞的幽默动图

当然,对企业来说内存不算什么大问题。但对我个人来说,问题就大了:我服务器都是每年 618、双十一 各大云服务商搞活动时捡的“破烂”,配置基本就是 1核1G 这种。一台服务器上,跑不了几个小 demo。

所以我开始尝试换语言开发,先后看了 Go 和 Rust,最后选择了 Rust。Rust 这几年热度不低,它是一门速度极快、高性能、注重内存安全的静态编译型语言。而 Axum 是 Rust 生态中一个专注于性能和简单性的 Web框架,用起来很顺手,也符合我的需求。在官方文档里踩了不少坑的同时,我顺手整理了一系列教程;目前网上关于 Axum 的资料也不多,就免费分享出来。

下面,我们就从本文开始 Rust + Axum 的 Web 开发之旅。建议最好懂一点 Rust 基础,不用难,多入几次门自然就熟了。

什么是 Axum?

Axum 利用了 hyper 库的功能来增强 Web 应用程序的速度和并发性。同时,Axum 通过与 Tokio 库集成,将 Rust 的 async/await 能力推到了前台,让开发者可以开发高性能的异步 API 和 Web 应用程序。

Rust 与 Axum 框架组合示意图

Axum 是一个注重人体工程学和模块化的 Web 应用程序框架,它有几个主要特点:

  • 使用无宏的 API 实现路由(router)功能
  • 使用提取器(extractor)对请求进行声明式解析
  • 简单、可预测的错误处理模式
  • 用最少的模板生成响应
  • 充分利用 tower 和 tower-http 的中间件、服务和工具生态系统

Axum 的基本功能基于 Tokio runtime,这给了 Rust 管理非阻塞、事件驱动活动的能力。这种能力对于平稳处理多个并发进程至关重要。

Axum 与现有框架不同的地方在于,它没有自己的中间件系统,而是直接使用 tower::Service。这意味着 Axum 可以无成本地获得超时、跟踪、压缩、授权等能力,还能与使用 hyper 或 tonic 编写的应用共享中间件。

此外,Axum 基于 Rust 强大的类型系统和所有权规则构建,这些规则在编译时就能防止数据竞争、内存泄漏等常见 Web 开发陷阱。模块化的设计也允许开发者只引入必要组件,创建出轻量、专注的应用。

创建一个项目

首先创建一个 Rust 项目。这里默认你已经装好 Rust 开发环境,具体安装步骤可以直接去官网查看。

cargo new rust_axum_web_guide

编写第一个 hello world

先在 Cargo.toml 中添加依赖:

[package]
name = "rust_axum_web_guide"
version = "0.1.0"
edition = "2021"

[dependencies]
axum = "0.7.9"
tokio = { version = "1.0", features = ["full"] }

axum 选择当前最新的 0.7.9 版本;再引入 Tokio,以便充分利用它的生态。

接下来用官网的 hello world 示例改写 main.rs:

use axum::{
    routing::get,
    Router,
};

#[tokio::main]
async fn main() {
    // build our application with a single route
    let app = Router::new().route("/", get(|| async { "Hello, World!" }));

    // run our app with hyper, listening globally on port 3000
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

其中,tokio::main 属性宏表明 Axum 其实就是一个 Tokio 应用。Tokio 是 Rust 非常优秀的 异步运行时框架,提供了编写异步网络服务所需的几乎所有功能。

Router::new().route 创建路由,tokio::net::TcpListener::bind 监听服务器地址,并把生成的 listener 传入 axum::serve(),启动服务。

启动项目:

cargo run

编译并运行当前项目,这个命令还会自动下载依赖。如果一切顺利,终端会输出 Running on http://localhost:3000。在浏览器中访问 http://localhost:3000,或者用 curl 访问,页面会显示 Hello, World!。

路由与处理器

Router 用来设置哪些路径指向哪些服务,它是一个用于组合处理程序和服务的结构体。路由机制负责把传入的 HTTP 请求定向到指定的 handler,而 handler 就是应用逻辑真正存在的地方。

先看一个例子:

use axum::{
    routing::get,
    Router,
};

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/", get(|| async { "Hello, World!" }))
        .route("/foo", get(get_foo).post(post_foo))
        .route("/foo/bar", get(foo_bar));

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

async fn get_foo() -> String {
    return String::from("get请求foo方法")
}

async fn post_foo() -> String {
    return String::from("post请求foo方法")
}

async fn foo_bar() -> String {
    return String::from("get请求->foo_bar")
}

路由和 handler 绑定后,handler 类似 Java Spring 中的 controller。处理器 handler 是一个异步函数或异步代码块,它接受零个或多个 extractors 作为参数,并返回一些可以转换为 IntoResponse 的内容。

也可以将路由和多个 handler 绑定,比如 .route("/foo", get(get_foo).post(post_foo)) 同时绑定了 GET 和 POST 方法;当然还可以指定 PUT、DELETE 等。

处理器更详细的信息,可见 axum::handler 文档。

用 postman 访问 /foo,分别发送 GET 或 POST 请求,可以得到不同的结果。

如果有多个完全相同的路由,程序会 panic。

路由更详细信息,可见 Router in axum 文档。

use axum::{routing::get, Router};

let app = Router::new()
    .route("/", get(|| async {}))
    .route("/", get(|| async {}));

提取器

提取器 Extractors 用于分离传入请求,以获得处理程序所需的部分,比如解析异步函数的参数。

捕获动态 URL 值以及查询参数

先看如何捕获动态 URL 值和查询参数,主要使用 path 与 query 提取器。

use axum::{
    routing::get,
    Router,
    extract::{Path, Query}
};
use std::collections::HashMap;
use serde::Deserialize;

#[derive(Deserialize)]
struct Page {
    number: u32,
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/", get(|| async { "Hello, World!" }))
        .route("/item/:id", get(print_item))
        .route("/item2/:name/:age", get(print_item_handler2))
        .route("/item3/:fullpath", get(print_item_handler3))
        .route("/query/:id", get(print_query));

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

async fn print_item(Path(_id): Path<u32>) -> String {
    format!("print_item: {}", _id)
}

async fn print_item_handler2(Path((name, age)): Path<(String, i64)>) -> String {
    format!("name: {name}, age: {age}")
}

async fn print_item_handler3(Path(fullpath): Path<HashMap<String, String>>) -> String {
    format!("path: /{fullpath:?}")
}

async fn print_query(Path(id): Path<u32>, Query(page): Query<Page>) -> String {
    format!("Query {} on page {}", id, page.number)
}

还需要添加依赖来支持序列化:

serde = { version = "1.0.216", features = ["derive"] }

测试结果:

/item/321            # 结果 print_item: 321
/item2/zj/18         # 结果 name: zj, age: 18
/item3/{name: zzjj}  # 结果 path: /{"fullpath": "{name: zzjj}"}
/query/10?number=31  # 结果 Query 10 on page 31

我们还能将多个参数写入到 hashmap 中:

use axum::{
    routing::get,
    Router,
    extract::Query
};
use serde::Deserialize;
use std::collections::HashMap;

#[derive(Deserialize, Debug)]
struct Info {
    name: String,
    age: u8,
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/", get(|| async { "Hello, World!" }))
        .route("/query_1", get(print_map_handler))
        .route("/query_2", get(print_map2_handler));

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

async fn print_map_handler(query: Query<Info>) -> String {
    let info: Info = query.0;
    format!("query1: {info:?}")
}

async fn print_map2_handler(query: Query<HashMap<String, String>>) -> String {
    format!("query2: {query:?}")
}

测试结果:

/query_1?name=zj&age=18  # result query1: Info { name: "zj", age: 18 }
/query_2?name=zj&age=18  # result query2: Query({"age": "18", "name": "zj"})

path 和 query 提取器一般适用于 GET 请求,那 POST 请求的参数呢?

捕获请求体的参数

常见的请求体参数一般有两种格式:json 和 form。下面看一个例子:

use axum::{
    routing::{get, post},
    Router,
    extract::{Json, Form}
};
use serde::Deserialize;

#[derive(Deserialize, Debug)]
struct CreateUser {
    username: String,
    age: u8
}

#[derive(Deserialize, Debug)]
struct LoginFormData {
    field_name: String,
    field_pwd: String,
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/", get(|| async { "Hello, World!" }))
        .route("/addUser", post(create_user_handler))
        .route("/submit", post(submit_form_handler));

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

async fn create_user_handler(Json(payload): Json<CreateUser>) -> String {
    format!("Added user: {payload:?}")
}

async fn submit_form_handler(Form(data): Form<LoginFormData>) -> String {
    format!("submit form: {data:?}")
}

测试结果:

post /addUser   # result  Added user: CreateUser { username: "zsan", age: 18 }
post /submit    # result  submit form: LoginFormData { field_name: "lis", field_pwd: "123" }

捕获请求头参数

一般通过 HeaderMap 来获取请求头参数,它会提取所有标头;如果想获取指定标头,需要再做一层处理。

use axum::{
    routing::get,
    Router,
    http::header::HeaderMap
};

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/", get(|| async { "Hello, World!" }))
        .route("/header", get(get_header_handler));

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

async fn get_header_handler(headers: HeaderMap) -> String {
    // format!("{:?}", headers) // 获取所有请求头
    headers
        .get(axum::http::header::USER_AGENT)
        .and_then(|v| v.to_str().ok())
        .map(|v| v.to_string())
        .unwrap()
}

测试结果:

(get) /header  # result  PostmanRuntime/7.43.0

除此之外,axum 还提供了许多有用的提取器,比如 Bytes、String、Body、BodyStream 用于获取请求正文。你也可以通过实现 FromRequest 来定义自己的提取器。

响应处理

任何实现 IntoResponse 的内容,都可以作为处理程序的返回值,它会被自动转换为响应。看一个例子:

use axum::{
    body::Body,
    routing::get,
    response::Json,
    Router,
};
use serde_json::{Value, json};

// `&'static str` becomes a `200 OK` with `content-type: text/plain; charset=utf-8`
async fn plain_text() -> &'static str {
    "foo"
}

// `Json` gives a content-type of `application/json` and works with any type
// that implements `serde::Serialize`
async fn json() -> Json<Value> {
    Json(json!({ "data": 42 }))
}

let app = Router::new()
    .route("/plain_text", get(plain_text))
    .route("/json", get(json));

更多详细例子,可以看官网文档。

错误处理

Axum 的目标是拥有一个简单且可预测的错误处理模型,这意味着将错误转换为响应很简单,并且可以保证所有错误都得到处理。

Axum 基于 tower::Service,它通过关联的错误类型来处理错误。如果你的 Service 产生错误,并且该错误一路传到 hyper,连接就会终止而不发送响应。这通常不可取。为此 Axum 要求所有服务都以 Infallible 作为错误类型——Infallible 表示错误永远不会发生。

这段引用自官方文档,但太绕了。说人话就是:发生错误时,不能让 HTTP 连接中断,否则响应无法正常返回;而是要保证响应能正常返回,并携带错误类型和错误信息。Axum 能很轻易地做到这点。

所以一般 Axum 处理程序绑定的方法会这样定义:

use axum::http::StatusCode;

async fn handler() -> Result<String, StatusCode> {
    // ...
}

虽然看起来可能因为 StatusCode 失败,但实际上这并不是一个真正的“错误”。如果此处理程序返回 Err(some_status_code),它仍会被转换为响应并返回给客户端。这是通过 StatusCode 实现的 IntoResponse 完成的。

无论你是返回 Err(StatusCode::NOT_FOUND) 还是 Err(StatusCode::INTERNAL_SERVER_ERROR),在 Axum 中这些都不被视为错误。

不过,更推荐的做法不是直接使用 StatusCode,而是使用中间的错误类型,最终再转换为响应。这样就能在处理程序中使用 ? 运算符。

看一个例子:

use axum::{
    routing::get,
    Router,
    body::Body,
    http::{Response, StatusCode},
    error_handling::HandleError,
};
use tower;

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/", get(|| async { "Hello, World!" }))
        .merge(router_fallible_service()) // 使用 Service 的错误处理
        .route("/foo", get(get_foo));

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

fn router_fallible_service() -> Router {
    // this service 可能出现任何错误
    let some_fallible_service = tower::service_fn(|_req| async {
        thing_that_might_fail().await?;
        Ok::<_, anyhow::Error>(Response::new(Body::empty()))
    });

    Router::new().route_service(
        "/test_error",
        // Service 适配器通过将错误转换为响应来处理错误。
        HandleError::new(some_fallible_service, handle_anyhow_error),
    )
}

async fn thing_that_might_fail() -> Result<(), anyhow::Error> {
    // 模拟报错
    anyhow::bail!("thing_that_might_fail")
}

// 将错误转化为 IntoResponse
async fn handle_anyhow_error(err: anyhow::Error) -> (StatusCode, String) {
    (
        StatusCode::INTERNAL_SERVER_ERROR,
        format!("Something went wrong: {err}"),
    )
}

async fn get_foo() -> Result<String, StatusCode> {
    Ok(String::from("get请求foo方法"))
}

大家可以自行运行一下,看看返回的响应。要正常运行上述示例,还需要添加依赖:

anyhow = "1.0.95"
tower = { version = "0.5.2", features = ["full"] }

更多其他用法,请移步 axum::error_handling 文档。

中间件

Axum 的独特之处在于它没有自己的定制中间件系统,而是与 Tower 集成。这意味着 tower 和 tower-http 的中间件都可以与 Axum 一起使用。axum 也可以将请求路由到任何 tower 服务,可以是你用 service_fn 编写的服务,也可以是其他 crate 的东西;从而充分复用和利用不同应用的生态,潜力巨大。

中间件允许你在请求到达处理程序之前或之后添加自定义逻辑,这为身份验证、日志记录、性能监控等通用能力提供了强大的实现方式。

这里举一个简单的例子:

use axum::{
    body::Body,
    http::Request,
    middleware::{self, Next},
    response::Response,
    routing::get,
    Router
};

async fn logging_middleware(req: Request<Body>, next: Next) -> Response {
    println!("Received a request to {}", req.uri());
    next.run(req).await
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/", get(|| async { "Hello, world!" }))
        .layer(middleware::from_fn(logging_middleware));

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

运行后,用户访问任意请求都会被终端记录下来:

Received a request to /foo
Received a request to /
Received a request to /test

更多例子,请移步 axum::middleware 文档。

本文就先到这里。一个 Web 框架基本解决的就是文章中提到的这些方面:路由、提取器、响应、错误处理和中间件。后续我们会继续探索 Axum 与数据库的集成,以及其他有意思的特性。下期再见。

如果你对 Rust 后端开发感兴趣,也可以到 云栈社区 逛逛,那里有更多 Rust 全栈与后端方向的学习资源。

参考资料:




上一篇:Rust Axum Web 开发入门:路由、提取器、日志追踪与配置读取全流程
下一篇:服务器被攻击后修改 SSH 端口,导致 Hexo 部署失败排查记录
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-10-22 08:10 , Processed in 0.095896 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表