axum 是一个易于使用,但功能强大的 Web 框架,旨在充分利用 Tokio 的生态系统。

Rust Web 框架:Axum 入门一探

高级特性

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

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

使用示例

先来一个Hello World的入门示例:

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

添加上面的依赖项后,就可以编码了:

use axum::{
    routing::get,
    Router,
};
use std::net::SocketAddr;
#[tokio::main]
async fn main() {
    let app = route("/", get(handler)); // http://127.0.0.1:3000
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    // run it with hyper on localhost:3000
    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}
// 处理器
async fn handler() -> &'static str {
    "Hello, World!"
}

GET/的请求响应是200 OK,其中正文是Hello, World!。任何其他请求将导致404 Not Found响应。

注:cargo run 启动后,浏览器里跑一下 http://127.0.0.1:3000 或者 curl -X GEThttp://127.0.0.1:3000

路由(Routers)

Router用于设置哪些路径指向哪些服务,可以使用一个简单的 DSL 来组合多个路由。

#[tokio::main]
async fn main() {
    // our router
    let app = Router::new()
        .route("/", get(root))  // http://127.0.0.1:3000
        .route("/foo", get(get_foo).post(post_foo)) // http://127.0.0.1:3000/foo
        .route("/foo/bar", get(foo_bar)); // http://127.0.0.1:3000/foo/bar
   // run it with hyper on localhost:3000
    axum::Server::bind(&"127.0.0.1:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}
// which calls one of these handlers
async fn root() -> String {
    String::from("hello axum")
}
async fn get_foo() -> String {
    String::from("get请求的foo")
}
async fn post_foo() -> String {
    String::from("post请求的foo")
}
async fn foo_bar() -> String {
    String::from("foo:bar")
}

注:这里 /foo 同时绑定了GET及POST方法的路由。可以用 crul 命令工具测试一下:

curl -X GET http://127.0.0.1:3000/foo

curl -X POST http://127.0.0.1:3000/foo

细节可以查看Router

处理器(Handlers)

在 axum 中,处理器(handler)是一个异步异步函数或者异步代码块,它接受零个或多个“ extractors “作为参数,并返回一些可以转换为一个“IntoResponse”的内容。

处理器是应用程序逻辑存在的地方,而 axum 应用程序是通过处理器之间的路由构建的。

请参见handler了解处理程序的更多详细信息。

提取器(Extractors)

请求可以使用 “提取器(Extractor)” 进行声明式的解析,是一个实现了FromRequestFromRequestParts的类型,作用是分离传入请求以获得处理程序所需的部分(比如解析异步函数的参数),如果请求的URI匹配,就会运行。

use axum::extract::{Path, Query, Json};
use std::collections::HashMap;  
// Path路径,eg. /users/<id>  
async fn path(Path(user_id): Path<u32>) {}  
// Query参数,eg. /users?id=123&name=jim  
async fn query(Query(params): Query<HashMap<String, String>>) {}
// Json 格式参数,一般用于 POST 请求  
async fn json(Json(payload): Json<serde_json::Value>) {}

例如,Json是一个提取器,它消耗请求主体并将其解析为JSON:

use axum::{ routing::get, Router, extract::Json};
use serde::Deserialize;
#[derive(Deserialize, Debug)]
struct CreateUser {
    username: String,
}
// curl -H "Content-Type: application/json" -d '{"username":"someName"}' -X POST http://127.0.0.1:3000/users
async fn create_user(Json(payload): Json<CreateUser>) -> (StatusCode, Json<CreateUser>){
    // `payload` is a `CreateUser`
    // 响应内容为Json格式,状态码是201
    (StatusCode::CREATED, Json(payload))
}
#[tokio::main]
async fn main() {
    // our router
    let app = Router::new()
        .route("/users", post(create_user)); // http://127.0.0.1:3000/users
    // run it with hyper on localhost:3000
    axum::Server::bind(&"127.0.0.1:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

注:cargo run 启动后,运行 curl 命令:

curl -H “Content-Type: application/json” -d ‘{“username”:”someName”}’ -X POST http://127.0.0.1:3000/users

axum提供了许多有用的提取器,例如:

  • Bytes,String,Body, 和BodyStream用于获取请求正文
  • Method,HeaderMap, 和Uri用于获取请求的特定部分
  • Form,Query,UrlParams, 和UrlParamsMap用于更高级别的请求解析
  • Extension用于跨处理程序共享状态的扩展
  • Request<hyper::Body>如果你想完全控制
  • Result<T, E>andOption<T>使提取器成为可选

你也可以通过实现FromRequest来定义你自己的提取器。

更多细节可以参看extract

构建响应(IntoResponse)

处理程序可以返回任何实现了IntoResponse的东西,它将被自动转换为响应:

use http::StatusCode;
use axum::response::{Html, Json};
use serde_json::{json, Value};
// We've already seen returning &'static str
async fn text() -> &'static str {
    "Hello, World!"
}
// String works too
async fn string() -> String {
    "Hello, World!".to_string()
}
// Returning a tuple of `StatusCode` and another `IntoResponse` will
// change the status code
async fn not_found() -> (StatusCode, &'static str) {
    (StatusCode::NOT_FOUND, "not found")
}
// `Html` gives a content-type of `text/html`
async fn html() -> Html<&'static str> {
    Html("<h1>Hello, World!</h1>")
}
// `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 }))
}

这意味着在实践中,你很少需要建立你自己的响应。你也可以实现IntoResponse来创建你自己的特定领域响应。

更多细节参看response

错误处理(Error handling)

axum旨在提供一个简单且可预测的错误处理模型,这意味着将错误转换为响应很简单,并且可以保证所有错误都得到处理。

use std::time::Duration;
use axum::{
    body::Body,
    error_handling::{HandleError, HandleErrorLayer},
    http::{Method, Response, StatusCode, Uri},
    response::IntoResponse,
    routing::get,
    BoxError, Router,
};
use tower::ServiceBuilder;
#[tokio::main]
async fn main() {
    let app = Router::new()
        .merge(router_fallible_service()) // 模拟使用 Service的错误处理
        .merge(router_fallible_middleware()) // 模拟使用中间件的错误处理
        .merge(router_fallible_extractor());  // 模拟使用提取器的错误处理  
    let addr = "127.0.0.1:3000";
    println!("listening on {}", addr);
    axum::Server::bind(&addr.parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}
// 错误处理方式1: 模拟使用 Service的错误处理
fn router_fallible_service() -> Router {
    // 这个 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(
        "/",
        // Service 适配器通过将错误转换为响应来处理错误。
        HandleError::new(some_fallible_service, handle_anyhow_error),
    )
}
// 业务处理逻辑,可能出现失败而抛出 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 handler_timeout() -> impl IntoResponse {
    println!("sleep 3 seconds");
    tokio::time::sleep(Duration::from_secs(3)).await; // 休眠3秒,模拟超时
    format!("Hello Error Handling !!!")
}
// 错误处理方式2 : 用中间件处理错误的路由
fn router_fallible_middleware() -> Router {
    Router::new()
        .route("/fallible_middleware", get(handler_timeout))
        .layer(
            ServiceBuilder::new()
                // `timeout` will produce an error if the handler takes
                // too long so we must handle those
                .layer(HandleErrorLayer::new(handler_timeout_error))
                .timeout(Duration::from_secs(1)),
        )
}
// 用中间件处理错误
async fn handler_timeout_error(err: BoxError) -> (StatusCode, String) {
    if err.is::<tower::timeout::error::Elapsed>() {
        (
            StatusCode::REQUEST_TIMEOUT,
            "Request time too long, Timeout!!!".to_string(),
        )
    } else {
        (
            StatusCode::INTERNAL_SERVER_ERROR,
            format!("Unhandled internal error: {}", err),
        )
    }
}
// 错误处理方式3: 用运行时提取器处理错误的路由
fn router_fallible_extractor() -> Router {
    Router::new()
        .route("/fallible_extractor", get(handler_timeout))
        .layer(
            ServiceBuilder::new()
                // `timeout` will produce an error if the handler takes
                // too long so we must handle those
                .layer(HandleErrorLayer::new(handler_timeout_fallible_extractor))
                .timeout(Duration::from_secs(1)),
        )
}
// 用运行时提取器处理错误
async fn handler_timeout_fallible_extractor(
    // `Method` and `Uri` are extractors so they can be used here
    method: Method,
    uri: Uri,
    // the last argument must be the error itself
    err: BoxError,
) -> (StatusCode, String) {
    (
        StatusCode::INTERNAL_SERVER_ERROR,
        format!("`{} {}` failed with {}", method, uri, err),
    )
}

