Rust in Everything
基于 axum 0.8.9

Router 的内部结构

主线位置:两条主线都在。装配主线上,每次 .route、.layer、.with_state 都拆开 Arc<RouterInner> 改一处再装回去,这一页看拆开时什么时候要复制。请求主线上,Router::call 依次问三个地方「这个路径归你吗」,第一个认领的把匹配结果写进 extensions,再把请求交给端点。

flowchart TB
  router["Router S"] --> arcinner["Arc RouterInner"]
  arcinner --> pr["path_router:你的路由"]
  arcinner --> fr["fallback_router"]
  arcinner --> dfb["default_fallback:bool"]
  arcinner --> caf["catch_all_fallback"]
  pr --> routes["routes 端点表"]
  pr --> nodearc["node:Arc Node"]
  nodearc --> mit["matchit 树"]
  routes --> ep1["Endpoint::MethodRouter"]
  routes --> ep2["Endpoint::Route"]

Router 本身只有一个指针。路由表、路由树、各种 fallback 都在 RouterInner 里;路由树和路由表又分成两半,中间用一个 u32 的 RouteId 连起来。这一页按这张图从外往里讲,最后看一个请求怎么从外往里走一遍。路径语法和冲突规则在 matchit 与路径语法,方法分发在 MethodRouter,Route 的装箱在 Route 与类型擦除。

四个字段

Router 和 RouterInner:

pub struct Router<S = ()> {
    inner: Arc<RouterInner<S>>,
}

impl<S> Clone for Router<S> {
    fn clone(&self) -> Self {
        Self {
            inner: Arc::clone(&self.inner),
        }
    }
}

struct RouterInner<S> {
    path_router: PathRouter<S, false>,
    fallback_router: PathRouter<S, true>,
    default_fallback: bool,
    catch_all_fallback: Fallback<S>,
}

克隆一个 Router 只是引用计数加一,和里面有多少条路由无关。四个字段:

字段是什么Router::new() 时
path_router你用 route、route_service、nest 注册的路由空
fallback_router另一个 PathRouter,IS_FALLBACK = true,专放 fallbacknew_fallback 在 / 和 /{*__private__axum_fallback} 各放一个 NotFound
default_fallbackfallback 是不是还是默认的那个;merge、nest 靠它决定用谁的 fallbacktrue
catch_all_fallback两个 PathRouter 都不认领时的最后一站Fallback::Default(Route::new(NotFound))

fallback 之所以也做成一个 PathRouter,是为了让 nest 能把子路由器的 fallback 连同前缀一起搬进来,规则见 nest、merge 与 fallback。IS_FALLBACK 是 const 泛型参数,两个 PathRouter 共用一份代码,区别只有三处:new_fallback / set_fallback 只为 PathRouter<S, true> 实现;merge 时 fallback 那一侧的 / 和通配路由直接替换而不是报冲突;匹配时不写 MatchedPath(下面讲)。

Router 实现了 Debug,可以直接看到这四个字段:

axum = { version = "=0.8.9", default-features = false }
use axum::routing::get;
use axum::Router;

fn main() {
  let app: Router = Router::new().route("/users/{id}", get(|| async { "user" }));
  println!("{app:#?}");
}

M2 Max、rustc 1.99.0,跑 5 次,fallback_router 里两个 HashMap 的条目顺序在两种排列之间变化(HashMap 的迭代顺序每次运行随机),内容相同。其中一次:

Router {
    path_router: PathRouter {
        routes: {
            RouteId(
                1,
            ): MethodRouter(
                MethodRouter {
                    get: BoxedHandler,
                    head: None,
                    delete: None,
                    options: None,
                    patch: None,
                    post: None,
                    put: None,
                    trace: None,
                    connect: None,
                    fallback: Default(
                        Route,
                    ),
                    allow_header: Bytes(
                        b"GET,HEAD",
                    ),
                },
            ),
        },
        node: Node {
            paths: {
                RouteId(
                    1,
                ): "/users/{id}",
            },
        },
    },
    fallback_router: PathRouter {
        routes: {
            RouteId(
                1,
            ): Route(
                Route,
            ),
            RouteId(
                2,
            ): Route(
                Route,
            ),
        },
        node: Node {
            paths: {
                RouteId(
                    2,
                ): "/{*__private__axum_fallback}",
                RouteId(
                    1,
                ): "/",
            },
        },
    },
    default_fallback: true,
    catch_all_fallback: Default(
        Route,
    ),
}
  • 两个 PathRouter 各有自己的 RouteId 序列,都从 1 开始。
  • get 槽是 BoxedHandler:handler 还欠着状态,没变成 Route(Route 与类型擦除)。
  • 端点有两种:Endpoint 是 MethodRouter(MethodRouter<S>) 或 Route(Route)。route 放进去的是前者,route_service、nest_service、fallback 放进去的是后者,后者不分方法。

