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

8,408 阅读7分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第5天,点击查看活动详情

我学习 rust 也有不少时间了,总是感觉自己这半桶水没点真功夫,写点小 demo 吧,基本都用别人封装好的现成api,面向api开发又感觉没啥难度。这样下去很难提升啊!思来想去睡不着,总想着自己应该能干点啥。 那么,就从我天天 crud 的项目开始。用 rust 从零开发一套 web 框架!自己动手造轮子!

经过我在网络上不断搜索,最终发现了极客兔兔大佬的博客,里面有好几个用 go 从零开发项目的教程。那我灵感就来了。照猫画虎,用 rust 重新开发一遍。当然,因为语言不一样,最终实现的逻辑肯定也是有差别的。目前先实现 web 框架,后面有时间再复现一遍其他项目。

初试牛刀

在正式敲代码之前,首先要介绍一下 hyper 这个底层库。这个库可以说是目前众多 rust HTTP 相关开发库的祖师爷,reqwest、warp、axum、actix-web、salvo 等等一大票网络库都是在 hyper 的基础上进行开发,毫不客气的说 hyper 已成为 Rust 网络程序生态的重要基石之一。当然,我这个项目也是站在巨人的肩膀上进行二次开发。

那么,首先我们来认识一下 hyper,下面的代码是我从官方文档进行复制并且稍微改动了一下:

use std::net::SocketAddr;
use hyper::{Body, Request, Response, Server,Method};
use hyper::service::{make_service_fn, service_fn};
use hyper::server::conn::AddrStream;
use std::convert::Infallible;

#[derive(Clone)]
struct AppContext {
    // Whatever data your application needs can go here
}

async fn handler(
    context: AppContext,
    addr: SocketAddr,
    req: Request<Body>
) -> Result<Response<Body>, Infallible> {

    match (req.uri().path(),req.method())  {
        ("/",&Method::GET)=> Ok(Response::new(Body::from("Hello World"))),
        ("/index",&Method::GET)=> Ok(Response::new(Body::from("Hello from index"))),
        _=> Ok(Response::new(Body::from("Hello empty")))
    }
}

#[tokio::main]
async fn main() {
    let context = AppContext {
        // ...
    };

    // A `MakeService` that produces a `Service` to handler each connection.
    let make_service = make_service_fn(move |conn: &AddrStream| {
        // We have to clone the context to share it with each invocation of
        // `make_service`. If your data doesn't implement `Clone` consider using
        // an `std::sync::Arc`.
        let context = context.clone();

        // You can grab the address of the incoming connection like so.
        let addr = conn.remote_addr();

        // Create a `Service` for responding to the request.
        let service = service_fn(move |req| {
            handler(context.clone(), addr, req)
        });

        // Return the service to hyper.
        async move { Ok::<_, anyhow::Error>(service) }
    });

    // Run the server like above...
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));

    let server = Server::bind(&addr).serve(make_service);

    if let Err(e) = server.await {
        eprintln!("server error: {}", e);
    }
}

从示例中 可以看出,server 收到 HTTP 请求后,调用 handler 函数进行处理,它就是我们常说的 HTTP handler。在 hyper 中,HTTP handler 仍然需要直接与 HTTP Request, Response 打交道。

在示例代码中使用了 make_service_fn, service_fn,但其实最重要的概念是 Service,它是 tower 中定义的一个 trait

pub trait Service<Request> {
type Response;
type Error;
type Future: Future<Output = Result<Self::Response, Self::Error>>;

    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>>;

    fn call(&mut self, req: Request) -> Self::Future;

}

通过查看 make_service_fn, service_fn这两个函数的源码便可发现:

Service 是对 request-response 模式的抽象。 request-response 模式是非常强大的,很多问题都可以用这个模式来表达。 比如前面提到 Connect trait,就可看作为 request=uri, response=connectionservice。 更进一步,其实任何函数都可视为 request/response,函数参数即 request,返回值即 response

poll_ready() 用于探测 service 的状态,是否正常工作,是否过载等。只有当 poll_ready() 返回 Poll::Ready(Ok(())) 时,才可以调用 call() 处理请求。

call() 则是真正处理请求的地方,它返回一个 future,因此相当于 async fn(Request) -> Result<Response, Error>

make_service_fn() 返回的类型为 MakeServiceFn, service_fn()返回的是 ServiceFn,它们都实现了 Service trait

MakeServiceFncall() 逻辑是,以新建的连接(AddrStream)为参数并返回一个 ServiceFn。相当于说,MakeServiceFnrequest=AddrStream, response=ServiceFn

ServiceFncall()逻辑则是,以 request 为参数,返回 response

同样,示例代码中也看到,我们可以直接对Server 进行 await。这是因为 Server 实现了 FutureServer 的逻辑是,不断调用 accept 接受新连接,然后通过 MakeServiceFn为该连接创建 ServiceFn,并通过 ServiceFn 处理这个连接上所有的请求。

这里的关键信息是,MakeServiceFn 全局只有一个,ServiceFn 每个连接创建一个。 如果我们想要跨 handler 共享信息,或者进行一些处理,就得通过 MakeServiceFnServiceFn 了。

渐入佳境

下面,就该我隆重登场了。

仔细观察实例中的handler函数,当你看到urimethod以及返回的数据。发现这不正是 web 框架处理路由和handler函数,并且返回Response的地方吗?

换句话来说,后面我们大部分的操作都是基于这个示例并且在handler函数内进行拓展。

我们先定义几个结构:

//上下文参数
struct AppContext {
   pub response: Response<Body>,
}

