likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 60

肉身化神

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

中间件是什么

中间件(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
转载自:https://juejin.cn/post/7174339191717756941
评论
请登录