PathRouter:树里只存 RouteId

PathRouter 和它的 Node:

pub(super) struct PathRouter<S, const IS_FALLBACK: bool> {
    routes: HashMap<RouteId, Endpoint<S>>,
    node: Arc<Node>,
    prev_route_id: RouteId,
    v7_checks: bool,
}

// ...

/// Wrapper around `matchit::Router` that supports merging two `Router`s.
#[derive(Clone, Default)]
struct Node {
    inner: matchit::Router<RouteId>,
    route_id_to_path: HashMap<RouteId, Arc<str>>,
    path_to_route_id: HashMap<Arc<str>, RouteId>,
}

matchit 树里存的不是端点,而是 RouteId(struct RouteId(u32))。端点放在旁边的 routes 表里。Node 另外记了两张反查表:route_id_to_path 用来设置 MatchedPath、在 nest/merge 时找回原路径;path_to_route_id 用来发现「这个路径已经注册过」。

这样拆开的好处在 Node 的类型上:它没有 S 参数。layer 和 with_state 要改的是每个端点(with_state 还把 Endpoint<S> 变成 Endpoint<S2>),而 PathRouter::with_state 只重建 routes,树原样搬过去:

        PathRouter {
            routes,
            node: self.node,
            prev_route_id: self.prev_route_id,
            v7_checks: self.v7_checks,
        }

layer 也一样。路由树只在加路由时才动。

PathRouter::route 加一条路由分两种情况:

  1. 路径已经在 path_to_route_id 里,并且那里是一个 MethodRouter:把旧的 prev_method_router.clone() 和新的合并(merge_for_path,同一方法重复时 panic,见 MethodRouter),放回原来的 RouteId。树不动。.route("/", get(a)).route("/", post(b)) 就是这样工作的。
  2. 否则 next_route_id 分配新编号(checked_add(1),超过 u32::MAX 条路由时 panic「Over u32::MAX routes created」),set_node 把路径插进树,再把端点放进表。
    fn set_node(&mut self, path: &str, id: RouteId) -> Result<(), String> {
        let node = Arc::make_mut(&mut self.node);

        node.insert(path, id)
            .map_err(|err| format!("Invalid route {path:?}: {err}"))
    }

Arc::make_mut:树没有被别人共享就原地插,被共享就先把整个 Node(matchit 树加两张表)复制一份。matchit 拒绝的路径在这里变成 Invalid route ... 的错误,再由 Router::route 的 panic_on_err! 变成 panic,报错原文见 matchit 与路径语法。

📝NOTE
没有蠢问题

问: Router 外面已经有一个 Arc,PathRouter 里的 node 为什么还要再包一个?

答: 两个 Arc 管的是不同的复制。外层的 Arc<RouterInner> 让克隆整个 Router 变便宜;一旦要改,RouterInner 还是可能被整个拆开复制(下一节)。那时 PathRouter 的 Clone 会克隆 routes 表里的每个端点,但 node 只是 self.node.clone(),引用计数加一。后面如果只是 layer、with_state,树一直是共享的那一份,不用复制;只有真的往树里插路径,set_node 的 Arc::make_mut 才复制它。端点和树分开计数,各在自己需要改的时候才复制。

改动 Router:独占就原地改,共享就复制

Router 的每个构建方法都按值拿 self,再交给 map_inner! 或 tap_inner! 这两个宏。两个宏都先调 into_inner:

    fn into_inner(self) -> RouterInner<S> {
        match Arc::try_unwrap(self.inner) {
            Ok(inner) => inner,
            Err(arc) => RouterInner {
                path_router: arc.path_router.clone(),
                fallback_router: arc.fallback_router.clone(),
                default_fallback: arc.default_fallback,
                catch_all_fallback: arc.catch_all_fallback.clone(),
            },
        }
    }

