大多數的Web框架都會提供中介軟體(Middleware)機制,使用固定的額外程式來處理每次的HTTP請求或是HTTP回應。Rocket框架對於中介軟體的支援是依靠「整流片」(Fairing)來完成的,我們可以替任意的型別實作fairing::Fairing
特性,使其能作為「掛鉤」(hook)來監聽Rocket框架收到HTTP請求時的事件,或是要送出HTTP回應時的事件。
fairing::Fairing
特性的基本實作方式如下:
use rocket::Rocket;
use rocket::fairing::{Fairing, Info, Kind};
use rocket::request::Request;
use rocket::response::Response;
use rocket::data::Data;
struct MyFairing;
impl Fairing for MyFairing {
fn info(&self) -> Info {
Info {
name: "the name of this fairing",
kind: Kind::Attach | Kind::Launch | Kind::Request | Kind::Response
}
}
fn on_attach(&self, rocket: Rocket) -> Result<Rocket, Rocket> {
println!("on_attach");
Ok(rocket)
}
fn on_launch(&self, _rocket: &Rocket) {
println!("on_launch");
}
fn on_request(&self, _request: &mut Request, _data: &Data) {
println!("on_request");
}
fn on_response(&self, _request: &Request, _response: &mut Response) {
println!("on_response");
}
}
在info
這個由fairing::Fairing
特性提供的方法中,我們必須要回傳一個fairing::Info
結構實體,裡面有現在正在被實作的整流片的名稱,以及它所要監聽的事件,事件會以fairing::Kind
結構實體來表示。fairing::Kind
結構體有實作BitOr
特性,因此可以直接使用|
進行OR運算,來合併多種fairing::Kind
結構實體,以監聽多種不同的事件。
一個整流片可以監聽的事件有以下四種:
- Attach:當這個結構實體被註冊到某個
Rocket
結構實體時。也就是使用Rocket
結構實體的attach
方法時。 - Launch:當
Rocket
正要啟動前。也就是使用Rocket
結構實體的launch
方法時。 - Request:當接收到HTTP請求,在尋找可匹配的路由處理程序前時。
- Response:當路由處理程序回傳HTTP回應後,在其被傳送給客戶端前時。
每種事件在被觸發的時候會去執行fairing::Fairing
特性中對應的方法,在實作fairing::Fairing
特性時,只需要針對有被監聽的事件來實作方法即可,不需要將所有事件的對應方法都實作出來(因為這些方法已有預設的實作)。例如,只想監聽Request和Response事件的話,程式可以改寫如下:
use rocket::Rocket;
use rocket::fairing::{Fairing, Info, Kind};
use rocket::request::Request;
use rocket::response::Response;
use rocket::data::Data;
struct MyFairing;
impl Fairing for MyFairing {
fn info(&self) -> Info {
Info {
name: "the name of this fairing",
kind: Kind::Request | Kind::Response
}
}
fn on_request(&self, _request: &mut Request, _data: &Data) {
println!("on_request");
}
fn on_response(&self, _request: &Request, _response: &mut Response) {
println!("on_response");
}
}
另外,data::Data
這個結構體我們以前並未提到,它其實也是一個資料守衛,可以用來匹配任何格式的HTTP主體,能夠直接讀取HTTP主體中的原始資料。
舉個例子,實作出一個可以分別統計GET和POST請求次數的整流片。如下:
use std::sync::atomic::{Ordering, AtomicUsize};
use std::io::Cursor;
use rocket::fairing::{Fairing, Info, Kind};
use rocket::request::Request;
use rocket::response::Response;
use rocket::data::Data;
use rocket::http::{Method, Status, ContentType};
#[derive(Default)]
struct MethodsCounter {
get: AtomicUsize,
post: AtomicUsize,
others: AtomicUsize,
}
impl Fairing for MethodsCounter {
fn info(&self) -> Info {
Info {
name: "the name of this fairing",
kind: Kind::Request | Kind::Response
}
}
fn on_request(&self, request: &mut Request, _data: &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)
};
}
fn on_response(&self, request: &Request, response: &mut Response) {
if response.status() == Status::NotFound {
if 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: {}\nPost: {}\nOthers: {}", get_count, post_count, others_count);
response.set_status(Status::Ok);
response.set_header(ContentType::Plain);
response.set_sized_body(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
方法,將要註冊的整流片實體當作參數傳進去即可。如下:
...
let rocket = rocket::ignite();
let rocket = rocket.attach(MethodsCounter::default());
...
直接用閉包當作整流片掛鉤
如果覺得實作fairing::Fairing
特性很麻煩的話,也可以直接透過fairing::AdHoc
結構體來包裹某個閉包,將其變成整流片來使用。這個方式適用於簡單的處理,無法如上面那個例子一樣,利用結構體來直接記錄狀態。
例如:
use rocket::fairing::AdHoc;
...
let rocket = rocket::ignite();
let rocket = rocket.attach(AdHoc::on_launch("Launch Printer", |_| {
println!("Rocket is about to launch! Exciting! Here we go...");
}))
.attach(AdHoc::on_request("Always-Get Rewriter", |req, _| {
req.set_method(Method::Get);
}));
...
不過,我們也可以在整流片中透過萬用的request::Request
結構實體來存取Rocket的「應用程式狀態」,例如:
use rocket::fairing::AdHoc;
use rocket::State;
...
let rocket = rocket::ignite();
let rocket = rocket.manage(MethodsCounter::default());
let rocket = rocket.attach(AdHoc::on_request("Request Methods Counter", |req, _| {
let methods_counter = req.guard::<State<MethodsCounter>>().unwrap().inner();
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| {
if res.status() == Status::NotFound {
if req.method() == Method::Get && req.uri().path() == "/counts" {
let methods_counter = req.guard::<State<MethodsCounter>>().unwrap().inner();
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: {}\nPost: {}\nOthers: {}", get_count, post_count, others_count);
res.set_status(Status::Ok);
res.set_header(ContentType::Plain);
res.set_sized_body(Cursor::new(body));
}
}
}));
...
用整流片處理Rocket框架的設定值
在先前的章節中有提到Rocket框架的設定檔可以傳入我們自訂的設定項目,由於Rocket的主旨在於安全的型別化,因此所有從外面傳進來的設定值都必須要在Rocket框架啟動前,經過程式檢查過格式後才可以被拿來使用。而檢查設定值最好的時機點,就是在整流片被觸發Attach事件時啦!
舉例來說,我們有個自訂的設定項目名稱buffer_size
,可以用來設定堆疊空間中緩衝空間的大小。我們希望這個值必須要大於等於256
,且小於等於67108864
。利用剛才介紹的fairing::AdHoc
結構體,可以撰寫出如以下的檢查程式:
use std::convert::TryFrom;
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))
}
}
}
...
use rocket::fairing::AdHoc;
let rocket = rocket.attach(AdHoc::on_attach("BUFFER_SIZE Checker", |rocket| {
let buffer_size = rocket.config().extras.get("buffer_size");
let checked_buffer_size = match buffer_size {
Some(buffer_size) => {
let buffer_size = buffer_size.as_integer().expect("the buffer size needs to be an integer");
BufferSize::try_from(buffer_size).unwrap()
}
None => {
BufferSize::default()
}
};
Ok(rocket.manage(checked_buffer_size))
}));
...
如此一來,檢查過後的buffer_size
就會被儲存成BufferSize
實體,被註冊進Rocket的應用程式狀態中。
總結
Rocket框架提供的「整流片」機制即類似其它Web框架所提供的「中介軟體」機制。由於整流片在監聽HTTP請求和回應事件時,有很大的可能會需要直接操作request::Request
結構實體和response::Response
結構實體,算是比較低階的用法,所以在程式實作上會稍微麻煩了一點,對於程式效能的影響也是需要加以考量的。
在下一個章節中,將會回頭介紹更進階的處理HTTP回應的方式。