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

let rocket = rocket::ignite();

let rocket = rocket.manage(config).manage(hit_count);

...

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

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

例如:

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

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

State<T>結構實體提供的inner方法可以讓我們直接取得其所包裹的實體的不可變參考。

原子(Atomic)狀態

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

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

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

use std::sync::atomic::AtomicU64;

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

struct HitCount {
    count: AtomicU64
}
    
fn main() {
let config = Config {
    domain: "magiclen.org".to_string()
};

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

let rocket = rocket::ignite();

let rocket = rocket.manage(config).manage(hit_count);

...
use rocket::State;
use std::sync::atomic::Ordering;
    
#[get("/hit")]
fn hit(config: State<Config>, hit_count: State<HitCount>) -> String {
    let s = format!("{:?}", config.inner());

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

使用原子結構體來代替Mutex結構體可以使程式效能更好!有興趣者可以延伸參考這篇文章:

https://magiclen.org/rust-atomic-mutex-performance-measurement/

請求狀態

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

例如:

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

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

impl<'a, 'r> FromRequest<'a, 'r> for MyGuard<'a> {
    type Error = ();

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

        println!("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(""))
}

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

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

struct MyGuard {
    authorization: Option<String>
}

impl<'a, 'r> FromRequest<'a, 'r> for &'a MyGuard {
    type Error = ();

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

            println!("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_ref().map(|s| s.as_str()).unwrap_or(""))
}

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

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

總結

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

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