tap_inner! 拿到 RouterInner 后执行几条语句改它,map_inner! 用一个表达式造一个新的;最后都 Arc::new 装回去。Router::route 是前一种:

    pub fn route(self, path: &str, method_router: MethodRouter<S>) -> Self {
        tap_inner!(self, mut this => {
            panic_on_err!(this.path_router.route(path, method_router));
        })
    }

Arc::try_unwrap 只在引用计数为 1 时成功,拿出来的就是原来那份数据,没有任何复制。一条 Router::new().route(..).route(..).layer(..) 的链里,每一步的 Router 都只有一个持有者,所以整条链都走 Ok 分支。

只要别处还有一个克隆,就走 Err 分支:RouterInner 没有实现 Clone,这里逐个字段复制。PathRouter 的 Clone 是 routes: self.routes.clone(),表里每个 Endpoint 都要克隆,MethodRouter 克隆它的每个方法槽;槽里的 Route 克隆装箱的服务,BoxedIntoRoute 克隆 handler。用户的服务和 handler 的 Clone 在这里被调用。

❗IMPORTANT
动脑筋:克隆一份 Router 之后再加一条路由,用户的服务被克隆几次?

平台 M2 Max、rustc 1.99.0。Svc 是一个手写的 tower 服务,每次 Clone 计数加一。先建 3 条路由,再依次做下面几步,每步打印这一步里 Svc 被克隆的次数。route /e 那一行是几?route /f again with post 那一行呢?

axum = { version = "=0.8.9", default-features = false }
tower = "=0.5.3"
use std::convert::Infallible;
use std::future::{ready, Ready};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::task::{Context, Poll};

use axum::extract::Request;
use axum::routing::{get_service, post_service};
use axum::Router;
use tower::Service;

// 数 Svc 被 clone 了几次
static CLONES: AtomicUsize = AtomicUsize::new(0);

fn clones() -> usize {
  CLONES.load(Ordering::SeqCst)
}

struct Svc;

impl Clone for Svc {
  fn clone(&self) -> Self {
    CLONES.fetch_add(1, Ordering::SeqCst);
    Svc
  }
}

impl Service<Request> for Svc {
  type Response = &'static str;
  type Error = Infallible;
  type Future = Ready<Result<&'static str, Infallible>>;

  fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Infallible>> {
    Poll::Ready(Ok(()))
  }

  fn call(&mut self, _req: Request) -> Self::Future {
    ready(Ok("svc"))
  }
}

fn step(label: &str, before: usize) {
  println!("{label:<40} Svc clones = {}", clones() - before);
}

fn main() {
  let b = clones();
  let app: Router = Router::new()
    .route_service("/a", Svc)
    .route("/b", get_service(Svc))
    .route("/c", get_service(Svc));
  step("build: 3 routes", b);

  // 先造好 MethodRouter,只数 route 本身的克隆
  let (d, e, f, f_post) = (get_service(Svc), get_service(Svc), get_service(Svc), post_service(Svc));

  let b = clones();
  let app = app.route("/d", d);
  step("route /d (inner not shared)", b);

  let b = clones();
  let keep = app.clone();
  step("app.clone()", b);

  let b = clones();
  let app = app.route("/e", e);
  step("route /e (inner shared with keep)", b);

  let b = clones();
  let app = app.route("/f", f);
  step("route /f (not shared again)", b);

  let b = clones();
  let app = app.route("/f", f_post);
  step("route /f again with post (merge)", b);

  drop((keep, app));
}

axum::serve 每条连接都会走一次复制

构建阶段的链很少共享,但运行时有一处一定共享:直接把 Router 交给 axum::serve 时,每接受一条连接,Router 对 IncomingStream 的 Service::call 执行 self.clone().with_state(())(为什么要这样做见 一个请求穿过 axum)。self 还被 serve 握着,self.clone() 和它共享同一个 Arc,with_state 用的 map_inner! 于是走 Err 分支,所有端点复制一遍。into_make_service 则在转换时就 with_state(()) 一次,之后每条连接只克隆 Arc。

用真实的监听端口对比,3 条 get_service(Svc) 路由,客户端是 hyper 自己的 client::conn(不读系统代理),开两条连接,每条发 2 个请求:

axum = { version = "=0.8.9", default-features = false, features = ["tokio", "http1"] }
tower = "=0.5.3"
hyper = { version = "=1.12.0", features = ["client", "http1"] }
hyper-util = { version = "=0.1.21", features = ["tokio"] }
tokio = { version = "=1.54.1", features = ["macros", "rt", "net"] }

Svc 和上面的一样,其余部分:

