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 每月下载次数
170KB
3K SLoC
Treemux
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
路由优先级
路由器中的优先级规则很简单。
- 静态路径段具有最高优先级。如果一个段及其子树能够匹配URL,则返回该匹配项。
- 通配符具有第二优先级。为了匹配特定通配符,该通配符及其子树必须匹配URL。
- 最后,当之前的路径段已经匹配,且没有静态或通配符条件匹配时,通配符规则将匹配。通配符规则必须位于模式的末尾。
因此,以下模式来自 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