大多數的Web框架都會提供中介軟體(Middleware)機制,使用固定的額外程式來處理每次的HTTP請求或是HTTP回應。Rocket框架對於中介軟體的支援是依靠「整流片」(Fairing)來完成的,我們可以替任意的型別實作fairing::Fairing
特性,使其能作為「掛鉤」(hook)來監聽Rocket框架收到HTTP請求時的事件,或是要送出HTTP回應時的事件。
fairing::Fairing
特性的基本實作方式如下:
use rocket::{
data::Data,
fairing::{self, Fairing, Info, Kind},
request::Request,
response::Response,
Build, Orbit, Rocket,
};
struct MyFairing;
#[rocket::async_trait]
impl Fairing for MyFairing {
fn info(&self) -> Info {
Info {
name: "the name of this fairing",
kind: Kind::Ignite | Kind::Liftoff | Kind::Request | Kind::Response | Kind::Shutdown,
}
}
async fn on_ignite(&self, rocket: Rocket<Build>) -> fairing::Result {
info!("on_ignite");
Ok(rocket)
}
async fn on_liftoff(&self, rocket: &Rocket<Orbit>) {
info!("on_liftoff");
}
async fn on_request(&self, request: &mut Request<'_>, data: &mut Data<'_>) {
info!("on_request");
}
async fn on_response<'r>(&self, request: &'r Request<'_>, response: &mut Response<'r>) {
info!("on_response");
}
async fn on_shutdown(&self, rocket: &Rocket<Orbit>) {
info!("on_shutdown");
}
}
在info
這個由fairing::Fairing
特性提供的方法中,我們必須要回傳一個fairing::Info
結構實體,裡面有現在正在被實作的整流片的名稱,以及它所要監聽的事件,事件會以fairing::Kind
結構實體來表示。fairing::Kind
結構體有實作BitOr
特性,因此可以直接使用|
進行OR運算,來合併多種fairing::Kind
結構實體,以監聽多種不同的事件。
一個整流片可以監聽的事件有以下五種:
- Ignite:當
Rocket
正要啟動前。也就是使用Rocket
結構實體的ignite
或是launch
方法時。 - Liftoff:當
Rocket
啟動後。也就是使用Rocket
結構實體的launch
方法後。 - Request:當接收到HTTP請求,在尋找可匹配的路由處理程序前時。
- Response:當路由處理程序回傳HTTP回應後,在其被傳送給客戶端前時。
- Shutdown:當
Rocket
正在被優雅的關機時。
每種事件在被觸發的時候會去執行fairing::Fairing
特性中對應的方法,在實作fairing::Fairing
特性時,只需要針對有被監聽的事件來實作方法即可,不需要將所有事件的對應方法都實作出來(因為這些方法已有預設的實作)。例如,只想監聽Request和Response事件的話,程式可以改寫如下:
use rocket::{
data::Data,
fairing::{Fairing, Info, Kind},
request::Request,
response::Response,
};
struct MyFairing;
#[rocket::async_trait]
impl Fairing for MyFairing {
fn info(&self) -> Info {
Info {
name: "the name of this fairing", kind: Kind::Request | Kind::Response
}
}
async fn on_request(&self, request: &mut Request<'_>, data: &mut Data<'_>) {
info!("on_request");
}
async fn on_response<'r>(&self, request: &'r Request<'_>, response: &mut Response<'r>) {
info!("on_response");
}
}
另外,data::Data
這個結構體我們以前並未提到,它其實也是一個資料守衛,可以用來匹配任何格式的HTTP主體,能夠直接讀取HTTP主體中的原始資料。
舉個例子,實作出一個可以分別統計GET和POST請求次數的整流片。如下:
use std::{
io::Cursor,
sync::atomic::{AtomicUsize, Ordering},
};
use rocket::{
data::Data,
fairing::{Fairing, Info, Kind},
http::{ContentType, Method, Status},
request::Request,
response::Response,
};
#[derive(Default)]
struct MethodsCounter {
get: AtomicUsize,
post: AtomicUsize,
others: AtomicUsize,
}
#[rocket::async_trait]
impl Fairing for MethodsCounter {
fn info(&self) -> Info {
Info {
name: "The Methods Counter", kind: Kind::Request | Kind::Response
}
}
async fn on_request(&self, request: &mut Request<'_>, _data: &mut Data<'_>) {
match request.method() {
Method::Get => self.get.fetch_add(1, Ordering::Relaxed),
Method::Post => self.post.fetch_add(1, Ordering::Relaxed),
_ => self.others.fetch_add(1, Ordering::Relaxed),
};
}
async fn on_response<'r>(&self, request: &'r Request<'_>, response: &mut Response<'r>) {
if response.status() == Status::NotFound
&& request.method() == Method::Get
&& request.uri().path() == "/counts"
{
let get_count = self.get.load(Ordering::Relaxed);
let post_count = self.post.load(Ordering::Relaxed);
let others_count = self.others.load(Ordering::Relaxed);
let body = format!("Get: {get_count}\nPost: {post_count}\nOthers: {others_count}");
response.set_status(Status::Ok);
response.set_header(ContentType::Plain);
response.set_sized_body(body.len(), Cursor::new(body));
}
}
}
以上程式,我們在fairing::Fairing
特性的on_request
方法中,去判斷HTTP請求方法的類型,並且使MethodsCounter
結構實體的對應欄位值加1。on_response
方法中,我們攔截了HTTP狀態碼為404
(NotFound)的所有回應,並判斷其HTTP請求方法是否為GET,且路徑為/counts
。如果是的話,就將回應的HTTP狀態碼修改為200
(Ok),Content-Type
標頭欄位修改為text/plain; charset=utf-8
,主體修改為MethodsCounter
結構實體的所有欄位資訊(也就是當前的請求次數的統計結果)。
若要將MethodsCounter
整流片註冊給Rocket來使用的話,就使用Rocket
結構實體提供的attach
方法,將要註冊的整流片實體當作參數傳進去即可。如下:
rocket::build().attach(MethodsCounter::default())
直接用閉包當作整流片掛鉤
如果覺得實作fairing::Fairing
特性很麻煩的話,也可以直接透過fairing::AdHoc
結構體來包裹某個閉包,將其變成整流片來使用。這個方式適用於簡單的處理,無法如上面那個例子一樣,利用結構體來直接記錄狀態。
例如:
use rocket::{fairing::AdHoc, http::Method};
...
rocket::build()
.attach(AdHoc::on_ignite("Launch Printer", |rocket| {
Box::pin(async move {
info!("Rocket is about to launch! Exciting! Here we go...");
rocket
})
}))
.attach(AdHoc::on_request("Always-Get Rewriter", |req, _| {
Box::pin(async move {
req.set_method(Method::Get);
})
}))
...
不過,我們也可以在整流片中透過萬用的request::Request
結構實體來存取Rocket的「應用程式狀態」,例如:
#[macro_use]
extern crate rocket;
use std::{
io::Cursor,
sync::atomic::{AtomicUsize, Ordering},
};
use rocket::{
fairing::AdHoc,
http::{ContentType, Method, Status},
State,
};
#[derive(Default)]
struct MethodsCounter {
get: AtomicUsize,
post: AtomicUsize,
others: AtomicUsize,
}
#[launch]
fn rocket() -> _ {
rocket::build()
.manage(MethodsCounter::default())
.attach(AdHoc::on_request("Request Methods Counter", |req, _| {
Box::pin(async move {
let methods_counter = req.guard::<&State<MethodsCounter>>().await.unwrap();
match req.method() {
Method::Get => methods_counter.get.fetch_add(1, Ordering::Relaxed),
Method::Post => methods_counter.post.fetch_add(1, Ordering::Relaxed),
_ => methods_counter.others.fetch_add(1, Ordering::Relaxed),
};
})
}))
.attach(AdHoc::on_response("Request Methods Getter", |req, res| {
Box::pin(async move {
if res.status() == Status::NotFound
&& req.method() == Method::Get
&& req.uri().path() == "/counts"
{
let methods_counter = req.guard::<&State<MethodsCounter>>().await.unwrap();
let get_count = methods_counter.get.load(Ordering::Relaxed);
let post_count = methods_counter.post.load(Ordering::Relaxed);
let others_count = methods_counter.others.load(Ordering::Relaxed);
let body =
format!("Get: {get_count}\nPost: {post_count}\nOthers: {others_count}");
res.set_status(Status::Ok);
res.set_header(ContentType::Plain);
res.set_sized_body(body.len(), Cursor::new(body));
}
})
}))
}
用整流片處理Rocket框架的設定值
在先前的章節中有提到Rocket框架的設定檔可以傳入我們自訂的設定項目,由於Rocket的主旨在於安全的型別化,因此所有從外面傳進來的設定值都必須要在Rocket框架啟動前,經過程式檢查過格式後才可以被拿來使用。而檢查設定值最好的時機點,就是在整流片被觸發Attach事件時啦!
舉例來說,我們有個自訂的設定項目名稱buffer_size
,可以用來設定堆疊空間中緩衝空間的大小。我們希望這個值必須要大於等於256
,且小於等於67108864
。利用剛才介紹的fairing::AdHoc
結構體,可以撰寫出如以下的檢查程式:
use std::{convert::TryFrom, str::FromStr};
const DEFAULT_BUFFER_SIZE: usize = 4096;
pub struct BufferSize(usize);
impl BufferSize {
pub fn get_value(&self) -> usize {
self.0
}
}
impl Default for BufferSize {
fn default() -> Self {
BufferSize(DEFAULT_BUFFER_SIZE)
}
}
impl TryFrom<i64> for BufferSize {
type Error = &'static str;
fn try_from(value: i64) -> Result<Self, Self::Error> {
if value < 256 {
Err("the buffer size is smaller than 256")
} else if value > 67108864 {
Err("the buffer size is bigger than 67108864")
} else {
Ok(BufferSize(value as usize))
}
}
}
impl FromStr for BufferSize {
type Err = &'static str;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let value: i64 = value.parse().map_err(|_| "the buffer size needs to be an integer")?;
Self::try_from(value)
}
}
...
use rocket::fairing::AdHoc;
rocket::build().attach(AdHoc::try_on_ignite("BUFFER_SIZE Checker", |rocket| {
Box::pin(async move {
let buffer_size = rocket.figment().find_value("buffer_size");
let checked_buffer_size = match buffer_size {
Ok(buffer_size) => match buffer_size.as_str() {
Some(buffer_size) => match BufferSize::from_str(buffer_size) {
Ok(buffer_size) => buffer_size,
Err(error) => {
error!("{error}");
return Err(rocket);
},
},
None => {
error!("the buffer size needs to be an integer");
return Err(rocket);
},
},
Err(_) => BufferSize::default(),
};
Ok(rocket.manage(checked_buffer_size))
})
}))
...
如此一來,檢查過後的buffer_size
就會被儲存成BufferSize
實體,被註冊進Rocket的應用程式狀態中。注意這邊使用到了rocket
結構實體提供的try_on_ignite
方法,而不是on_ignite
,為的是讓BUFFER_SIZE Checker
整流片有能力在發現問題時中止Rocket應用程式的啟動。
總結
Rocket框架提供的「整流片」機制即類似其它Web框架所提供的「中介軟體」機制。由於整流片在監聽HTTP請求和回應事件時,有很大的可能會需要直接操作request::Request
結構實體和response::Response
結構實體,算是比較低階的用法,所以在程式實作上會稍微麻煩了一點,對於程式效能的影響也是需要加以考量的。
在下一個章節中,將會回頭介紹更進階的處理HTTP回應的方式。