fn app() -> Router {
  Router::new()
    .route("/a", get_service(Svc))
    .route("/b", get_service(Svc))
    .route("/c", get_service(Svc))
}

// 开两条连接,每条发 2 个请求,数每条连接上 Svc 被克隆几次
async fn drive(addr: std::net::SocketAddr, label: &str) {
  for conn_no in 1..=2 {
    let before = clones();
    let stream = tokio::net::TcpStream::connect(addr).await.unwrap();
    let (mut sender, conn) = hyper::client::conn::http1::handshake(TokioIo::new(stream)).await.unwrap();
    tokio::spawn(conn);
    for _ in 0..2 {
      let req = Request::get("/a").header("host", "x").body(Body::empty()).unwrap();
      let res = sender.send_request(req).await.unwrap();
      assert_eq!(res.status(), 200);
    }
    println!("{label}: connection {conn_no}, 2 requests: Svc clones = {}", clones() - before);
  }
}

#[tokio::main(flavor = "current_thread")]
async fn main() {
  let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
  let addr = listener.local_addr().unwrap();
  // 在 spawn 之前建好:get_service 自己会克隆一次,不要算进第一条连接
  let router = app();
  tokio::spawn(async move { axum::serve(listener, router).await.unwrap() });
  drive(addr, "serve(router)").await;

  let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
  let addr = listener.local_addr().unwrap();
  let make = app().into_make_service();
  tokio::spawn(async move { axum::serve(listener, make).await.unwrap() });
  drive(addr, "serve(into_make_service)").await;
}

(Body 来自 axum::body,TokioIo 来自 hyper_util::rt。)跑了 3 次,输出相同:

serve(router): connection 1, 2 requests: Svc clones = 5
serve(router): connection 2, 2 requests: Svc clones = 5
serve(into_make_service): connection 1, 2 requests: Svc clones = 2
serve(into_make_service): connection 2, 2 requests: Svc clones = 2

每个请求克隆 1 次是 MethodRouter 克隆它选中的 Route(Route 与类型擦除),两种写法都有。差出来的 3 次是 serve(router) 每条连接把 3 个端点各复制一次,和路由条数成正比。树不在其中:with_state 不插路径,node 仍是共享的那一份。服务的 Clone 便宜(通常是几个 Arc)时这点开销可以不管;路由多、连接短、Clone 又不便宜时,into_make_service() 把它省掉。

请求进来之后,hyper-util 的 TowerToHyperService 每个请求再克隆一次 Router(hyper 卷),这一次只是 Arc 计数加一。

📝NOTE
冷知识:Router 是在 0.7.4 才包进 Arc 的

0.7 换到 hyper 1.0 之后,issue #2449 报告「Performance regression (requests per second) from 0.6.20 to 0.7.2 when using many routes」:1000 条路由,oha 压测 0.6.20 是 48525 req/s,0.7.2 只有 13334。维护者在 issue 里解释了每个请求克隆 Router 的原因:「hyper's Service trait uses &self whereas tower uses &mut self. So we have to clone the router to get a &mut Router that we can call.」当时 Router 的字段是直接存的,克隆一次就要复制全部路由。

修复是把字段收进 Arc<RouterInner>,本页的 into_inner 就是这时加的。最初的 #2476 要求 Router 先是 Sync,那是破坏性改动,没有合并;#2481 先用几个 Mutex 让 Router 在不改 API 的情况下变成 Sync,再由 #2483(「Internally Arc Router, without breaking changes」)合并 Arc 的改动。issue 里的测试结果:改动后的分支 47294 req/s,回到了 0.6.20 的水平。CHANGELOG 记为「Fix performance regression present since axum 0.7.0」,随 axum 0.7.4 发布。#2481 当时就写明「For axum 0.8, we can then revert this PR」:0.8.0 要求所有 handler 和服务 Sync(#2473),那几个 Mutex 由 #2947(「Remove mutexes around boxed services」)去掉。

call_with_state:三处依次认领

Router<()> 的 Service::call 把 body 换成 axum 的 Body,调 Router::call_with_state:

    pub(crate) fn call_with_state(&self, req: Request, state: S) -> RouteFuture<Infallible> {
        let (req, state) = match self.inner.path_router.call_with_state(req, state) {
            Ok(future) => return future,
            Err((req, state)) => (req, state),
        };

        let (req, state) = match self.inner.fallback_router.call_with_state(req, state) {
            Ok(future) => return future,
            Err((req, state)) => (req, state),
        };

        self.inner
            .catch_all_fallback
            .clone()
            .call_with_state(req, state)
    }
