Rocket框架提供了維持狀態(State)的機制,將狀態分為「應用程式狀態」和「請求狀態」兩種。前者可以將任何型別當作狀態來使用,而且可以自由、便利地使這些狀態「載入」到任意路由處理程序中;而後者則是因一個HTTP請求可能會由多個路由處理程序嘗試匹配而誕生出來的快取機制。



應用程式狀態

應用程式狀態的有效範圍是整個Rocket應用程式。使用方式很簡單,只需將想要作為狀態使用的實體(可以是任意型別),全都透過Rocket結構實體的manage方法來註冊給Rocket使用就好了。

例如:

use std::sync::Mutex;

struct Config {
    domain: String,
}

struct HitCount {
    count: usize,
}

let config = Config { domain: "magiclen.org".to_string() };

let hit_count = Mutex::new(HitCount { count: 0 });

rocket::build().rocket.manage(config).manage(hit_count)

對於會在Rocket應用程式運行時被改變的狀態,我們可以用Mutex結構體來包裹它,使其可以在多個執行緒下被安全存取。

當我們需要在路由處理程序中讀取這些狀態時,可以在路由處理程序的函數中,加上型別為&State<T>的參數,T即為我們註冊給Rocket當狀態使用的實體的型別。

例如:

#[macro_use]
extern crate rocket;

use std::sync::Mutex;

use rocket::State;

#[derive(Debug)]
struct Config {
    domain: String,
}

struct HitCount {
    count: usize,
}

#[get("/hit")]
fn hit(config: &State<Config>, hit_count: &State<Mutex<HitCount>>) -> String {
    let s = format!("{:?}", config);

    let mut hit_count = hit_count.lock().unwrap();

    hit_count.count += 1;

    s
}

#[get("/hit-count")]
fn hit_count(hit_count: &State<Mutex<HitCount>>) -> String {
    format!("{}", hit_count.lock().unwrap().count)
}

#[launch]
fn rocket() -> _ {
    let config = Config {
        domain: "magiclen.org".to_string()
    };

    let hit_count = Mutex::new(HitCount {
        count: 0
    });

    rocket::build().manage(config).manage(hit_count).mount("/", routes![hit, hit_count])
}

原子(Atomic)狀態

對於基本資料型別(如i8u32f64bool等),我們可以利用Rust標準函式庫內建的std::sync::atomic模組所提供的原子結構體。這些結構體包含所有基本資料型別的原子(Atomic)版本,例如AtomicI8AtomicU32AtomicF64AtomicBool等。

不過由於Rocket框架是以資料型別來區分應用程式狀態,因此若要使用原子結構體,最好另外自訂一個型別將其包裹起來,以免後續無法再使用相同的原子結構體。

承前一個小節的程式範例,可以用原子結構體改寫如下:

#[macro_use]
extern crate rocket;

use std::sync::atomic::{AtomicU64, Ordering};

use rocket::State;

#[derive(Debug)]
struct Config {
    domain: String,
}

struct HitCount {
    count: AtomicU64,
}

#[get("/hit")]
fn hit(config: &State<Config>, hit_count: &State<HitCount>) -> String {
    let s = format!("{:?}", config);

    hit_count.count.fetch_add(1, Ordering::Relaxed);

    s
}

#[get("/hit-count")]
fn hit_count(hit_count: &State<HitCount>) -> String {
    format!("{}", hit_count.count.load(Ordering::Relaxed))
}

#[launch]
fn rocket() -> _ {
    let config = Config {
        domain: "magiclen.org".to_string()
    };

    let hit_count = HitCount {
        count: AtomicU64::new(0)
    };

    rocket::build().manage(config).manage(hit_count).mount("/", routes![hit, hit_count])
}

在臨界區段(Critical Section)的資源數量未滿4個的情況下,使用原子結構體來代替Mutex結構體可以使程式效能更好!

請求狀態

請求狀態的有效範圍是一個HTTP請求。由於Rocket框架使用請求守衛來進行路由處理程序的匹配,因此為了避免同一個請求守衛在同一個HTTP請求中被匹配多次,我們可以使用請求的快取機制來暫存請求狀態。

例如:

#[macro_use]
extern crate rocket;

use rocket::request::{FromRequest, Outcome, Request};

struct MyGuard<'a> {
    authorization: Option<&'a str>,
}

#[rocket::async_trait]
impl<'r> FromRequest<'r> for MyGuard<'r> {
    type Error = ();

    async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
        let authorization = request.headers().get("authorization").next();

        info!("Create a new MyGuard instance.");

        Outcome::Success(MyGuard {
            authorization,
        })
    }
}

#[get("/auth?<id>", rank = 1)]
fn handler_1(guard: MyGuard, id: i32) -> String {
    format!("ID: {}, Auth: {}", id, guard.authorization.unwrap_or(""))
}

#[get("/auth?<token>", rank = 2)]
fn handler_2(guard: MyGuard, token: String) -> String {
    format!("Token: {}, Auth: {}", token, guard.authorization.unwrap_or(""))
}

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![handler_1, handler_2])
}

有關於info!巨集的用法可以參考這篇文章:

以上程式在對一個HTTP請求尋找可匹配的路由處理程序時,MyGuard這個請求守衛的from_request關聯函數可能會被執行多次,而建立出好幾個MyGuard結構實體。若要解決這個問題,可以用請求狀態將以上程式改寫成:

#[macro_use]
extern crate rocket;

use rocket::request::{FromRequest, Outcome, Request};

struct MyGuard {
    authorization: Option<String>,
}

#[rocket::async_trait]
impl<'r> FromRequest<'r> for &'r MyGuard {
    type Error = ();

    async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
        Outcome::Success(request.local_cache(|| {
            let authorization = request.headers().get("authorization").next();

            info!("Create a new MyGuard instance.");

            MyGuard {
                authorization: authorization.map(|s| s.to_string())
            }
        }))
    }
}

#[get("/auth?<id>", rank = 1)]
fn handler_1(guard: &MyGuard, id: i32) -> String {
    format!("ID: {}, Auth: {}", id, guard.authorization.as_deref().unwrap_or(""))
}

#[get("/auth?<token>", rank = 2)]
fn handler_2(guard: &MyGuard, token: String) -> String {
    format!("Token: {}, Auth: {}", token, guard.authorization.as_deref().unwrap_or(""))
}

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![handler_1, handler_2])
}

雖然這個作法可以使傳入Request結構實體的request方法的閉包不被多次執行,但這也會使得from_request函數回傳的資料型別不能使用Rocket的生命周期參數(因為傳入local_cache方法的閉包必須回傳實體,或是生命周期為'static的參考),各有利弊。

總結

「應用程式狀態」和「請求狀態」分別針對全Rocket應用程式以及單一的HTTP請求提供額外的資料暫存功能。而「應用程式狀態」其實還有更為強大的應用方式,那就是可以用來儲存Rocket應用程式所用到的資料庫的實體!有關於Rocket框架與資料庫的搭配用法,將會在之後的章節做介紹。

下一章:中介軟體(Middleware)與整流片(Fairing)