参见error_handling了解更多关于axum错误处理模型以及如何优雅地处理错误的详细信息。

中间件(Middleware)

axum 支持来自towertower-http的中间件。

[dependencies]
axum = "0.6.16"
tokio = { version = "1.0", features = ["full"] }
tower = { version = "0.4.13", features = ["full"] }
tower-http = { version = "0.4", features = ["fs", "trace", "compression-br"] }

添加上面的依赖项后,就可以编码了:

use std::net::SocketAddr;
use axum::{routing::get, Router};
use tower::ServiceBuilder;
use tower_http::{compression::CompressionLayer, trace::TraceLayer};
#[tokio::main]
async fn main() {
    let middleware_stack = ServiceBuilder::new()
        // add high level tracing of requests and responses
        .layer(TraceLayer::new_for_http())
        // compression responses
        .layer(CompressionLayer::new())
        // convert the `ServiceBuilder` into a `tower::Layer`;
        .into_inner();
    let app = Router::new()
        .route("/", get(|| async { "Hello, World!" }))
        .layer(middleware_stack);
    // run it with hyper on localhost:3000
    let addr = SocketAddr::from(([127, 0, 0, 1], 8080));
    println!("listening on {}", addr);
    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

这个功能很关键,因为它允许我们只写一次中间件,并在不同的应用中分享它们。例如,axum不需要提供自己的 tracing/logging 中间件,可以直接使用来自tower-httpTraceLayer。同样的中间件也可以用于用 tonic 制作的客户端或服务器路由到任何tower::Service

axum也可以将请求路由到任何 tower 服务。可以是你用service_fn编写的服务,也可以是来自其他 crate 的东西,比如来自tower-httpServeFile

use axum::{
    body::Body, http::Request, response::Response, routing::get_service, Router,
};
use std::{convert::Infallible, net::SocketAddr};
use tower::service_fn;
use tower_http::services::ServeFile;
#[tokio::main]
async fn main() {
    let app = Router::new()
        // GET `/static/Cargo.toml` goes to a service from tower-http
        .route(
            "/static",
            get_service(ServeFile::new("Cargo.toml")),
        )
        .route(
            // Any request to `/` goes to a some `Service`
            "/",
            get_service(service_fn(|_: Request<Body>| async {
                let res = Response::new(Body::from("Hi from `GET /`"));
                Ok::<_, Infallible>(res)
            })),
        );
    // run it with hyper on localhost:3000
    let addr = SocketAddr::from(([127, 0, 0, 1], 8080));
    println!("listening on {}", addr);
    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

为 axum 编写中间件有几种不同的方法。详见中间件

与处理器共享状态(Sharing state with handlers)

在处理程序之间共享某些状态是很常见的。例如,可能需要共享到其他服务的数据库连接或客户端池。

最常见的三种方法是:

  • 使用状态提取器:State
  • 使用请求扩展提取器:Extension
  • 使用闭包捕获:Closure
use axum::{
    extract::{Path, State},
    response::IntoResponse,
    routing::get,
    Extension, Router,
};
use std::sync::{Arc, atomic::{AtomicUsize, Ordering}};
// 共享状态结构体
#[derive(Debug)]
struct AppState {
    // ...
    state: AtomicUsize,
}
// 方法1: 使用 State 状态提取器
async fn handler_as_state_extractor(State(state): State<Arc<AppState>>) -> impl IntoResponse {
    // ...
    state.state.fetch_add(1, Ordering::SeqCst); //请求一次 state 的值递增1
    format!("State extract shared_state: {:?}", state)
}
// 方法2: 使用 Extension 请求扩展提取器
// 这种方法的缺点是,如果尝试提取一个不存在的扩展,可能是因为忘记添加中间件,
// 或者因为提取了错误的类型,那么将得到运行时错误(特别是500 Internal Server Error 响应)。
async fn handler_as_extension_extractor(Extension(state): Extension<Arc<AppState>>) -> impl IntoResponse {
    // ...
    state.state.fetch_add(1, Ordering::SeqCst); //请求一次 state 的值递增1
    format!("Extension extract shared_state: {:?}", state)
}
// 方法3: 使用闭包捕获(closure captures)直接传递给处理器
async fn get_user(Path(user_id): Path<String>, state: Arc<AppState>) -> impl IntoResponse {
    // ...
    state.state.fetch_add(1, Ordering::SeqCst); //请求一次 state 的值递增1
    format!("closure captures shared_state: {:?}", state)
}
#[tokio::main]
async fn main() {
    // 处理器共享状态(Sharing state with handlers)
    let shared_state = Arc::new(AppState { state: 0.into()/* ... */ });
    let shared_state_extension = Arc::clone(&shared_state);
    let shared_state_closure = Arc::clone(&shared_state);
    let app = Router::new()
        .route("/state", get(handler_as_state_extractor)) // 1.使用State提取器
        .with_state(shared_state)
        .route("/extension", get(handler_as_extension_extractor)) // 2.使用Extension提取器
        .layer(Extension(shared_state_extension))
        .route(
            "/users/:id",
            get({
                move |path| get_user(path, shared_state_closure)  // 3.使用闭包捕获直接传递给处理器
            }),
        );
    let addr = "127.0.0.1:3000";
    println!("listening on {}", addr);
    axum::Server::bind(&addr.parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

注:可以用浏览器跑一下或者 crul 命令工具测试一下(交替请求),看下state的是否在共享基础上递增

curl -X GET http://127.0.0.1:3000/state

curl -X GET http://127.0.0.1:3000/extension

curl -X GET http://127.0.0.1:3000/users/111

总结

axum 是一个易于使用,功能强大的 Web 框架,旨在充分利用 Tokio 的生态系统,使用无宏 API 实现了路由功能,基于hyper构建的,中间件基于 tower 和 tower-http 生态实现,可利用其中中间件、服务以及实用程序。支持 WebSocket 和其他协议,以及异步 I/O。

  • axum 的中间件是直接使用 tower 的抽象,这样的好处就是:
    1. 使用了统一 的 Service 和 Layer 抽象标准,方便大家来繁荣生态
    2. 复用和充分利用 tokio / hyper/ tonic 生态,潜力很大
  • axum 的路由机制并没有使用像 rocket那样的属性宏,而是提供了简单的 DSL (链式调用)。路由是基于迭代和正则表达式来匹配的,所以路由性能和 actix-web 差不多。
  • 也提供了方便的提取器 ,只要实现 FromRequest 就是一个提取器,实现起来也非常方便。

总之,Axum 是 Rust 在 Web 开发领域的一个里程碑,它强势带动了 tokio/tower 生态,潜力很大。

参考

  • docs.rs/axum/latest…
  • docs.rs/axum/latest…
  • github.com/tokio-rs/ax…
  • tokio.rs/blog/2021-0…
  • blog.logrocket.com/rust-axum-e…