25从零开始用Rust编写nginx,序列化之serde是如何工作的

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
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的自定义导出:
RUST 复制代码
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);
}

以下输出:

BASH 复制代码
$ 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,可以极好的精简配置结构
RUST 复制代码
#[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,将在反序列化的时候会尝试调用

RUST 复制代码
/// 我们将根据该字符串的值能否解析成目标类型,如果失败返回错误
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
    E: de::Error,
{
}

比如value值为数值,且反序列反时选择deserialize_i64,将在反序列化的时候会尝试调用

RUST 复制代码
/// 我们将根据该数值的值能否解析成目标类型,如果失败返回错误
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做反序列化,但是此时我们又需要同时支持数字的处理,首先我们先定义模块

RUST 复制代码
pub struct DisplayFromStrOrNumber;

此时该模块需要实现序列化及反序列化。

实现序列化,将用标准的Display做输出:

rust 复制代码
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做反序列化:

rust 复制代码
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均不是我们定义的,如果我们可以重新实现,那么此有可能其它第三方库也实现了,那么我们在引用的时候可能就有多种实现方法,从而无法确定调用函数。

那么此时我们做一层包裹方法

RUST 复制代码
pub struct ConfigDuration(pub Duration);

此时我们只需要重新实现DisplayFromStr就可以了

RUST 复制代码
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))
            }
        }
    }
}

这样子我们在加上声名即可以实现自定义的序列化过程了:

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

结语

序列化不管在配置还是在传输等过程中,都是必不可少的存在,了解序列化及反序列化的过程我们将可以更快的找到切入点去实现自己的功能。

点击 [关注][在看][点赞] 是对作者最大的支持

相关推荐
skaiuijing几秒前
Sparrow系列拓展篇:消息队列和互斥锁等IPC机制的设计
c语言·开发语言·算法·操作系统·arm
雯0609~1 小时前
c#:winform调用bartender实现打印(学习整理笔记)
开发语言·c#
胜天半子_王二_王半仙3 小时前
c++源码阅读__smart_ptr__正文阅读
开发语言·c++·开源
沐泽Mu3 小时前
嵌入式学习-C嘎嘎-Day08
开发语言·c++·算法
Non importa3 小时前
汉诺塔(hanio)--C语言函数递归
c语言·开发语言·算法·学习方法
LinuxST3 小时前
27、基于Firefly-rk3399中断休眠唤醒实验(按键中断)
linux·开发语言·stm32·嵌入式硬件
Tony_long74833 小时前
Python学习——猜拳小游戏
开发语言·python·学习
跳动的梦想家h3 小时前
黑马点评 秒杀下单出现的问题:服务器异常---java.lang.NullPointerException: null(已解决)
java·开发语言·redis
苹果醋33 小时前
前端面试之九阴真经
java·运维·spring boot·mysql·nginx
ac-er88883 小时前
PHP 二分法查找算法
开发语言·算法·php