flowchart TB
  req["Request"] --> pr{"path_router 匹配?"}
  pr -->|"是"| ext["写 extensions"]
  pr -->|"否:还回请求"| fr{"fallback_router 匹配?"}
  fr -->|"是"| ext2["只写路径参数"]
  fr -->|"否:还回请求"| caf["catch_all_fallback"]
  ext --> ep["按 RouteId 取端点"]
  ext2 --> ep
  ep --> mr["MethodRouter 按方法分发"]
  ep --> rt["Route 直接调用"]

请求和状态都按值传。没匹配上的 PathRouter 返回 Err((req, state)),把它们原样还回来,下一处接着用,不需要克隆请求。这个 Result 的 Err 很大,所以 PathRouter::call_with_state 上有 #[allow(clippy::result_large_err)]:

        #[cfg(feature = "original-uri")]
        {
            use crate::extract::OriginalUri;

            if req.extensions().get::<OriginalUri>().is_none() {
                let original_uri = OriginalUri(req.uri().clone());
                req.extensions_mut().insert(original_uri);
            }
        }

        let (mut parts, body) = req.into_parts();

        match self.node.at(parts.uri.path()) {
            Ok(match_) => {
                let id = *match_.value;

                if !IS_FALLBACK {
                    #[cfg(feature = "matched-path")]
                    crate::extract::matched_path::set_matched_path_for_request(
                        id,
                        &self.node.route_id_to_path,
                        &mut parts.extensions,
                    );
                }

                url_params::insert_url_params(&mut parts.extensions, match_.params);

                let endpoint = self
                    .routes
                    .get(&id)
                    .expect("no route for id. This is a bug in axum. Please file an issue");

                let req = Request::from_parts(parts, body);
                match endpoint {
                    Endpoint::MethodRouter(method_router) => {
                        Ok(method_router.call_with_state(req, state))
                    }
                    Endpoint::Route(route) => Ok(route.clone().call_owned(req)),
                }
            }
            // explicitly handle all variants in case matchit adds
            // new ones we need to handle differently
            Err(MatchError::NotFound) => Err((Request::from_parts(parts, body), state)),
        }

每一步:

  1. OriginalUri 在匹配之前插入,而且只在还没有时插入。所以 404 的请求也有它,嵌套的内层路由器不会覆盖外层的值。
  2. matchit 按 uri.path() 查树,拿到 RouteId 和路径参数。matchit 只看路径,方法、头都不参与。
  3. 只有 path_router 写 MatchedPath(if !IS_FALLBACK);两个 PathRouter 都写路径参数。
  4. 用 RouteId 在 routes 表里取端点,一次 HashMap 查找。expect 只在树和表不一致时触发,正常情况下不会发生。
  5. Endpoint::MethodRouter 交给 MethodRouter::call_with_state 按方法选槽;Endpoint::Route 克隆后直接调用,不看方法。

fallback_router 里注册了 / 和 /{*__private__axum_fallback},凡是以 / 开头的路径都能匹配上,catch_all_fallback 什么时候才会用到?nest 里的注释给了答案:「It is only used for CONNECT requests with an empty path」。authority 形式的请求目标(CONNECT example.com:443)路径为空,matchit 什么都匹配不上。Fallback::call_with_state 直接调用,不经过匹配,也就不写路径参数。下面用这一点区分请求落在了哪一处:

axum = { version = "=0.8.9", default-features = false }
tower = { version = "=0.5.3", features = ["util"] }
http-body-util = "=0.1.5"
tokio = { version = "=1.54.1", features = ["macros", "rt"] }
use axum::body::Body;
use axum::extract::rejection::RawPathParamsRejection;
use axum::extract::{RawPathParams, Request};
use axum::http::{Method, Uri};
use axum::response::IntoResponse;
use axum::routing::get;
use axum::Router;
use http_body_util::BodyExt;
use tower::ServiceExt;

