在这篇文章中,我们将全面了解什么是中间件,该模式的好处,以及如何在Rust服务器应用程序中使用中间件。
大多数服务器框架都有一个叫做“路由”的系统,它根据各种参数(通常是URL路径)来路由请求。在HTTP路由中通常是路径和请求方法(GET、POST、PUT等)的组合。路由的好处是它允许将每个逻辑路径分开,这使得更容易构建大型系统。
单独的路径处理程序是很棒的,但有时你需要将相同的逻辑应用于一组路径或所有路径。这就是中间件的用武之地。与单独路径处理程序不同,中间件会在注册到它的每个请求路径上调用。与处理程序一样,中间件也是函数。
中间件在很大程度上依赖于实现者怎么去实现。我们将看到一些具体的例子,但是不同的框架在其中间件实现中选择了不同的权衡。一些中间件被实现在不可变状态下工作,并充当请求和响应的转换器。有些框架将输入视为可变的,可以自由地修改请求对象。
requests
|
v
+----- layer_three -----+
| +---- layer_two ----+ |
| | +-- layer_one --+ | |
| | | | | |
| | | handler | | |
| | | | | |
| | +-- layer_one --+ | |
| +---- layer_two ----+ |
+----- layer_three -----+
|
v
responses
要在Rocket中制造整流罩,你必须实现整流罩特性:
1struct MyCounterFaring {
2 get_requests: AtomicUsize,
3}
4
5#[rocket::async_trait]
6impl Fairing for MyCounterFaring {
7 fn info(&self) -> Info {
8 Info {
9 name: "GET Counter",
10 kind: Kind::Request
11 }
12 }
13
14 async fn on_request(&self, request: &mut Request<'_>, _: &mut Data<'_>) {
15 if let Method::Get = request.method() {
16 self.get.fetch_add(1, Ordering::Relaxed);
17 }
18 }
19}
1#[launch]
2fn rocket() -> _ {
3 rocket::build()
4 .attach(MyCounterFaring {
5 get_requests: AtomicUsize::new(0),
6 })
7 .attach(other_fairing)
8}
Rocket的整流罩有几个钩子函数。它们每个都有一个默认实现,因此可以省略(不必为每个钩子显式地编写方法)。
请求使用on_request,这将在接收到请求时触发。这个钩子函数对请求有一个可变的引用,因此可以修改请求。
响应使用on_response,与on_request类似,它对响应对象有可变的访问权(它对请求也有不可变的访问权)。使用这个钩子函数,你可以注入报头或者修改部分响应(如404)。
通用服务器钩子,Rocket的整流罩超越了请求和响应,可以控制应用程序的启动和关闭:
on_ignite,在启动服务器之前运行。能够验证配置值,设置初始状态。
on_liftoff,在服务器启动之后运行,用于启动与的Rocket应用程序相关的服务。
on_shutdown,此钩子函数可用于在应用程序关闭前关闭服务并保存状态,不返回任何请求。
与Rocket类似,Axum是一个用于web应用程序的HTTP框架。Axum中间件是基于tower的,它是一个独立的crate,用于处理Rust中较低层次的基础网络。Axum和tower中间件被称为“层”。
这个演示来自于Tower的文档,在你被吓走之前,我们将很快看到一种更简单的实现中间件的方法。
1pub struct LogLayer {
2 target: &'static str,
3}
4
5impl<S> Layer<S> for LogLayer {
6 type Service = LogService<S>;
7
8 fn layer(&self, service: S) -> Self::Service {
9 LogService {
10 target: self.target,
11 service
12 }
13 }
14}
15
16// This service implements the Log behavior
17pub struct LogService<S> {
18 target: &'static str,
19 service: S,
20}
21
22impl<S, Request> Service<Request> for LogService<S>
23where
24 S: Service<Request>,
25 Request: fmt::Debug,
26{
27 type Response = S::Response;
28 type Error = S::Error;
29 type Future = S::Future;
30
31 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
32 self.service.poll_ready(cx)
33 }
34
35 fn call(&mut self, request: Request) -> Self::Future {
36 // Insert log statement here or other functionality
37 println!("request = {:?}, target = {:?}", request, self.target);
38 self.service.call(request)
39 }
40}
41
1use axum::{routing::get, Router};
2
3async fn handler() {}
4
5let app = Router::new()
6 .route("/", get(handler))
7 .layer(LogLayer { target: "our site" })
8 // `.route_layer` will only run the middleware if a route is matched
9 .route_layer(TimeOutLayer)
1Router::new()
2 .route("/", get(handler))
3 .layer(
4 ServiceBuilder::new()
5 .layer(layer_three)
6 .layer(layer_two)
7 .layer(layer_one)
8 )
1async fn auth<B>(req: Request<B>, next: Next<B>) -> Result<Response, StatusCode> {
2 let auth_header = req.headers()
3 .get(http::header::AUTHORIZATION)
4 .and_then(|header| header.to_str().ok());
5
6 match auth_header {
7 Some(auth_header) if token_is_valid(auth_header) => {
8 Ok(next.run(req).await)
9 }
10 _ => Err(StatusCode::UNAUTHORIZED),
11 }
12}
1let app = Router::new()
2 .route("/", get(|| async { /* ... */ }))
3 .route_layer(middleware::from_fn(auth));
DEBUG request{method=GET path="/foo"}: tower_http::trace::on_request: started processing request
DEBUG request{method=GET path="/foo"}: tower_http::trace::on_response: finished processing request latency=1 ms status=200