开启成长之旅!这是我参与「日新计划 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=connection
的 service
。 更进一步,其实任何函数都可视为 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
。
MakeServiceFn
的 call()
逻辑是,以新建的衔接(AddrStream)
为参数并回来一个 ServiceFn
。相当于说,MakeServiceFn
的 request=AddrStream
, response=ServiceFn
。
ServiceFn
的 call()
逻辑则是,以 request
为参数,回来 response
。
相同,示例代码中也看到,咱们能够直接对Server
进行 await
。这是由于 Server
完成了 Future
。 Server
的逻辑是,不断调用 accept
接受新衔接,然后经过 MakeServiceFn
为该衔接创建 ServiceFn
,并经过 ServiceFn
处理这个衔接上一切的恳求。
这儿的关键信息是,MakeServiceFn
大局只要一个,ServiceFn
每个衔接创建一个。 如果咱们想要跨 handler
同享信息,或许进行一些处理,就得经过 MakeServiceFn
和 ServiceFn
了。
渐至佳境
下面,就该我隆重登场了。
仔细观察实例中的handler
函数,当你看到uri
和method
以及回来的数据。发现这不正是 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;
}
好了,咱们整个结构的原型现已出来了。完成了路由映射表,供给了用户注册静态路由的办法,同时也封装了启动服务的函数。接下来咱们持续在此基础上进行修修补补。