惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
人人都是产品经理
人人都是产品经理
爱范儿
爱范儿
aimingoo的专栏
aimingoo的专栏
博客园 - 叶小钗
H
Help Net Security
Microsoft Security Blog
Microsoft Security Blog
The Cloudflare Blog
S
SegmentFault 最新的问题
小众软件
小众软件
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 司徒正美
The GitHub Blog
The GitHub Blog
量子位
H
Hackread – Cybersecurity News, Data Breaches, AI and More
V
V2EX
Martin Fowler
Martin Fowler
博客园 - 【当耐特】
J
Java Code Geeks
D
DataBreaches.Net
云风的 BLOG
云风的 BLOG
F
Fortinet All Blogs
Blog — PlanetScale
Blog — PlanetScale
Last Week in AI
Last Week in AI

又耳笔记

用AI挣钱之AI建站:二维码生成器工具 用AI挣钱之AI建站:https://markdowntopdf.top Cloudflare Workers实战番外一:用Static Assets托管网站,告别传统方案 Cloudflare Workers实战(五):不止JavaScript,拥抱Python与Rust Cloudflare Workers实战(四):托管和分发静态文件 Cloudflare Workers实战(三):实现认证、重定向与缓存 Cloudflare Workers实战(二):动态修改后端响应 Cloudflare Workers实战(一):随心所欲操作客户端请求 Cloudflare workers不完全指南 使用n8n创作短篇小说 水果风波:信任的代价 用Python将PDF文件转换成图片 rust网络框架Pingora源码阅读3 rust网络框架Pingora源码阅读2 rust网络框架Pingora源码阅读1 Pingora快速入门教程1之总览 Rust模板引擎askama快速入门引擎 使用Loco快速搭建自己的后台系统 用Rust发一封图文并茂的邮件 Rust小项目: 写一个简单的恶意流量阻断器 Rust小项目: 写一个简单的网页爬虫 Rust命令行库Clap快速入门教程 Rust小项目:用Rust写一个端口扫描器 Rust真全栈开发快速入门 rust声明宏快速入门教程 Rust文本处理快速入门教程 RUST web框架axum快速入门教程6之测试 白嫖免费的Rust在线运行时shuttle RUST web框架axum快速入门教程5之中间件 用Rust来做以太坊开发5之事件日志及签名
RUST web框架axum快速入门教程4之路由
About 又耳宁 关于技术与股票的一些零碎想法 · 2023-12-16 · via 又耳笔记

本文主要讨论axum的路由,通过路由我们可以灵活的来将不同的请求路径路由到不同的handler,也能自由的组合不同的路由对象来处理请求。

axum的路由主要分为两个部分,一部分是匹配规则,一部分是路由对象的组合方式。

往期文章:

匹配规则

一般来说,路由的匹配都是通过前缀树算法来实现的,axum的路由规则也是前缀树,不过axum并没有自己实现这个前缀树的算法,而是使用现有的第三方库matchit,支持三种匹配方式,完全匹配,命名参数匹配,通配符匹配,代码如下:

// https://youerning.top/post/axum/quickstart-4
use matchit::Router;

fn main() -> Result<(), Box<dyn std::error::Error>>{
    let mut router = Router::new();
    // 完全匹配
    router.insert("/home", "youerning.top")?;
    let matched = router.at("/home")?;
    assert_eq!(*matched.value, "youerning.top");

    // 命名参数匹配  官方叫做Named Parameters
    router.insert("/users/:id", "A User")?;
    let matched = router.at("/users/978")?;
    assert_eq!(matched.params.get("id"), Some("978"));
    assert_eq!(*matched.value, "A User");

    // 通配符匹配   官方叫做Catch-all Parameters
    router.insert("/*p", "youerning.top")?;

    assert_eq!(router.at("/foo.js")?.params.get("p"), Some("foo.js"));
    assert_eq!(router.at("/c/bar.css")?.params.get("p"), Some("c/bar.css"));
    // 注意不能匹配到/
    assert!(router.at("/").is_err());
    Ok(())
}

