25. 乾貨系列從零用Rust編寫正反向代理,序列化之serde是如何工作的

問蒙服務框架發表於2023-11-10

wmproxy

wmproxy已用Rust實現http/https代理, socks5代理, 反向代理, 靜態檔案伺服器,四層TCP/UDP轉發,內網穿透,後續將實現websocket代理等,會將實現過程分享出來,感興趣的可以一起造個輪子

專案地址

國內: https://gitee.com/tickbh/wmproxy

github: https://github.com/tickbh/wmproxy

序列化

  序列化(Serialization)是指將資料結構或物件狀態轉化為可以儲存或傳輸的形式的過程。

  在序列化過程中,物件的成員屬性和型別資訊一起被轉換為一個位元組流或可列印字元流,以便於儲存或網路傳輸。

  這個位元組流或字元流可以再次被反序列化(Deserialization)還原為原始物件狀態。

  字元流比如JSON,位元組流比如ProtoBuf

Rust中的序列化

  在Rust中序列化最常用且支援最廣的為第三方庫serde,當前在github上已有8000顆star

  常用的比如JSON庫的serde_json,比如YAMLTOMLBSON等,依靠serde庫之上,對常用的格式已經有了廣泛的的支援。

  在程式碼中,Serde資料模型的序列化部分由特定義 Serializer,反序列化部分由特徵定義Deserializer。這些是將每個 Rust 資料結構對映到 29 種可能型別之一的方法。特徵的每個方法Serializer對應於資料模型的一種型別。

  支援基礎型別如常用的布林值,整型,浮點型,字串,位元組流

  支援的高階型別,如tuplestructseqenum可以對映成各種內建的資料結構。

如何使用serde

假如用現有的資料格式,如json之類的,可以輕鬆的實現。

  1. 配置Cargo.toml
[package]
name = "wmproxy"
version = "0.1.0"
authors = ["wenmeng <user@wm-proxy.com>"]

[dependencies]
serde = { version = "1.0", features = ["derive"] }

# 這僅僅是測試用例,需要用哪個可以選擇新增
serde_json = "1.0"
  1. 現在src/main.rs使用Serde的自定義匯出:
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let point = Point { x: 1, y: 2 };

    let serialized = serde_json::to_string(&point).unwrap();
    println!("serialized = {}", serialized);

    let deserialized: Point = serde_json::from_str(&serialized).unwrap();
    println!("deserialized = {:?}", deserialized);
}

以下輸出:

$ cargo run
serialized = {"x":1,"y":2}
deserialized = Point { x: 1, y: 2 }

serde中的屬性引數

在使用serde中經常可以看到在欄位前加一些屬性引數,這些是約定該欄位序列化或反序列化時將如何處理的,下面我們看以下的例子:

  • #[serde(default)]
    這是設定預設引數,或者可以帶上#[serde(default="???")],這裡???將是一個函式名,不能帶引數,可以直接訪問,如Vec::new可以直接訪問的函式。
fn default_y() -> i32  {
    1024
}
#[derive(Serialize, Deserialize, Debug)]
struct Point {
    #[serde(default)]
    x: i32,
    #[serde(default="default_y")]
    y: i32,
}

此時我們反序化一個值時,如果沒有x的引數會將x預設設定成0,如果沒有y引數,將會呼叫default_y函式,也就是y會預設為1024。

  • #[serde(rename = "name")]
    重新命名欄位名字,在記憶體中顯示長的名字好理解,在配置中可以用短的名字好配置。此外還有#[serde(rename_all = "...")]可以將所有的名字結構變成全小寫,或者全大寫之類或者駝峰結構等。
  • #[serde(skip)]
    該欄位跳過序列化及反序列化,也就是一些記憶體物件或者臨時資料不適合做序列化,用此來做約束。還有#[serde(skip_serializing)]跳過序列化和#[serde(skip_deserializing)]跳過反序列化等。
  • #[serde(flatten)]
    將不能解析的資料統一挪入到另一個資料結構,在此專案中用到的通用的配置化結構,就將其均挪到了CommonConfig,可以極好的精簡配置結構
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HttpConfig {
    #[serde(default = "Vec::new")]
    pub server: Vec<ServerConfig>,
    #[serde(default = "Vec::new")]
    pub upstream: Vec<UpstreamConfig>,
    #[serde(flatten)]
    #[serde(default = "CommonConfig::new")]
    pub comm: CommonConfig,
}
  • #[serde(with = "module")]
    這個是自定義序列化的關鍵,也是他強大的基礎,可以很好的實現自定義的一些操作,就比如配置一個整型,現在要把他轉成Duration或者原來是一個字串"4k"表示大小,現在需要把他按資料大小轉成數字4096,就需要自定義的序列化過程。
    該聲名同時包含了serialize_withdeserialize_with,該模組需實現$module::serialize$module::deserialize做對應的序列化和反序列化。

serde的工作原理

序列化

以下過程是Rust中的資料結構是如何轉化成目標格式的

Rust (結構體列舉) 
  ↓
  -- Serialize(序列化) --> 當前結構體中,有對欄位進行協議說明的,加屬性標記
  ↓
  -- 資料的格式(如JSON/BSON/YAML等) --> 根據對應的輸出庫(serde_json/serde_yaml)輸出相應的位元組流

