敞开生长之旅!这是我参加「日新方案 12 月更文挑战」的第9天,点击检查活动概况

肉身化神

到了现在这一步,想必大家应该也能猜到要干啥了。没错!中间件,它来了!只需完成中间件的刺进和处理,那整个结构的灵魂也就完整了。

中间件是什么

中间件(middlewares),简略说,就对错事务的技能类组件。Web 结构本身不或许去了解所有的事务,因而不或许完成所有的功用。因而,结构需求有一个插口,允许用户自己界说功用,嵌入到结构中,好像这个功用是结构原生支撑的一样。因而,对中间件而言,需求考虑 2 个比较要害的点:

  1. 刺进点在哪?使用结构的人并不关怀底层逻辑的详细完成,如果刺进点太底层,中间件逻辑就会非常复杂。如果刺进点离用户太近,那和用户直接界说一组函数,每次在 Handler 中手艺调用没有多大的优势了。
  2. 中间件的输入是什么?中间件的输入,决议了扩展才能。暴露的参数太少,用户发挥空间有限。

那关于咱们这个 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办法。可是,其本质都在调用RouterNodeadd_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
    }
}

或许仔细的同学会发现:你这个不过是在全局和分组上面增加中间件,那我怎样在单个路由增加中间件啊。

哎,这位同学是有些仔细,但还不够仔细。我直接调用Routeradd_hooks函数,只需传入相应的路由和handler,不就增加上了单个路由的中间件了。

当然,中间件这么简略就增加上去了,好像也太儿戏了。(没办法,个人才能有限,暂时也没有更好的处理办法了,后边有时间再揣摩揣摩,也欢迎大家来讨论完善)

优雅关机

现在,咱们这个web结构的基本架构现已完成了。尽管非常简陋,也不是不能用。咱们持续来优化一下,逐步丰厚一下细节部分,比方加上CTRL+C关机。

其实hypertokio本身是带有关机服务的,咱们的结构是根据hypertokio,天然也是调用他们的办法:

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;
    }
    ...
}

刚好也能够梳理一下现在的文件目录,将RouterGroupRouterNodeRouter以及AppContext拆分出来提取到单个文件,而之前用来测验的main函数,也可提取到examples/hello.rs,作为独立的测验文件。现在,整个结构的目录应该如下:

用 rust 从零开发一套 web 框架:day5

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