上面代码改自matchit的官方示例,为啥用它,我想是因为它超级快吧,下面是它的性能测试比较, 200纳秒以内!!! 性能恐怖如斯。

Compare Routers/matchit 
time:   [197.57 ns 198.74 ns 199.83 ns]

Compare Routers/actix
time:   [26.805 us 26.811 us 26.816 us]

Compare Routers/path-tree
time:   [468.95 ns 470.34 ns 471.65 ns]

Compare Routers/regex
time:   [22.539 us 22.584 us 22.639 us]

Compare Routers/route-recognizer
time:   [3.7552 us 3.7732 us 3.8027 us]

Compare Routers/routefinder
time:   [5.7313 us 5.7405 us 5.7514 us]

下面是axum的代码,跟上面的代码没有太多区别


use axum::{response::Html, routing::get, Router, extract::Path};

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/", get(index_hanlder))
        .route("/users/:id", get(user_handler1))
        .route("/download/*path", get(download_handler2))
        ;

    let addr = "0.0.0.0:8080";
    axum::Server::bind(&addr.parse().unwrap())
      .serve(app.into_make_service())
      .await
      .unwrap();
}

async fn index_hanlder() -> Html<&'static str> {
    Html("<h1>Hello, World!</h1>")
}

async fn user_handler1(Path(id): Path<i32>) -> String {
    format!("name: {id}")
}

async fn download_handler2(Path(path): Path<String>) -> String {
    format!("download path: /{path}")
}

服务(Service)

axum是构筑在其他框架之上的框架,所以可以复用其他框架的一些特性,比如Tower里面的Service概念(一个trait),我们可以很简单的将实现了这个trait的对象注册到路由中。


use axum::{
    body::Body,
    response::Response,
    routing::get,
    Router,
    http::Request,
    routing::any_service,
};
use std::convert::Infallible;
use tower::service_fn;


#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/", get(index_handler))
        .route(
            "/",
            any_service(service_fn(|req: Request<Body>| async move {
                let body = Body::from(format!("Hi from `{} /`", req.method()));
                let res = Response::new(body);
                Ok::<_, Infallible>(res)
            }))
        )
        .route(
            "/service1",
            any_service(service_fn(|req: Request<Body>| async move {
                let body = Body::from(format!("Hi from `{} /service1`", req.method()));
                let res = Response::new(body);
                Ok::<_, Infallible>(res)
            }))
        )
        .route_service(
            "/service2",
            service_fn(|req: Request<Body>| async move {
                let body = Body::from(format!("Hi from `{} /service2`", req.method()));
                let res = Response::new(body);
                Ok::<_, Infallible>(res)
            })
        )
        ;

    let addr = "0.0.0.0:8080";
    axum::Server::bind(&addr.parse().unwrap())
      .serve(app.into_make_service())
      .await
      .unwrap();
}

async fn index_handler() -> String {
    format!("index page")
}

值得注意的是,注册服务的路由的时候会跟常规的路由合并, 比如这里的/路由跟后面的服务路由合并了,如果使用GET方法将会调用index_handler,其他方法就调用这个服务对象

使用Service大致有两个好处,一是捕获所有方法的路由,二是可以将实现了tower Service trait的对象直接纳入进来(如果你有的话)。tower是一个很棒的框架,很多框架都使用了它,比如hyper, reqwest, 以及本文的axum

路由嵌套

很多时候我们会将路由分割成一个个部分,这些部分会有层级关系,比如/api/users,/api/products, 我们一般将/api称为前缀,而后面的路由由一个个小的路由对象来提供请求。


use axum::{
    body::Body,
    routing::get,
    Router,
    extract::Path,
    http::Request,
};


#[tokio::main]
async fn main() {
    let user_routes = Router::new()
        .route("/:id", get(path_handler));

    let product_routes = Router::new()
        .route("/:id", get(path_handler));

    let api_routes = Router::new()
        .route("/", get(api_handler))
        .nest("/users", user_routes)
        .nest("/products", product_routes);

    let app = Router::new()
        .route("/", get(index_handler))
        .nest("/api", api_routes);

    let addr = "0.0.0.0:8080";
    axum::Server::bind(&addr.parse().unwrap())
      .serve(app.into_make_service())
      .await
      .unwrap();
}