// fallback_router 匹配时会插入一组(空的)路径参数,catch_all_fallback 不经过匹配
// CONNECT 的 2xx 响应 body 会被 RouteFuture 清空,所以结果放进响应头
async fn fallback(uri: Uri, params: Result<RawPathParams, RawPathParamsRejection>) -> impl IntoResponse {
  let params = match params {
    Ok(p) => format!("Ok({:?})", p.iter().collect::<Vec<_>>()),
    Err(e) => format!("Err({})", e.body_text()),
  };
  let info = format!("path={:?} params={params}", uri.path());
  ([("x-fallback", info)], "from fallback")
}

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

  for (method, uri) in [(Method::GET, "/nope"), (Method::CONNECT, "example.com:443")] {
    let req = Request::builder().method(method.clone()).uri(uri).body(Body::empty()).unwrap();
    let res = app.clone().oneshot(req).await.unwrap();
    let status = res.status();
    let info = res.headers()["x-fallback"].to_str().unwrap().to_owned();
    let body = res.into_body().collect().await.unwrap().to_bytes();
    println!("{method} {uri} -> {status}\n  x-fallback: {info}\n  body: {body:?}");
  }
}

跑了 3 次,输出相同:

GET /nope -> 200 OK
  x-fallback: path="/nope" params=Ok([])
  body: b"from fallback"
CONNECT example.com:443 -> 200 OK
  x-fallback: path="" params=Err(No paths parameters found for matched route)
  body: b""

Router::fallback 把同一个 handler 同时放进 fallback_router(fallback_endpoint)和 catch_all_fallback,所以两个请求都到了同一个函数,但来路不同:/nope 经过 fallback_router 的匹配,拿到一组空的路径参数(通配参数 __private__axum_fallback 被过滤掉了);CONNECT 的路径是空字符串,落到 catch_all_fallback,extensions 里根本没有路径参数,RawPathParams 提取失败。CONNECT 那次 body 为空,是 RouteFuture 对 CONNECT 2xx 响应的处理,见 Route 与类型擦除。

匹配结果写进 extensions

匹配成功后写进请求 extensions 的有三样,后面的提取器从这里读:

写入的值写入位置谁读
OriginalUriPathRouter::call_with_state 开头,没有才写OriginalUri 提取器
MatchedPath(或嵌套时的 MatchedNestedPath)set_matched_path_for_request,只在 path_routerMatchedPath 提取器、中间件
UrlParamsinsert_url_params,两个 PathRouter 都写Path、RawPathParams

百分号解码、非法 UTF-8 怎么处理、axum 自己的通配参数怎么过滤,在 Path、Query 与请求元数据 讲过。这里补一点:insert_url_params 遇到 extensions 里已有 UrlParams::Params 时是追加(current.extend(params)),而不是覆盖。什么时候会有「已有」?只有请求先被一个 Router 匹配过、再进入另一个 Router 的时候。nest 不是这种情况:它把子路由的路径加上前缀直接插进父路由树(「flatten」),一次匹配就得到全部参数。nest_service 才是:父路由器只注册 {prefix}/{*__private__axum_nest_tail_param},子路由器作为一个服务收到剥掉前缀的请求,自己再匹配一次。

axum = { version = "=0.8.9", default-features = false, features = ["matched-path", "original-uri"] }
tower = { version = "=0.5.3", features = ["util"] }
http-body-util = "=0.1.5"
tokio = { version = "=1.54.1", features = ["macros", "rt"] }
use axum::body::Body;
use axum::extract::rejection::RawPathParamsRejection;
use axum::extract::{MatchedPath, OriginalUri, RawPathParams, Request};
use axum::http::Uri;
use axum::routing::get;
use axum::Router;
use http_body_util::BodyExt;
use tower::ServiceExt;

async fn show(
  matched: Option<MatchedPath>,
  OriginalUri(orig): OriginalUri,
  uri: Uri,
  params: Result<RawPathParams, RawPathParamsRejection>,
) -> String {
  let params = match params {
    Ok(p) => format!("{:?}", p.iter().collect::<Vec<_>>()),
    Err(e) => format!("rejection {}: {}", e.status(), e.body_text()),
  };
  format!(
    "uri={uri} original={orig} matched={:?} params={params}",
    matched.as_ref().map(|m| m.as_str())
  )
}

#[tokio::main(flavor = "current_thread")]
async fn main() {
  let items = || Router::new().route("/items/{item}", get(show));
  let app: Router = Router::new()
    .route("/users/{id}", get(show))
    .nest("/t/{tenant}", items())
    .nest_service("/s/{tenant}", items())
    .fallback(show);

  for path in ["/users/7", "/t/acme/items/42", "/s/acme/items/42", "/nope"] {
    let req = Request::get(path).body(Body::empty()).unwrap();
    let res = app.clone().oneshot(req).await.unwrap();
    let body = res.into_body().collect().await.unwrap().to_bytes();
    println!("GET {path}\n  {}", String::from_utf8_lossy(&body));
  }
}

