什麼是服務器中間件?

在這篇文章中,我們將全面瞭解什麼是中間件,該模式的好處,以及如何在 Rust 服務器應用程序中使用中間件。

中間件是什麼?

web 服務器通常爲請求提供響應。通常,選擇的協議是 HTTP。處理程序 (有時稱爲響應回調) 接受請求數據並返回響應。

大多數服務器框架都有一個叫做 “路由” 的系統,它根據各種參數 (通常是 URL 路徑) 來路由請求。在 HTTP 路由中通常是路徑和請求方法 (GET、POST、PUT 等) 的組合。路由的好處是它允許將每個邏輯路徑分開,這使得更容易構建大型系統。

單獨的路徑處理程序是很棒的,但有時你需要將相同的邏輯應用於一組路徑或所有路徑。這就是中間件的用武之地。與單獨路徑處理程序不同,中間件會在註冊到它的每個請求路徑上調用。與處理程序一樣,中間件也是函數。

中間件在很大程度上依賴於實現者怎麼去實現。我們將看到一些具體的例子,但是不同的框架在其中間件實現中選擇了不同的權衡。一些中間件被實現在不可變狀態下工作,並充當請求和響應的轉換器。有些框架將輸入視爲可變的,可以自由地修改請求對象。

中間件作爲一個堆棧

中間件往往是有序的,也就是說,每一層處理請求或響應並將結果其傳遞到下一層,請求或響應按照定義良好的順序通過中間件:

requests
           |
           v
+----- layer_three -----+
| +---- layer_two ----+ |
| | +-- layer_one --+ | |
| | |               | | |    
| | |    handler    | | |
| | |               | | |
| | +-- layer_one --+ | |
| +---- layer_two ----+ |
+----- layer_three -----+
           |
           v
        responses

中間件應用

認證

許多路由可能需要用戶信息,傳入的請求包含通過 cookie 或 http 身份驗證的用戶信息。我們可以將此邏輯抽象到認證中間件中,並將其傳遞給後續處理程序,而不是每個路由處理程序都處理提取用戶信息。

日誌

關於用戶將訪問哪些路徑以及何時訪問的信息非常有用。通過日誌中間件,我們可以記錄和存儲請求信息,以供以後分析。

壓縮和響應優化

中間件還可以修改輸出響應,並通過 gzip 和 brotli 等算法壓縮輸出。另一個用例是圖像大小調整,使用請求的信息識別移動視圖,輸出響應可以返回較小的圖像,而不是巨大的 4k 圖像,最終降低了帶寬。

比較不同服務器框架的中間件實現

Rocket

Rocket 是一個服務器框架,Rocket 的中間件實現被稱爲 fairings (整流罩,是的,Rocket 中有許多與火箭相關的雙關語)。

來自 Rocket 的整流罩文檔:

“Rocket 的整流罩很像其他框架的中間件,但它們有幾個關鍵的區別:整流罩不能直接終止或響應傳入請求。整流罩不能將任意的、非請求數據注入到請求中。整流罩可以阻止應用程序啓動。整流罩可以檢查和修改應用程序的配置。”

要在 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}

使用. attach 方法,嚮應用程序添加整流罩是非常簡單的:

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 的整流罩超越了請求和響應,可以控制應用程序的啓動和關閉:

Axum

與 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

我們可以使用. layer(類似於 Rocket 中的. attach) 在 Axum 應用程序上註冊我們的新層 (中間件)。

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)

還有 ServiceBuilder,這是鏈接層的推薦方法。它們的執行順序與附加的順序相反 (首先運行 layer_one)。

1Router::new()
2    .route("/", get(handler))
3    .layer(
4        ServiceBuilder::new()
5            .layer(layer_three)
6            .layer(layer_two)
7            .layer(layer_one)
8    )

簡單的方法

使用 middleware::from_fn 爲 Axum 編寫中間件,Axum 文檔中的例子:

 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));

使用已有的層,因爲 Axum 是建立在 tower 上,所以有一些很容易導入的中間件可以添加到層中。其中一個是 TraceLayer,它記錄請求的進入和響應:

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

在 tower_http crate 中有許多層可以使用,而不需要你自己編寫。

本文翻譯自:

https://www.shuttle.rs/blog/2022/08/04/middleware

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/ST54ezfGCcBEejzpsgOL4Q