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

2024-01-29 14:44

本文主要是介绍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
[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>
whereE: de::Error,
{
}

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

/// 我们将根据该数值的值能否解析成目标类型,如果失败返回错误
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>whereE: Error, {
}

或者以上两种格式我们都是支持的,比如时间可以支持数字8或者"8s",此时我们需要同时将数字或者字符串同时支持转成Duration::new(8,0),那么此时我们自定义的反序列化函数可以我选择deserialize_any,并分别实现visit_i64visit_str

举个例子

以下是通过标准的Display做输出及FromStr做反序列化,但是此时我们又需要同时支持数字的处理,首先我们先定义模块

pub struct DisplayFromStrOrNumber;

此时该模块需要实现序列化及反序列化。
实现序列化,将用标准的Display做输出:

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

实现反序列化,我们将数字统一转成字符串,然后用FromStr做反序列化:


impl<'de, T> DeserializeAs<'de, T> for DisplayFromStrOrNumber
whereT: FromStr,T::Err: Display,
{fn deserialize_as<D>(deserializer: D) -> Result<T, D::Error>whereD: Deserializer<'de>,{struct Helper<S>(PhantomData<S>);impl<'de, S> Visitor<'de> for Helper<S>whereS: 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>whereE: de::Error,{value.parse::<Self::Value>().map_err(de::Error::custom)}/// 将数字转成字符串从而能调用FromStr函数fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>whereE: 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>,
}

结语

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

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

这篇关于25从零开始用Rust编写nginx,序列化之serde是如何工作的的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/657256

相关文章

深度解析Nginx日志分析与499状态码问题解决

《深度解析Nginx日志分析与499状态码问题解决》在Web服务器运维和性能优化过程中,Nginx日志是排查问题的重要依据,本文将围绕Nginx日志分析、499状态码的成因、排查方法及解决方案展开讨论... 目录前言1. Nginx日志基础1.1 Nginx日志存放位置1.2 Nginx日志格式2. 499

Olingo分析和实践之EDM 辅助序列化器详解(最佳实践)

《Olingo分析和实践之EDM辅助序列化器详解(最佳实践)》EDM辅助序列化器是ApacheOlingoOData框架中无需完整EDM模型的智能序列化工具,通过运行时类型推断实现灵活数据转换,适用... 目录概念与定义什么是 EDM 辅助序列化器?核心概念设计目标核心特点1. EDM 信息可选2. 智能类

Java中的xxl-job调度器线程池工作机制

《Java中的xxl-job调度器线程池工作机制》xxl-job通过快慢线程池分离短时与长时任务,动态降级超时任务至慢池,结合异步触发和资源隔离机制,提升高频调度的性能与稳定性,支撑高并发场景下的可靠... 目录⚙️ 一、调度器线程池的核心设计 二、线程池的工作流程 三、线程池配置参数与优化 四、总结:线程

Nginx安全防护的多种方法

《Nginx安全防护的多种方法》在生产环境中,需要隐藏Nginx的版本号,以避免泄漏Nginx的版本,使攻击者不能针对特定版本进行攻击,下面就来介绍一下Nginx安全防护的方法,感兴趣的可以了解一下... 目录核心安全配置1.编译安装 Nginx2.隐藏版本号3.限制危险请求方法4.请求限制(CC攻击防御)

nginx中端口无权限的问题解决

《nginx中端口无权限的问题解决》当Nginx日志报错bind()to80failed(13:Permissiondenied)时,这通常是由于权限不足导致Nginx无法绑定到80端口,下面就来... 目录一、问题原因分析二、解决方案1. 以 root 权限运行 Nginx(不推荐)2. 为 Nginx

SpringSecurity整合redission序列化问题小结(最新整理)

《SpringSecurity整合redission序列化问题小结(最新整理)》文章详解SpringSecurity整合Redisson时的序列化问题,指出需排除官方Jackson依赖,通过自定义反序... 目录1. 前言2. Redission配置2.1 RedissonProperties2.2 Red

nginx 负载均衡配置及如何解决重复登录问题

《nginx负载均衡配置及如何解决重复登录问题》文章详解Nginx源码安装与Docker部署,介绍四层/七层代理区别及负载均衡策略,通过ip_hash解决重复登录问题,对nginx负载均衡配置及如何... 目录一:源码安装:1.配置编译参数2.编译3.编译安装 二,四层代理和七层代理区别1.二者混合使用举例

nginx -t、nginx -s stop 和 nginx -s reload 命令的详细解析(结合应用场景)

《nginx-t、nginx-sstop和nginx-sreload命令的详细解析(结合应用场景)》本文解析Nginx的-t、-sstop、-sreload命令,分别用于配置语法检... 以下是关于 nginx -t、nginx -s stop 和 nginx -s reload 命令的详细解析,结合实际应

SpringBoot集成LiteFlow工作流引擎的完整指南

《SpringBoot集成LiteFlow工作流引擎的完整指南》LiteFlow作为一款国产轻量级规则引擎/流程引擎,以其零学习成本、高可扩展性和极致性能成为微服务架构下的理想选择,本文将详细讲解Sp... 目录一、LiteFlow核心优势二、SpringBoot集成实战三、高级特性应用1. 异步并行执行2

前端如何通过nginx访问本地端口

《前端如何通过nginx访问本地端口》:本文主要介绍前端如何通过nginx访问本地端口的问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、nginx安装1、下载(1)下载地址(2)系统选择(3)版本选择2、安装部署(1)解压(2)配置文件修改(3)启动(4)