18个版本

0.8.5 2023年4月13日
0.8.4 2023年2月2日
0.8.3 2022年10月1日
0.8.1 2022年5月3日
0.5.1 2021年2月6日

#150 in HTTP服务器

43 每月下载次数

MIT 许可证

170KB
3K SLoC

Treemux

Documentation Version License Actions

Treemux 是一个轻量级的高性能 HTTP 请求路由器。

此路由器支持路由模式中的变量,并匹配请求方法。它也非常可扩展。

此路由器针对高性能和小内存占用进行了优化。即使在非常长的路径和大量路由的情况下也能很好地扩展。使用压缩动态 trie(radix tree)结构进行高效匹配。

Treemux 是由 @ibraheemdev 的 httprouter-rs 分支而来,但现在已经大部分重写。如果您熟悉 Go 世界,这是 @dimfeld 的 httptreemux 与 @julienschmidt 的 httprouter 的等效物。它还添加了一些基于 tower-layer 的中间件支持。

用法

以下是一个简单的示例

use treemux::{middleware_fn, Treemux, RouterBuilder, Params, RequestExt};
use treemux::middlewares;
use std::convert::Infallible;
use hyper::{Request, Response, Body};
use hyper::http::Error;

async fn index(_: Request<Body>) -> Result<Response<Body>, Error> {
  Ok(Response::new("Hello, World!".into()))
}

async fn hello(req: Request<Body>) -> Result<Response<Body>, Error> {
  let user_name = req.params().get("user").unwrap();
  Ok(Response::new(format!("Hello, {}", user_name).into()))
}

#[tokio::main]
async fn main() {
  let router = Treemux::builder();
  let mut router = router.middleware(middleware_fn(middlewares::log_requests));
  router.get("/", index);
  router.get("/hello/:user", hello);

  hyper::Server::bind(&([127, 0, 0, 1], 3000).into())
    .serve(router.into_service())
    .await;
}

处理器

处理器是一个 RequestHandler,可以从简单的异步函数 async fn(hyper::Request) -> Result<hyper::Response<Body>, http::Error> 创建。

中间件

此路由器支持中间件堆栈。中间件是实现 tower layers Layer<RequestHandler, Service = RequestHandler> 的实现。有一个辅助函数将闭包转换为层。请参阅 示例

路由作用域

您可以将路由范围指定到基本路径,以保持代码更加简洁,这也支持命名参数。

use hyper::{Body, Request, Response, Server};

use treemux::{RequestExt, RouterBuilder, Treemux};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
  tracing_subscriber::fmt::init();

  let mut router = Treemux::builder();
  router.get("/docs", |_req| async {
    Ok(Response::new("This displays some docs".into()))
  });

  let mut api_group = router.scope("/api");
  api_group.get("/todos/:id", |req: Request<Body>| async move {
    let body = format!(
      "{} {} id={}",
      req.method(),
      req.route(),
      req.params().get("id").unwrap().to_string()
    );
    Ok(Response::new(body.into()))
  });

  let _server = Server::bind(&([127, 0, 0, 1], 3000).into())
    .serve(router.into_service())
    .await?;
  Ok(())
}

路由规则

路径中的每个变量只能与一个段匹配,除非是URL末尾的可选通配符变量。

