开启成长之旅!这是我参与「日新计划 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;
}

好了,咱们整个结构的原型现已出来了。完成了路由映射表,供给了用户注册静态路由的办法,同时也封装了启动服务的函数。接下来咱们持续在此基础上进行修修补补。