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

结语

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

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

25. 干货系列从零用Rust编写正反向代理,序列化之serde是如何工作的的更多相关文章

  1. (转)Spring Boot干货系列:(七)默认日志logback配置解析

    转:http://tengj.top/2017/04/05/springboot7/ 前言 今天来介绍下Spring Boot如何配置日志logback,我刚学习的时候,是带着下面几个问题来查资料的, ...

  2. (转)Spring Boot干货系列:(四)开发Web应用之Thymeleaf篇

    转:http://tengj.top/2017/03/13/springboot4/ 前言 Web开发是我们平时开发中至关重要的,这里就来介绍一下Spring Boot对Web开发的支持. 正文 Sp ...

  3. bloom-server 基于 rust 编写的 rest api cache 中间件

    bloom-server 基于 rust 编写的 rest api cache 中间件,他位于lb 与api worker 之间,使用redis 作为缓存内容存储, 我们需要做的就是配置proxy,同 ...

  4. 【转】Spring Boot干货系列:(一)优雅的入门篇

    转自Spring Boot干货系列:(一)优雅的入门篇 前言 Spring一直是很火的一个开源框架,在过去的一段时间里,Spring Boot在社区中热度一直很高,所以决定花时间来了解和学习,为自己做 ...

  5. Spring Boot干货系列:(八)数据存储篇-SQL关系型数据库之JdbcTemplate的使用

    Spring Boot干货系列:(八)数据存储篇-SQL关系型数据库之JdbcTemplate的使用 原创 2017-04-13 嘟嘟MD 嘟爷java超神学堂 前言 前面几章介绍了一些基础,但都是静 ...

  6. Spring Boot干货系列:(七)默认日志框架配置

    Spring Boot干货系列:(七)默认日志框架配置 原创 2017-04-05 嘟嘟MD 嘟爷java超神学堂 前言 今天来介绍下Spring Boot如何配置日志logback,我刚学习的时候, ...

  7. Spring Boot干货系列:(五)开发Web应用JSP篇

    Spring Boot干货系列:(五)开发Web应用JSP篇 原创 2017-04-05 嘟嘟MD 嘟爷java超神学堂 前言 上一篇介绍了Spring Boot中使用Thymeleaf模板引擎,今天 ...

  8. Spring Boot干货系列:(四)Thymeleaf篇

    Spring Boot干货系列:(四)Thymeleaf篇 原创 2017-04-05 嘟嘟MD 嘟爷java超神学堂 前言 Web开发是我们平时开发中至关重要的,这里就来介绍一下Spring Boo ...

  9. Spring Boot干货系列:(一)优雅的入门篇

    Spring Boot干货系列:(一)优雅的入门篇 2017-02-26 嘟嘟MD 嘟爷java超神学堂   前言 Spring一直是很火的一个开源框架,在过去的一段时间里,Spring Boot在社 ...

  10. Java多线程干货系列—(四)volatile关键字

    原文地址:http://tengj.top/2016/05/06/threadvolatile4/ <h1 id="前言"><a href="#前言&q ...

随机推荐

  1. error: Microsoft Visual C++ 14.0 or greater is required. Get it with "Microsoft C++ Build Tools": https://visualstudio.microsoft.com/visual-cpp-build-tools/

    解决办法: python3 是用 VC++ 14 编译的, python27 是 VC++ 9 编译的, 安装 python3 的包需要编译的也是要 VC++ 14 以上支持的.可以下载安装这个:vi ...

  2. python教程 入门学习笔记 第1天 初识python python语言环境安装 python编写器

    初识python 一.python语言简介: 1.起源:1989年由荷兰的前谷歌程序员吉多.范罗苏姆(龟叔)创造,python的命名来源于英国电视喜剧Monty Python's Flying Cir ...

  3. Notepad正则表达式用法

    https://blog.csdn.net/cuckoo1/article/details/52165449

  4. UiAutomator2.0(转)

    1.     概述 UI测试(功能测试.黑盒测试)不需要测试者了解应用程序的内部实现细节,只需要知道当执行了某些特定的动作后是否会得到其预期的输出.这种测试方法,在团队合作中可以更好地分离的开发和测试 ...

  5. 文件上传的multipart/form-data属性,你理解了吗

    form表单经常用于前端发送请求,比如:用户填写信息.选择数据.上传文件,对于不同的场景,上传数据的格式也会有些区别. action action 表示该请求的 url 地址,定义在form上,请求的 ...

  6. 聊聊 RocketMQ 名字服务

    NameServer 是专为 RocketMQ 设计的轻量级名字服务,它的源码非常精简,八个类 ,少于1000行代码. 这篇文章, 笔者会从基础概念.Broker发送心跳包.NameServer 维护 ...

  7. springboot jar thin

    springboot jar thin springboot 应用 jar 瘦身.springboot jar 太大.jar与依赖包分离. 两种方法,第一种,spring-boot-thin-laun ...

  8. 知识图谱(Knowledge Graph)- Neo4j 5.10.0 使用 - Python 操作

    数据基于: 知识图谱(Knowledge Graph)- Neo4j 5.10.0 使用 - CQL - 太极拳传承谱系表 这是一个非常简单的web应用程序,它使用我们的Movie图形数据集来提供列表 ...

  9. xlsx和path的运用

    从后端获取Excel模板 app.get('/api/download-template', (req, res) => { const templatePath = path.join(__d ...

  10. git命令和遇到的问题

    命令 1.快速关联/修改Git远程仓库地址 (1).删除本地仓库当前关联的无效远程地址,再为本地仓库添加新的远程仓库地址 git remote -v //查看git对应的远程仓库地址 git remo ...