反序列化

以下以JSON格式是如何轉化成Rust的結構,在JSON中屬於鍵值對且值有特定的資料格式,其中key將解析成資料結構中的欄位名,值value將根據反序列化可以嘗試解析的型別嘗試是否能轉成目標型別。

比如value值為字串,且反序列反時選擇deserialize_str,將在反序列化的時候會嘗試呼叫

/// 我們將根據該字串的值能否解析成目標型別,如果失敗返回錯誤
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
    E: de::Error,
{
}

比如value值為數值,且反序列反時選擇deserialize_i64,將在反序列化的時候會嘗試呼叫

/// 我們將根據該數值的值能否解析成目標型別,如果失敗返回錯誤
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
    where
        E: Error, {
}

或者以上兩種格式我們都是支援的,比如時間可以支援數字8或者"8s",此時我們需要同時將數字或者字串同時支援轉成Duration::new(8,0),那麼此時我們自定義的反序列化函式可以我選擇deserialize_any,並分別實現visit_i64visit_str

舉個例子

以下是透過標準的Display做輸出及FromStr做反序列化,但是此時我們又需要同時支援數字的處理,首先我們先定義模組

pub struct DisplayFromStrOrNumber;

此時該模組需要實現序列化及反序列化。
實現序列化,將用標準的Display做輸出:

impl<T> SerializeAs<T> for DisplayFromStrOrNumber
where
    T: Display,
{
    fn serialize_as<S>(source: &T, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.collect_str(source)
    }
}

實現反序列化,我們將數字統一轉成字串,然後用FromStr做反序列化:


impl<'de, T> DeserializeAs<'de, T> for DisplayFromStrOrNumber
where
    T: FromStr,
    T::Err: Display,
{
    fn deserialize_as<D>(deserializer: D) -> Result<T, D::Error>
    where
        D: Deserializer<'de>,
    {
        struct Helper<S>(PhantomData<S>);
        impl<'de, S> Visitor<'de> for Helper<S>
        where
            S: FromStr,
            <S as FromStr>::Err: Display,
        {
            type Value = S;

            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
                write!(formatter, "a string")
            }

            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
            where
                E: de::Error,
            {
                value.parse::<Self::Value>().map_err(de::Error::custom)
            }

            /// 將數字轉成字串從而能呼叫FromStr函式
            fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
                where
                    E: Error, {
                format!("{}", v).parse::<Self::Value>().map_err(de::Error::custom)
            }
        }

        deserializer.deserialize_any(Helper(PhantomData))
    }
}

  此時我們已有了標準模組了,我們只能重新實現類的DisplayFromStr,由於現有的型別如Duration我們不能重新實現impl Display for Duration因為介面Display和型別Duration均不是我們定義的,如果我們可以重新實現,那麼此有可能其它第三方庫也實現了,那麼我們在引用的時候可能就有多種實現方法,從而無法確定呼叫函式。

  那麼此時我們做一層包裹方法

pub struct ConfigDuration(pub Duration);

此時我們只需要重新實現DisplayFromStr就可以了


impl FromStr for ConfigDuration {
    type Err=io::Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        if s.len() == 0 {
            return Err(io::Error::new(io::ErrorKind::InvalidInput, ""));
        }

        let d = if s.ends_with("ms") {
            let new = s.trim_end_matches("ms");
            let s = new.parse::<u64>().map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, ""))?;
            Duration::new(0, (s * 1000_000) as u32)
        } else if s.ends_with("h") {
            let new = s.trim_end_matches("h");
            let s = new.parse::<u64>().map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, ""))?;
            Duration::new(s * 3600, 0)
        } else if s.ends_with("min") {
            let new = s.trim_end_matches("min");
            let s = new.parse::<u64>().map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, ""))?;
            Duration::new(s * 60, 0)
        } else if s.ends_with("s") {
            let new = s.trim_end_matches("s");
            let s = new.parse::<u64>().map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, ""))?;
            Duration::new(s, 0)
        } else {
            let s = s.parse::<u64>().map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, ""))?;
            Duration::new(s, 0)
        };

        Ok(ConfigDuration(d))
    }
}


impl Display for ConfigDuration {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let ms = self.0.subsec_millis();
        let s = self.0.as_secs();
        if ms > 0 {
            f.write_str(&format!("{}ms", ms as u64 + s * 1000))
        } else {
            if s >= 3600 && s % 3600 == 0 {
                f.write_str(&format!("{}h", s / 3600))
            } else if s >= 60 && s % 60 == 0 {
                f.write_str(&format!("{}min", s / 60))
            } else {
                f.write_str(&format!("{}s", s))
            }
        }
    }
}

這樣子我們在加上聲名即可以實現自定義的序列化過程了:

pub struct CommonConfig {
    #[serde_as(as = "Option<DisplayFromStrOrNumber>")]
    pub rate_limit_per: Option<ConfigDuration>,
}

結語

序列化不管在配置還是在傳輸等過程中,都是必不可少的存在,瞭解序列化及反序列化的過程我們將可以更快的找到切入點去實現自己的功能。

點選 [關注][在看][點贊] 是對作者最大的支援

相關文章