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 与类型擦除。
四个字段
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,专放 fallback | new_fallback 在 / 和 /{*__private__axum_fallback} 各放一个 NotFound |
default_fallback | fallback 是不是还是默认的那个;merge、nest 靠它决定用谁的 fallback | true |
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 加一条路由分两种情况:
- 路径已经在
path_to_route_id里,并且那里是一个MethodRouter:把旧的prev_method_router.clone()和新的合并(merge_for_path,同一方法重复时 panic,见 MethodRouter),放回原来的RouteId。树不动。.route("/", get(a)).route("/", post(b))就是这样工作的。 - 否则
next_route_id分配新编号(checked_add(1),超过u32::MAX条路由时 panic「Overu32::MAXroutes 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 与路径语法。
问: 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 在这里被调用。
平台 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 计数加一。
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)),
}
每一步:
OriginalUri在匹配之前插入,而且只在还没有时插入。所以 404 的请求也有它,嵌套的内层路由器不会覆盖外层的值。- matchit 按
uri.path()查树,拿到RouteId和路径参数。matchit 只看路径,方法、头都不参与。 - 只有
path_router写MatchedPath(if !IS_FALLBACK);两个PathRouter都写路径参数。 - 用
RouteId在routes表里取端点,一次HashMap查找。expect只在树和表不一致时触发,正常情况下不会发生。 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 的有三样,后面的提取器从这里读:
| 写入的值 | 写入位置 | 谁读 |
|---|---|---|
OriginalUri | PathRouter::call_with_state 开头,没有才写 | OriginalUri 提取器 |
MatchedPath(或嵌套时的 MatchedNestedPath) | set_matched_path_for_request,只在 path_router | MatchedPath 提取器、中间件 |
UrlParams | insert_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是一次匹配。