/// 请求处理函数
type Handler = dyn Fn(&mut AppContext) + Send + Sync + 'static;

//路由
pub struct Router {
    handlers: HashMap<String, Box<Handler>>,
}

我们定义最原始的路由,里面用hashmap存储了路由路径和请求的Handle函数。 Handler是一个类型别名,准确来说是实现了 Fn(&mut AppContext)特征的闭包,要存储闭包的话。就只能用 box 把它包裹一层。至于AppContext暂时用来表示函数的返回数据,后面会逐步进行拓展(或者对Handler进一步改造)。

为了实现Router::new().get("/index", handle_hello).get("/hello", handle_hello)这样的路由写法,我们为Router实现一些方法:

impl Router {
    pub fn new() -> Self {
        Router { routes: HashMap::new() }
    }
    fn add_route<F>(mut self, path: &str, method: Method, handler: F) -> Self
    where
        F: Fn(&mut AppContext) + Send + Sync + 'static,
    {
        let key = format!("{}+{}", path, method);
        self.handlers.insert(key, Box::new(handler));
        self
    }

    fn get<F>(self, path: &str, handler: F) -> Self
    where
        F: Fn(&mut AppContext) + Send + Sync + 'static,
    {
        self.add_route(path, Method::GET, handler)
    }

    fn post<F>(self, path: &str, handler: F) -> Self
    where
        F: Fn(&mut AppContext) + Send + Sync + 'static,
    {
        self.add_route(path, Method::POST, handler)
    }

    fn delete<F>(self, path: &str, handler: F) -> Self
    where
        F: Fn(&mut AppContext) + Send + Sync + 'static,
    {
        self.add_route(path, Method::DELETE, handler)
    }

    fn put<F>(self, path: &str, handler: F) -> Self
    where
        F: Fn(&mut AppContext) + Send + Sync + 'static,
    {
        self.add_route(path, Method::PUT, handler)
    }

    fn patch<F>(self, path: &str, handler: F) -> Self
    where
        F: Fn(&mut AppContext) + Send + Sync + 'static,
    {
        self.add_route(path, Method::PATCH, handler)
    }
}

这里要特别提一嘴add_route这个函数,因为真正添加路由的方法在这里(往 hashMap 中添加内容),所以它传入的第一个参数是必须是mut self,表示可变的参数。但是其他的get/post等方法却是传入了self,有兴趣的同学可以思考一下为什么呢?或者能不能也改为mut self&self之类的?

接着对 handler 函数进行改造,把handler函数从hashMap提取出来,并执行:

async fn handler(addr: SocketAddr, req: Request<Body>, router: Arc<Router>) -> Result<Response<Body>, Infallible> {
    let key = format!("{}+{}", req.uri().path(), req.method().as_str());
    if let Some(handle) = router.handlers.get(&key) {
        let mut context = AppContext {
            response: Response::new(Body::empty()),
        };
        (handle)(&mut context);
        Ok(context.response)
    } else {
        Ok(Response::new(Body::from("404 not found")))
    }
}

最后回到 main 函数,在之前示例的基础上改动一下:

async fn main() {
    let handle_hello = |c: &mut AppContext| {
        c.response = Response::new(Body::from("handle_hello"));
        println!("Hello, from {:#?}", c.response);
    };

    let router: Arc<Router> = Arc::new(Router::new().get("/index", handle_hello));
      ...
    let make_service = make_service_fn(move |conn: &AddrStream| {
      ....
        let service = service_fn(move |req| handler(addr, req, router.clone()));
      ...
    });
    ....
}

服务跑起来,打开http://localhost:3000/index或者http://localhost:3000,便可以看到效果了。

筑基成功

现在功能已经实现了,我们再对函数进行拆分和封装一下。

router 相关的内容提取到router.rs,把handler函数和main函数里的运行逻辑提取到server.rs文件。再把相关内容在lib.rs进行导出。

pub mod router;
pub mod server;

现在运行入口改为run函数,将 监听的 ip 端口和路由传入,然后在main函数中触发即可。

pub async fn run(addr: SocketAddr, router: Router) {
     let router: Arc<Router> = Arc::new(router);

    // A `MakeService` that produces a `Service` to handler each connection.
    let make_service_fn = make_service_fn(move |conn: &AddrStream| {
        // We have to clone the context to share it with each invocation of
        // `make_service`. If your data doesn't implement `Clone` consider using
        // an `std::sync::Arc`.
        let router = router.clone();
        // You can grab the address of the incoming connection like so.
        let addr = conn.remote_addr();

        // Create a `Service` for responding to the request.
        let service = service_fn(move |req| handler(addr, req, router.clone()));

        // Return the service to hyper.
        async move { Ok::<_, Infallible>(service) }
    });

    let server = Server::bind(&addr).serve(make_service_fn);

    if let Err(e) = server.await {
        eprintln!("server error: {}", e);
    }
}
use ray::router::{AppContext, Router};
use ray::server;
use hyper::{Body, Response};
use std::net::SocketAddr;
use std::sync::Arc;

#[tokio::main]
async fn main() {
    let handle_hello = |c: &mut AppContext| {
        c.data = Response::new(Body::from("handle_hello"));
    };

    let router = Router::new().get("/index", handle_hello);

    // Run the server like above...
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));

    server::run(addr, router).await;
}

好了,我们整个框架的原型已经出来了。实现了路由映射表,提供了用户注册静态路由的方法,同时也封装了启动服务的函数。接下来我们继续在此基础上进行修修补补。