敞开生长之旅!这是我参加「日新方案 12 月更文挑战」的第9天,点击检查活动概况
肉身化神
到了现在这一步,想必大家应该也能猜到要干啥了。没错!中间件,它来了!只需完成中间件的刺进和处理,那整个结构的灵魂也就完整了。
中间件是什么
中间件(middlewares),简略说,就对错事务的技能类组件。Web 结构本身不或许去了解所有的事务,因而不或许完成所有的功用。因而,结构需求有一个插口,允许用户自己界说功用,嵌入到结构中,好像这个功用是结构原生支撑的一样。因而,对中间件而言,需求考虑 2 个比较要害的点:
- 刺进点在哪?使用结构的人并不关怀底层逻辑的详细完成,如果刺进点太底层,中间件逻辑就会非常复杂。如果刺进点离用户太近,那和用户直接界说一组函数,每次在
Handler
中手艺调用没有多大的优势了。 - 中间件的输入是什么?中间件的输入,决议了扩展才能。暴露的参数太少,用户发挥空间有限。
那关于咱们这个 Web 结构而言,中间件应该规划成什么样呢?接下来的内容,基本上是我依据路由节点数据结构来完成的。
中间件的方位
先回想下在前面的路由节点树里,咱们其实预留了用来寄存中间件的 hooks
数组。
//Trie 树
#[derive(Clone)]
pub struct RouterNode {
pub pattern: Option<String>, // 待匹配路由,例如 /p/:lang
pub part: Option<String>, // 路由中的一部分,例如 :lang
pub children: Vec<RouterNode>, // 子节点,例如 [doc, tutorial, intro]
pub is_match: bool, // 是否准确匹配,part 含有 : 或 * 时为true
pub hooks: Vec<String>, //中间件钩子函数
pub method: Option<String>, //恳求办法
}
只需把hooks
的内容替换为Handler
即可(怎样还多了个Arc
,跟说好的不一样啊?有兴趣的同学能够自己思考一下):
pub hooks: Vec<Arc<Handler>>
比方作用于全局的中间件,即所有的恳求都会被中间件处理。那只需在顶层的 hooks
加上这个中间件就能够了。如果是作用在分组的中间件,同样能够在该分组的主节点 hooks
加上。甚至单个路由能够在自己的 hooks
加上属于自己的中间件。
中间件规划
咱们这里中间件的界说与路由映射的 Handler
共同,处理的输入是AppContext
对象。刺进点是结构接收到恳求初始化AppContext
对象后,允许用户使用自己界说的中间件做一些额定的处理,例如记载日志等,以及对AppContext
进行二次加工。不过,咱们这里先完成简略的前置中间件,在路由 Handler
之前进行处理。后边有时间再持续优化。
那咱们的中间件要怎样样加到路由节点树上面呢,先来看下面的示例:
let mut router = Router::new()
.hooks(global_hook)
.get("/user", handle_hello)
.get("/ghost/:id", handle_hello);
let g1 = router.group("/admin").hooks(group_hook);
g1.get("/hello", |c: &mut AppContext| {
let s = format!("hello world from admin");
c.string(None, &s)
});
为了让中间件的增加变得简略,咱们分别在根Router
、路由分组RouterGroup
加上hooks
办法。可是,其本质都在调用RouterNode
的add_hooks
impl Router{
...
//新增中间件钩子函数
pub fn hooks<F>(mut self, handler: F) -> Self
where
F: Fn(&mut AppContext) + Send + Sync + 'static,
{
self.roots.hooks.push(Arc::new(handler));
self
}
//新增中间件钩子函数
pub fn add_hooks<F>(&mut self, path: &str, method: Option<&str>, handler: F)
where
F: Fn(&mut AppContext) + Send + Sync + 'static,
{
let method = match method {
Some(method) => Some(method.to_string()),
None => None,
};
if let Some(node) = self.roots.match_path_mut(path, method, true) {
node.add_hooks(handler);
} else {
let parts = parse_pattern(path);
let mut child = self.roots.new_child(parts[0]);
child.add_hooks(handler);
self.roots.children.push(child)
}
}
}
impl RouterNode{
...
//新增中间件钩子函数
pub fn add_hooks<F>(&mut self, handler: F)
where
F: Fn(&mut AppContext) + Send + Sync + 'static,
{
self.hooks.push(Arc::new(handler));
}
}
impl<'a> RouterGroup<'a> {
...
//新增中间件钩子函数
pub fn hooks<F>(self, handler: F) -> Self
where
F: Fn(&mut AppContext) + Send + Sync + 'static,
{
self.router.add_hooks(&self.path, None, handler);
self
}
}
或许仔细的同学会发现:你这个不过是在全局和分组上面增加中间件,那我怎样在单个路由增加中间件啊。
哎,这位同学是有些仔细,但还不够仔细。我直接调用Router
的add_hooks
函数,只需传入相应的路由和handler
,不就增加上了单个路由的中间件了。
当然,中间件这么简略就增加上去了,好像也太儿戏了。(没办法,个人才能有限,暂时也没有更好的处理办法了,后边有时间再揣摩揣摩,也欢迎大家来讨论完善)
优雅关机
现在,咱们这个web结构的基本架构现已完成了。尽管非常简陋,也不是不能用。咱们持续来优化一下,逐步丰厚一下细节部分,比方加上CTRL+C
关机。
其实hyper
和tokio
本身是带有关机服务的,咱们的结构是根据hyper
和tokio
,天然也是调用他们的办法:
pub async fn run(addr: SocketAddr, router: Router) {
...
let server = Server::bind(&addr).serve(make_service_fn);
let graceful = server.with_graceful_shutdown(shutdown_signal());
// Await the `server` receiving the signal...
if let Err(e) = graceful.await {
eprintln!("server error: {}", e);
}
}
//CTRL+C关机
async fn shutdown_signal() {
tokio::signal::ctrl_c().await.expect("CTRL+C 关机失败");
}
日志记载
咱们再加上日志记载和一些debug信息,这样整个路由链路追踪起来也更方便些。
当然,为了方便debug
,咱们先在Router
和路由节点树RouterNode
完成Debug
特征:
use std::fmt::Debug;
impl Debug for Router {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Router").field("roots", &self.roots).finish()
}
}
impl Debug for RouterNode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RouterNode")
.field("pattern", &self.pattern)
.field("part", &self.part)
.field("children", &self.children)
.field("is_match", &self.is_match)
.field("method", &self.method)
.finish()
}
}
在Cargo.toml
增加log
日志库:
[dependencies]
...
log = "0.4.17"
[dev-dependencies]
env_logger = "0.9.3"
[[example]]
name = "hello"
path = "examples/hello.rs"
注意:env_logger
这个库是在dev-dependencies
环境。这里的依靠只会在运行测验、示例和 benchmark 时才会被引进。
别的,在遍地函数埋下记载点位:
async fn handler(addr: SocketAddr, req: Request<Body>, router: Arc<Router>) -> Result<Response<Body>, Infallible> {
//获取节点
let (node, params) = router.get_route(req.method().clone().into(), req.uri().path());
debug!("当时路由节点:{:#?}", node);
if let Some(node) = node {
...
debug!("当时路由AppContext:{:#?}", context);
let mut hooks = Vec::new();
(handle)(&mut context);
} else {
context.string(Some(StatusCode::NOT_FOUND), "404 not found");
}
Ok(context.response)
}
pub async fn run(addr: SocketAddr, router: Router) {
...
debug!("路由表:{:#?}", router.clone());
...
let graceful = server.with_graceful_shutdown(shutdown_signal());
info!("发动成功,发动端口为: {}", addr);
...
}
async fn shutdown_signal() {
tokio::signal::ctrl_c().await.expect("CTRL+C 关机失败");
info!("收到ctrl+c关机服务");
}
//获取路由节点
pub fn get_route(&self, method: Method, path: &str) -> (Option<&RouterNode>, HashMap<String, String>) {
trace!("查找路由:{}", path);
let search_parts = parse_pattern(path);
...
trace!("路径中的参数值:{:?}", params);
return (n, params);
}
//刺进节点
pub fn insert(&mut self, method: Method, path: &str, parts: Vec<&str>, height: usize) {
if parts.len() == height {
self.pattern = Some(String::from(path));
self.method = Some(String::from(method.as_str()));
trace!("刺进节点:{:#?}",self);
return;
}
...
}
刚好也能够梳理一下现在的文件目录,将RouterGroup
、RouterNode
和Router
以及AppContext
拆分出来提取到单个文件,而之前用来测验的main
函数,也可提取到examples/hello.rs
,作为独立的测验文件。现在,整个结构的目录应该如下:
在main
函数中初始化env_logger
日志库,加上几个测验的路由:
use env_logger::{self, Env};
use ray::{context::AppContext, router::Router, server};
use std::net::SocketAddr;
#[tokio::main]
async fn main() {
let env = Env::default()
.filter_or("MY_LOG_LEVEL", "debug")//这里修正日志等级
.write_style_or("MY_LOG_STYLE", "always");
env_logger::init_from_env(env);
let handle_hello = |c: &mut AppContext| c.string(None, "hello world from handler");
let get_hello = |c: &mut AppContext| {
let key = format!("hello world from post,query: {}", c.params.get("id").unwrap());
return c.string(None, &key);
};
let post_hello = |c: &mut AppContext| {
let key = format!("hello world from post,query");
return c.string(None, &key);
};
let global_hook = |c: &mut AppContext| println!("Hello from global_hook,path: {:?}", c.path);
let group_hook = |c: &mut AppContext| println!("Hello from group_hook,path: {:?}", c.path);
let handle_hook = |c: &mut AppContext| println!("Hello from handle_hook,path: {:?}", c.path);
let mut router = Router::new()
.hooks(global_hook)
.get("/user", handle_hello)
.get("/user/index", handle_hello)
.get("/ghost/:id", get_hello)
router.add_hooks("/user/index",Some("get"), handle_hook);
let g1 = router.group("/admin").hooks(group_hook);
g1.get("/hello", |c: &mut AppContext| {
let s = format!("hello world from admin");
c.string(None, &s)
})
.get("/index", |c: &mut AppContext| {
let s = format!("hello world from index");
c.string(None, &s)
});
// Run the server like above...
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
server::run(addr, router).await;
}
最终,运行一下测验文件,应该能够看到不少日志信息:
cargo run --example hello