什麼是服務器中間件?
在這篇文章中,我們將全面瞭解什麼是中間件,該模式的好處,以及如何在 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 的整流罩超越了請求和響應,可以控制應用程序的啓動和關閉:
-
on_ignite,在啓動服務器之前運行。能夠驗證配置值,設置初始狀態。
-
on_liftoff,在服務器啓動之後運行,用於啓動與的 Rocket 應用程序相關的服務。
-
on_shutdown,此鉤子函數可用於在應用程序關閉前關閉服務並保存狀態,不返回任何請求。
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