一些有效的URL模式示例包括

  • /post/all
  • /post/:postid
  • /post/:postid/page/:page
  • /post/:postid/:page
  • /images/*path
  • /favicon.ico
  • /:year/:month/
  • /:year/:month/:post
  • /:page

注意,上述所有URL模式可以在路由器中同时存在。

: 开头的路径元素表示路径中的通配符。通配符只能匹配单个路径段。也就是说,模式 /post/:postid 将匹配 /post/1 或 /post/1/,但不能匹配 /post/1/2。

* 开头的路径元素是一个通配符,其值将是一个字符串,包含由通配符匹配的URL中的所有文本。例如,对于模式 /images/*path 和请求的URL images/abc/def,path 将包含 abc/def。通配符路径不会匹配空字符串,因此在此示例中,如果也想匹配 /images/,则需要安装单独的路由。

在路由模式中使用 : 和 *

可以通过转义字符 :* 在路径段的开始处使用它们。段开头的双重反斜杠被解释为单个反斜杠。这些转义只在路径段的非常开头进行检查;在其他地方不需要或处理这些转义。

router.get("/foo/\\*starToken", handler) // matches /foo/*starToken
router.get("/foo/star*inTheMiddle", handler) // matches /foo/star*inTheMiddle
router.get("/foo/starBackslash\\*", handler) // matches /foo/starBackslash\*
router.get("/foo/\\\\*backslashWithStar", handler) // matches /foo/\*backslashWithStar

路由分组

允许您使用给定的路径前缀创建一组新的路由。这使得创建像

  • /api/v1/foo
  • /api/v1/bar

这样的路径簇更容易。要使用它,您这样做

let mut router = Treemux::builder();
let mut api = router.scope("/api/v1");
api.get("/foo", fooHandler) // becomes /api/v1/foo
api.get("/bar", barHandler) // becomes /api/v1/bar

路由优先级

路由器中的优先级规则很简单。

  1. 静态路径段具有最高优先级。如果一个段及其子树能够匹配URL,则返回该匹配项。
  2. 通配符具有第二优先级。为了匹配特定通配符,该通配符及其子树必须匹配URL。
  3. 最后,当之前的路径段已经匹配,且没有静态或通配符条件匹配时,通配符规则将匹配。通配符规则必须位于模式的末尾。

因此,以下模式来自 simpleblog 的改编,我们将看到某些匹配

let mut router = Treemux::builder();
router.get("/:page", pageHandler)
router.get("/:year/:month/:post", postHandler)
router.get("/:year/:month", archiveHandler)
router.get("/images/*path", staticHandler)
router.get("/favicon.ico", staticHandler)

示例场景

  • /abc 将匹配 /:page
  • /2014/05 将匹配 /:year/:month
  • /2014/05/really-great-blog-post 将匹配 /:year/:month/:post
  • /images/CoolImage.gif 将匹配 /images/*path
  • /images/2014/05/MayImage.jpg 也将匹配 /images/*path,其中所有在 /images 之后的文本都将存储在变量 path 中。
  • /favicon.ico 将匹配 /favicon.ico

特殊方法行为

如果将 Treemux.head_can_use_get 设置为 true,当处理 HEAD 请求时,如果没有为该模式添加 HEAD 处理程序,路由器将调用该模式的 GET 处理程序。这种行为默认启用。

Hyper 已经正确处理了 HEAD 方法,只发送头部信息,所以在大多数情况下,您的处理程序不需要为它添加任何特殊处理。

默认情况下,Treemux::global_options 是一个空处理程序,不会影响您的路由。如果您设置了处理程序,它将在已由其他方法注册的路径上的 OPTIONS 请求上被调用。如果您使用 Treeemux::options 设置特定路径的处理程序,它将覆盖该路径的全局选项处理程序。

use treemux::{Treemux, RouterBuilder};
use hyper::{Request, Response, Body};
use hyper::http::Error;

async fn global_options(_: Request<Body>) -> Result<Response<Body>, Error> {
  Ok(Response::builder()
      .header("Access-Control-Allow-Methods", "Allow")
      .header("Access-Control-Allow-Origin", "*")
      .body(Body::empty())
      .unwrap())
}

async fn options_override(_: Request<Body>) -> Result<Response<Body>, Error> {
  Ok(Response::builder()
      .header("Access-Control-Allow-Methods", "Allow")
      .header("Access-Control-Allow-Origin", "ui.example.com")
      .body(Body::empty())
      .unwrap())
}


fn main() {
  let mut router = Treemux::builder();
  router.global_options(global_options);
  router.options("/otheropts", options_override);
}

尾部斜杠

路由器对带有尾部斜杠的路径有特殊处理。如果将带有尾部斜杠的模式添加到路由器中,任何不带有尾部斜杠的匹配都将重定向到带有斜杠的版本。如果模式没有尾部斜杠,带有尾部斜杠的匹配将重定向到不带斜杠的版本。

尾部斜杠标志仅针对模式存储一次。也就是说,如果为带有尾部斜杠的方法添加模式,该模式的所有其他方法也将被视为具有尾部斜杠,无论是否指定了这些方法的尾部斜杠。然而,可以通过将 Treemux.redirect_trailing_slash 设置为 false 来关闭此行为。默认设置为 true。

此规则的例外是通配符模式。默认情况下,在通配符模式下禁用尾部斜杠重定向,因为无法预测整个 URL 结构和所需模式。如果希望在通配符模式下删除尾部斜杠,请将 Treemux::remove_catch_all_trailing_slash 设置为 true。

let mut router = Treemux::builder()
router.get("/about", pageHandler)
router.get("/posts/", postIndexHandler)
router.post("/posts", postFormHandler)

/*
GET /about will match normally.
GET /about/ will redirect to /about.
GET /posts will redirect to /posts/.
GET /posts/ will match normally.
POST /posts will redirect to /posts/, because the GET method used a trailing slash.
*/

