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. Spring-Bean实例化三种的方式

    Bean实例化三种方式 无参构造实例化(重点) 工厂静态方法实例化 工厂实例方法实例化 工厂静态方法实例化 1.编写接口 package com.my; public interface UserDa ...

  2. Blazor中用浏览器打开一个链接的最好方法

    适用于Blazor Wasm和Blazor SSR 调用下面的js方法 说一下为什么不用window.open,有可能被拦截是小问题,大问题是打开新页面未加载完时,回到原页面,大概率卡死,无法点击任何 ...

  3. .Net Core控制台程序读取Json配置文件

    1 添加依赖(可以直接通过nuget包安装 Microsoft.Extensions.Configuration.Json 这个会依赖其他两个会自动安装依赖的) Microsoft.Extension ...

  4. Django创建数据库时设置字符集

    在控制台输入一下命令: create database 数据库名 charset=utf8;

  5. 代码随想录算法训练营第四天|力扣24.两两交换链表节点、力扣19.删除链表的倒数第N个结点、力扣面试02.07链表相交、力扣142.环形链表

    两两交换链表中的节点(力扣24.) dummyhead .next = head; cur = dummyhead; while(cur.next!=null&&cur.next.ne ...

  6. 一款开源免费、更符合现代用户需求的论坛系统:vanilla

    对于个人建站来说,WordPress相信很多读者都知道了.但WordPress很多时候我们还是用来建立自主发布内容的站点为主,适用于个人博客.企业主站等.虽然有的主题可以把WordPress变为论坛, ...

  7. 免费拥有自己的 Github 资源加速器

    TurboHub 是一个免费的 Github 资源加速下载站点,可以帮助你快速下载 Github 上的资源.其核心逻辑是通过 Azure Static Web Apps 服务和 Azure Funct ...

  8. Elasticsearch 保姆级入门篇

    Elasticsearch 是一个分布式的.面向生产规模工作负载优化的搜索引擎. Kibana 可以将 Elasticsearch 中的数据转化为直观的图表.图形和仪表盘. 这篇文章,您将学习本地安装 ...

  9. 定义一个函数,传入一个字典和一个元组,将字典的值(key不变)和元组的值交换,返回交换后的字典和元组

    知识点:zip() 函数用于将可迭代的对象作为参数,将对象中对应的元素打包成一个个元组,然后返回由这些元组组成的列表. li=[3,4,5] t=(7,8,9) print(list(zip(li,t ...

  10. 万字长文硬核AQS源码分析

    阅读本文前,需要储备的知识点如下,点击链接直接跳转. java线程详解 Java不能操作内存?Unsafe了解一下 一文读懂LockSupport AQS简介 AQS即AbstractQueuedSy ...