跑了 3 次,输出相同:

GET /users/7
  uri=/users/7 original=/users/7 matched=Some("/users/{id}") params=[("id", "7")]
GET /t/acme/items/42
  uri=/items/42 original=/t/acme/items/42 matched=Some("/t/{tenant}/items/{item}") params=[("tenant", "acme"), ("item", "42")]
GET /s/acme/items/42
  uri=/items/42 original=/s/acme/items/42 matched=Some("/s/{tenant}/items/{item}") params=[("tenant", "acme"), ("item", "42")]
GET /nope
  uri=/nope original=/nope matched=None params=[]

两种嵌套在 handler 看来一模一样,内部走的路不同:

  • /t/...(nest):父路由树里本来就有一条 /t/{tenant}/items/{item},一次匹配拿到 tenant 和 item。
  • /s/...(nest_service):父路由器匹配 /s/{tenant}/{*__private__axum_nest_tail_param},写入 tenant(通配参数被过滤)和 MatchedNestedPath;子路由器看到的路径已经是 /items/42(uri 那一列),它匹配出 item,extend 到 tenant 后面;MatchedPath 由 append_nested_matched_path 把两层模板拼起来。
  • /nope 落到 fallback_router:没有 MatchedPath,路径参数是空列表。OriginalUri 照样有,因为它在匹配之前就写了。

两种嵌套各自的取舍在 nest、merge 与 fallback。

动脑筋答案

build: 3 routes                          Svc clones = 2
route /d (inner not shared)              Svc clones = 0
app.clone()                              Svc clones = 0
route /e (inner shared with keep)        Svc clones = 4
route /f (not shared again)              Svc clones = 0
route /f again with post (merge)         Svc clones = 1

跑了 3 次,输出相同。逐行看:

  • build: 2:不是 route 克隆的。get_service(Svc) 把服务放进 get 槽时自己克隆一次(MethodRouter::on_endpoint 里的 *out = endpoint.clone()),两个 get_service 各一次;route_service 直接装箱,不克隆。所以后面的 MethodRouter 都在计数开始前造好。
  • route /d: 0:链上的 Router 只有一个持有者,Arc::try_unwrap 成功。
  • app.clone(): 0:只是 Arc 计数加一。
  • route /e: 4:keep 还握着同一个 Arc,into_inner 走 Err 分支,/a、/b、/c、/d 四个端点各复制一次。set_node 接着用 Arc::make_mut 把 matchit 树也复制了一份,这个程序数不到它。
  • route /f: 0:上一步得到的是一个新的 Arc,只有 app 持有,又回到独占。
  • route /f again with post: 1:没有共享,但路径已存在,PathRouter::route 合并时 prev_method_router.clone() 把旧 MethodRouter 的 get 槽克隆了一次。旧的那份随后被新的替换掉。

所以「共享时改动」的代价和当时的路由条数成正比。构建时这通常不是问题(一条链是独占的),要注意的是上一节运行时的那一处。

小结

  • Router 是 Arc<RouterInner>,克隆只是计数加一。RouterInner 有四个字段:path_router、fallback_router 两个 PathRouter,default_fallback 标记,以及只给空路径(authority 形式的 CONNECT)用的 catch_all_fallback。
  • PathRouter 的 matchit 树只存 RouteId(u32),端点在旁边的 HashMap 里;树不带状态类型,layer、with_state 只重建端点表,树原样复用。
  • 构建方法都经过 into_inner:Arc::try_unwrap 成功就原地改,失败就逐个复制端点(调用你的服务和 handler 的 Clone),插路径时 Arc::make_mut 再复制树。
  • 直接 axum::serve(listener, router) 时每条连接 self.clone().with_state(()),Arc 共享,端点全部复制一遍;into_make_service() 只在转换时做一次。
  • 请求依次交给 path_router、fallback_router、catch_all_fallback,没匹配上就把 (req, state) 原样还回去。匹配上的写 MatchedPath(只有 path_router)和路径参数,再按 RouteId 查表、交给端点。
  • 路径参数在已有时追加:这只发生在 nest_service 这类「路由器里再套一个路由器」的情形,nest 是一次匹配。

💬 关联讨论

发起讨论
加载中...