自定义重定向

RedirectBehavior 设置在路由器使用 RedirectTrailingSlash 或 RedirectClean 将请求重定向到请求 URL 的规范版本时的行为。默认行为是返回 301 状态,将浏览器重定向到与给定模式匹配的 URL 版本。

这是 RedirectBehavior 接受的值。您还可以将这些值添加到 RedirectMethodBehavior 映射中,以定义每个方法的自定义重定向行为。

  • Redirect301 - HTTP 301 永久移动;这是默认值。
  • Redirect307 - HTTP/1.1 临时重定向
  • Redirect308 - RFC7538 永久重定向
  • UseHandler - 不要重定向到规范路径。只需调用处理程序。

理由/用法

POST 请求上,大多数接收到 301 的浏览器会提交一个 GET 请求到重定向的 URL,这意味着任何数据都可能丢失。如果您想处理并避免这种行为,可以使用 Redirect307,这会导致大多数浏览器使用原始方法和请求体重新提交请求。

由于 307 应该是一个临时重定向,因此提出了新的 308 状态代码,它与它相同,除了它正确地表示重定向是永久的。这里的大问题是 RFC 相对较新,较老或不遵守规范的浏览器将无法处理它。因此,除非您真的知道自己在做什么,否则不建议使用。

最后,UseHandler值将直接调用模式对应的处理函数,而不将URL重定向到规范版本。

转义斜杠

Hyper会自动处理URL中的转义字符,将“+”转换为空格,将“%XX”转换为相应的字符。当URL中包含未转义的“%2f”时,这可能会引发问题,因为“%2f”会被转义为“/”。这对大多数应用来说不是问题,但它将阻止路由器正确匹配路径和通配符。

例如,模式/post/:post不会匹配/post/abc%2fdef,因为“abc%2fdef”未转义为/post/abc/def。期望的行为是匹配,并将post通配符设置为abc/def

因此,此路由器默认使用存储在Request.RequestURI变量中的原始URL。然后对通配符和捕获所有进行转义,以实现期望的行为。

错误处理程序

未找到处理程序

您可以使用另一个处理程序来处理无法由此路由器匹配的请求,方法是使用Router::not_found处理程序。

例如,not_found处理程序可以用来返回404页面

use treemux::Treemux;
use hyper::{Request, Response, Body};
use hyper::http::Error;

async fn not_found(_req: Request<Body>) -> Result<Response<Body>, Error> {
  Ok(Response::builder()
    .status(404)
    .body(Body::empty())
    .unwrap())
}

fn main() {
  let mut router = Treemux::builder();
  router.not_found(not_found);
}

方法不允许处理程序

