Skip to content

Rust+Axum高性能web系列-从入门到速通教程

哈喽,大家好呀,我是呼噜噜,平时闲暇地时候我们程序员会写一些小项目,比如网页、桌面、app、小程序等等,写完后会想和大家展示,这就有对外服务的需求

如何对外服务呢?我们一般会把的应用的后端核心部分,部署到服务器上

这就会有个问题?国内的服务器资源属实不便宜,我个人习惯用java+springboot来快速写业务,Java就比较占内存,而其starter框架SpringBoot更会吃大量的内存,哪怕是很简单的demo

当然对于企业来说内存不算什么问题,但对我个人来说,问题就大了,我的服务器都是每年618、双十一,各大云服务商搞活动时,捡的"破烂"(每年需要打包搬家~~)

配置基本就是1核1G的这种,一台服务器上,都跑不了几个小demo~

所以我就一直在尝试用其他语言来开发,比如Go、Rust,最后选择了Rust,Rust这个语言近几年也比较火,Rust是一种速度极快、高性能、注重内存安全的静态编译型语言;而Axum 又是 Rust 语言中的一个专注于性能和简单性的Web框架,试试感觉还不错,符合我的需求,在官方文档里踩坑的同时也就顺手写了一系列的教程,另外如今网上相关资料也不多,这里就免费分享给大家了

那让我们从本文开始Rust+Axum来进Web开发之旅~(最好会一点rust基础,不难,多入几次门就好~)

什么是Axum?

Axum 它利用了 hyper 库的功能来增强Web应用程序的速度和并发性。Axum 还通过与 Tokio 库集成,将 Rustasync/await 功能推到了前台,使得开发者可以开发高性能的异步 APIWeb 应用程序。

Axum是一个专注于人体工程学和模块化的 Web 应用程序框架,rust它的一些特点:

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

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

Axum 与现有框架不同的地方。Axum 没有自己的中间件系统,而是使用tower::Service。这意味着 axum 可以无成本地获得超时、跟踪、压缩、授权等功能。它还可以让你与使用 hypertonic 编写的应用程序共享中间件。

此外 Axum 是基于Rust强大的类型系统和所有权规则构建的,这些规则在编译时防止了常见的Web开发陷阱,如数据竞争和内存泄漏。此外,Axum的模块化设计理念允许开发人员通过仅添加必要的组件来创建轻量级、专注的应用程序。

创建一个项目

我们首先开始创建一个rust项目,这里我们默认你已经安装好rust开发环境了,具体安装步骤直接上官网查看一下即可

plain
cargo new rust_axum_web_guide

编写第一个hello world

添加依赖:

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,旨在充分利用 Tokio 的生态系统

我们使用官网上的hello world例子,来改写 main.rs

rust
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() 中使用,启动 axum 服务

启动项目

toml
cargo run

编译并运行当前项目,执行这个命令,它还会自动下载依赖

如果一切顺利,我们应该会在终端中看到输出“Running on http://localhost:3000”。在浏览器中访问http://localhost:3000 ,或使用类似 curl 的工具,将显示消息"Hello, Rust!"

路由与处理器

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

我们先来看个例子:

rust
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!" }))
        .route("/foo", get(get_foo).post(post_foo))
        .route("/foo/bar", get(foo_bar));


    // 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();
}


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方法;当然还可以指定了HTTP方法PUT、DELETE

处理器更详细的信息,可见axum::handler - Rust

用postman访问"/foo",get请求或者post请求,可以看到不同的结果

如果有多个相同的路由,则会恐慌

路由更详情信息,可见Router in axum - Rust

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

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

提取器

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

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

我们先来看看如何捕获动态URL值以及查询参数,主要使用 pathquery 提取器

rust
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() {
    // build our application with a single route
    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));


    // 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();
}



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)
}

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

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

测试结果:

rust
/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

rust
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() {
    // build our application with a single route
    let app = Router::new().route("/", get(|| async { "Hello, World!" }))
    .route("/query_1", get(print_map_handler))
    .route("/query_2", get(print_map2_handler));

    // 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();
}



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:?}")
}

测试结果:

rust
/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"})

pathquery 提取器一般适用于 GET 请求,那么 POST 请求的参数呢?

捕获请求体的参数

常见的请求体参数一般有两种格式:jsonform,我们下面举个例子

rust
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() {
    // build our application with a single route
    let app = Router::new().route("/", get(|| async { "Hello, World!" }))
    .route("/addUser", post(create_user_handler))
    .route("/submit", post(submit_form_handler));

    // 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();
}

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:?}")
}

测试结果:

rust
post /addUser   #result    Added user: CreateUser { username: "zsan", age: 18 }

post /submit    #result    submit form: LoginFormData { field_name: "lis", field_pwd: "123" }

捕获请求头参数

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

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

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

    // 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();
}

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()
}

测试结果:

rust
(get)  /header  #result    PostmanRuntime/7.43.0

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

响应处理

任何实现 IntoResponse东西 ,都可以被处理程序返回,它将被自动转换为响应,我们来看个例子:

rust
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 确保您 始终依赖 type system 生成响应。axum 通过要求所有服务都将 Infallible 作为其错误类型来实现这一点。Infallible 是永远不会发生的错误的错误类型

这段引用自官方文档,但太绕了,其核心意思,说人话就是:发生错误,不能让http连接中断,这导致响应无法正常返回;而是保证响应能正常返回,继而携带错误类型和错误信息;而axum能很轻易地做到~

所以一般axum的处理器绑定的方法都是这样定义:

rust
use axum::http::StatusCode;

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

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

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

而不是直接使用 StatusCode请使用中间的错误类型,最终可以转换为响应。 这样就可以在处理程序中使用?运算符

我们来看一个例子:

rust
use axum::{
    routing::get,
    Router,
    body::Body,
    http::{Response, StatusCode},
    error_handling::HandleError,
};
use tokio::sync::mpsc::error;
use tower;

#[tokio::main]
async fn main() {
    // build our application with a single route
    let app = Router::new()

        .route("/", get(|| async { "Hello, World!" }))

        .merge(router_fallible_service())//使用Service的错误处理

        .route("/foo", get(get_foo));

    // 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();
}

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方法"))
}

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

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

更多其他用法,请移步axum::error_handling - Rust

中间件

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

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

我们这里举个简单的例子:

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


// use tower::{Service, Layer};

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();
}

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

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

更多例子,请移步axum::middleware - Rust

本文就先到这里啦,一个web框架基本解决的就是文章中的这些方面,后续会探索axum与数据库的集成,或者其他有意思的特性,我们下期再见~


参考资料:

https://docs.rs/axum/latest/axum/index.html

https://www.twilio.com/en-us/blog/build-high-performance-rest-apis-rust-axum


作者:小牛呼噜噜

本文到这里就结束啦,感谢阅读,关注同名公众号:小牛呼噜噜,防失联+获取更多技术干货