async fn index_handler() -> String {
    format!("hello world")
}

async fn api_handler() -> String {
    format!("api handler")
}

async fn path_handler(Path(id): Path<i32>, req: Request<Body>) -> String {
    format!("id[{id}] at {}", req.uri())
}

值得注意的是,在嵌套的子路由里面看到的uri不是完整的uri,比如/api/users/1看到的路由是"/1", 如果需要完整的url路径需要使用OriginalUri

当然了,还可以嵌套Service这和上面的路由差不多,这里就不演示

fallback

默认情况下,创建的路由都有一个404的默认fallback用于捕获无法匹配的路由,axum自然也是支持手动指定的。


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


#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/", get(index_handler))
        .fallback(fallback);

    let addr = "0.0.0.0:8080";
    axum::Server::bind(&addr.parse().unwrap())
      .serve(app.into_make_service())
      .await
      .unwrap();
}

async fn index_handler() -> &'static str {
    "<h1>Hello, World!</h1>"
}

async fn fallback() -> String {
    format!("youerning.top")
}

Handler的简单介绍

axum通过宏生成了支持最多17个参数的Handler实现,这也是为啥我们可以只用一个简单的异步函数就能作为handler的原因, 它的实现代码写得很漂亮。

// 为各种类型生成Handler实现的声明函
macro_rules! impl_handler {
    (
        [$($ty:ident),*], $last:ident
    ) => {
        #[allow(non_snake_case, unused_mut)]
        impl<F, Fut, S, B, Res, M, $($ty,)* $last> Handler<(M, $($ty,)* $last,), S, B> for F
        where
            F: FnOnce($($ty,)* $last,) -> Fut + Clone + Send + 'static,
            Fut: Future<Output = Res> + Send,
            B: Send + 'static,
            S: Send + Sync + 'static,
            Res: IntoResponse,
            $( $ty: FromRequestParts<S> + Send, )*
            $last: FromRequest<S, B, M> + Send,
        {
            type Future = Pin<Box<dyn Future<Output = Response> + Send>>;

            fn call(self, req: Request<B>, state: S) -> Self::Future {
                Box::pin(async move {
                    let (mut parts, body) = req.into_parts();
                    let state = &state;

                    $(
                        let $ty = match $ty::from_request_parts(&mut parts, state).await {
                            Ok(value) => value,
                            Err(rejection) => return rejection.into_response(),
                        };
                    )*

                    let req = Request::from_parts(parts, body);

                    let $last = match $last::from_request(req, state).await {
                        Ok(value) => value,
                        Err(rejection) => return rejection.into_response(),
                    };

                    let res = self($($ty,)* $last,).await;

                    res.into_response()
                })
            }
        }
    };
}

// 空参数的实现
impl<F, Fut, Res, S, B> Handler<((),), S, B> for F
where
    F: FnOnce() -> Fut + Clone + Send + 'static,
    Fut: Future<Output = Res> + Send,
    Res: IntoResponse,
    B: Send + 'static,
{
    type Future = Pin<Box<dyn Future<Output = Response> + Send>>;

    fn call(self, _req: Request<B>, _state: S) -> Self::Future {
        Box::pin(async move { self().await.into_response() })
    }
}

// 为1到16个参数的函数签名生成对的handler实现
all_the_tuples!(impl_handler);
macro_rules! all_the_tuples {
    ($name:ident) => {
        $name!([], T1);
        $name!([T1], T2);
    	// 省略其他列表
        $name!([T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15], T16);
    };
}

小结

axum的路由还是比较简单明了的,通过路由规则和路由嵌套应该能够应付大多数情况了。不过本文也有一些东西这里没有提到,那就是嵌套路由的状态共享和中间件,这个需要看看官方文档或者源代码。

参考链接