如果模式匹配,但没有与请求方法相关联的处理程序,则路由器将调用Treemux::method_not_allowed处理程序。此处理程序的默认版本只写入状态码405并适当地设置Allow响应头字段。

use treemux::{AllowedMethods, RouterBuilder, Treemux};
use hyper::{
  header::{ALLOW, CONTENT_TYPE},
  http, Request, Response, StatusCode, Body
};

async fn method_not_allowed(req: Request<Body>) -> Result<Response<Body>, http::Error> {
  let allowed = req
    .extensions()
    .get::<AllowedMethods>()
    .map(|v| {
      v.methods()
        .iter()
        .map(|v| v.as_str().to_string())
        .collect::<Vec<String>>()
        .join(", ")
    })
    .unwrap_or_default();

  Ok(
    Response::builder()
      .status(StatusCode::METHOD_NOT_ALLOWED)
      .header(CONTENT_TYPE, "application/json; charset=utf-8")
      .header(ALLOW, &allowed)
      .body(format!(
        r#"{{"message":"Method not allowed, try {}"}}"#,
        allowed
      ).into())
      .unwrap(),
  )
}

fn main() {
  let mut router = Treemux::builder();
  router.method_not_allowed(method_not_allowed);
}

恐慌处理

可以将Treemux::panic_handler设置为提供自定义的恐慌处理。默认的恐慌处理程序会在错误级别记录堆栈跟踪。

额外用例

静态文件

您可以使用路由器通过static_files辅助方法提供静态文件目录中的页面。请记住,您需要指定两个路由来处理“/”和“/*”的情况。

要启用此功能,请在您的cargo.toml中添加hyper-staticfile crate。

use hyper::Server;
use treemux::{static_files, RouterBuilder, Treemux};

// requires `hyper-staticfile = "0.6"` in cargo.toml

#[tokio::main]
async fn main() -> anyhow::Result<()> {
  let mut router = Treemux::builder();
  router.get("/", static_files("./examples/static"));
  router.get("/*", static_files("./examples/static"));

  Server::bind(&([127, 0, 0, 1], 3000).into())
    .serve(router.into_service())
    .await?;
  Ok(())
}

多域名/子域名

这里有一个快速示例:您的服务器是否服务多个域名/主机?您想使用子域名?为每个主机定义一个路由器!

use treemux::{Treemux, RouterBuilder};
use hyper::service::{make_service_fn, service_fn};
use hyper::{http, Body, Request, Response, Server, StatusCode};
use std::collections::HashMap;
use std::convert::Infallible;
use std::sync::Arc;

pub struct HostSwitch(HashMap<String, Treemux>);

impl HostSwitch {
  async fn serve(&self, req: Request<Body>) -> Result<Response<Body>, http::Error> {
    let forbidden = Response::builder()
      .status(StatusCode::FORBIDDEN)
      .body(Body::empty())
      .unwrap();
    match req.headers().get("host") {
      Some(host) => match self.0.get(host.to_str().unwrap()) {
        Some(router) => router.serve(req).await,
        None => Ok(forbidden),
      },
      None => Ok(forbidden),
    }
  }
}

async fn hello(_: Request<Body>) -> Result<Response<Body>, http::Error> {
    Ok(Response::new(Body::default()))
}

#[tokio::main]
async fn main() {
  let mut router = Treemux::builder();
  router.get("/", hello);

  let mut host_switch: HostSwitch = HostSwitch(HashMap::new());
  host_switch.0.insert("example.com:12345".into(), router.into());

  let host_switch = Arc::new(host_switch);
  
  let make_svc = make_service_fn(move |_| {
    let host_switch = host_switch.clone();
    async move {
      Ok::<_, Infallible>(service_fn(move |req: Request<Body>| {
        let host_switch = host_switch.clone();
        async move { host_switch.serve(req).await }
      }))
    }
  });

  let server = Server::bind(&([127, 0, 0, 1], 3000).into())
      .serve(make_svc)
      .await;
}

依赖关系

~7–20MB
~294K SLoC