基于serde实现一个RESP序列化库
与Redis建立连接
为了方便后续测试,我们首先连接到Redis服务端,并手动输入一些命令。
打开Redis服务器,首先使用netcat
测试连接,并手动输入指令,确认连接。
1 | netcat -v localhost 6379 |
1 | +PONG |
接下来我们使用Rust中的std::net
编写连接到Redis的程序。
1 | fn main() -> Result<(), io::Error>{ |
1 | Connect to 127.0.0.1:6379 |
使用Serde编写自己的序列化库
在Serde的相关文档中介绍了如何使用Serde进行二次开发。
错误类型
首先单独定义Error
枚举类型,定义在序列化,反序列化中可能遇到的错误。根据规范,需要在其中添加Error::Message(String)
枚举以向serde
返回错误消息。
这里我定义了一些可能遇到的错误:
Eof
:读取数据时读取到Eof
Syntax
:广义上的语法错误,当错误匹配不到具体类型时,记为该错误ExpectedSign
:首字节没有读取到+
、-
等符号。ExpectedBulkString
:$<SIZE>\r\n
消息之后没有跟上字符串内容。ExpectedArrayElement
:*<SIZE>\r\n
消息之后遗失了数组元素。UnexpectedCR
:当读取简单字符串时其中包含了CR
。UnexpectedType
:类型不匹配。IntegerOverflow
:数字大小超过64位整数范围。BulkStringOverflow
:大容量字符串大小超过512MB。WrongSizeOfBulkString
:$<SIZE>\r\n
后的大容量字符串长度与<SIZE>
不匹配。
RESP类型
我们定义RESP类型作为使用该库的主体,它是一个包含RESP五种类型的枚举类
1 | pub enum RESPType { |
在BulkString
和Array
枚举中都添加了Some
结构,这是由于RESP允许空值,必须要把空值和""
以及[]
区分开来。我们希望完成编写后,可以实现这样的功能:
1 | let arr = vec![ |
这样的语法看起来有些冗长,我们可以考虑在以后的更新中使用宏编写简洁的代码。在目前我们以实现功能为主。
序列化
我们需要做两件事:编写序列化器,为RESP类型实现序列化。
编写Serializer
首先定义Serializer
并为其实现Serialize
的trait
。
1 | pub struct Serializer<W: Write> { |
考虑到这个协议大多用于通信,我们围绕W: impl Write
实现我们的功能。注意到序列化器中有一个itoa::Buffer
,它是itoa
库中的内容,用于实现快速的数字转字符串。
Serde为自行编写序列化器提供了相当便利的方法,我们只需要实现Serialize
的Trait
就行。这个Trait
中包含了各种方法,每个方法序列化一种类型的值。
Serde库将所有Rust中的值划分为一些数据模型,具体内容可以从官方文档中查看到。RESP并不是一种通用数据格式,它只能用于一些特定值的序列化,因此我们不需要实现诸如tuple
struct
等数据类型的序列化,只需要针对每种RESP类型实现一个方法即可。调用其他方法直接返回Error::UnexpectedType
,以期望使用库的用户不要在不支持的类型上进行序列化。
具体来说,我们实现以下方法:
serialize_ix
:序列化整数serialize_str
:序列化简单字符串和错误serialize_none
:序列化空值serialize_bytes
:序列化大容量字符串serialize_seq
:序列化数组
值得注意的是,虽然字符串和错误类型的格式不同,但是Serde对此没有细粒度的区分,只有一种字符串序列化的方法,因此我们需要在为RESP类型实现序列化时再做区分。
对于数组类型,还需实现SerializeSeq
。序列化数组分为三部:建立首部,逐个序列化元素,建立尾部。在Serialize
中实现了建立首部,在SerializeSeq
中实现逐个序列化和建立尾部。RESP的尾部没有任何标记,直接返回即可。对于每个元素,只需要调用各自的serialize
方法。
为RESPType实现Serialize
这一部分较为简单。前四个枚举值基本只需调用serialize
方法即可,对于Array
,Serde有固定格式的序列化语句:
1 | let mut serializer = serializer.serialize_seq(Some(arr.len()))?; |
注意到serialize_seq
就是我们之前编写的,支持链式调用的方法,它为Array
添加了首部之后返回自己。
反序列化
类似的,反序列化的目标是实现一个from_str
函数,从这个函数返回RESPType
。通过实现自己的Deserializer
、SeqAccess
和Visitor
来实现这个函数。
我们的Deserializer定义为:
1 | pub struct Deserializer<'de> { |
它的内部是一个生命周期为de
的字符串切片与offset
,表示正在处理的内容的偏移量,在异常时使用。基本的思路为:为Deserializer
实现Deserialize
的Trait
并实现单独序列化一个RESPType
的方法。为了反序列化RESPArray
,我们需要使用Visitor
与SeqAccess
,后者提供了迭代RESPArray
的方法,前者执行将Serialize
的返回值转化为RESPType
类型的过程。
具体的算法在Deserializer<'de>
的方法中,依据RESP协议,我们从输入中获取数据,并解析为对应的&str
i64
&[u8]
等值。随后我们使用基本方法实现Deserialize
的相关方法。
Deserialize
的方法依赖于Visitor
,以deserialize_bytes
为例:
1 | fn deserialize_bytes<V>(self, visitor: V) -> Result<V::Value> |
数据处理过程为:Deserializer
处理为基本顺序→Visitor
包装为RESPType
数据。
为了遍历数组,定义RESPArrayAccess
类,该类包含一个Deserializer
和remain_cnt
,用于表示剩余的元素数量。
1 | struct RESPArrayAccess<'a, 'de: 'a> { |
我们实现SeqAccess
的方法,并在Visitor
中的visit_seq
中使用它:
1 | fn visit_seq<A>(self, mut seq: A) -> std::result::Result<Self::Value, A::Error> |
由于RESP协议是自解释的,所以我们可以为Deserializer
实现deserialize_any()
:
1 | fn deserialize_any<V>(self, visitor: V) -> Result<V::Value> |
最后我们封装功能到from_str
中,它会使用Deserializer
进行解析。
1 | pub fn from_str<T>(s: & str) -> Result<T> |
注意到泛型T为实现了DeserializeOwned
的类型而非Deserialize<'a>
,这是因为后者序列化为可能含有借用的类型,例如我们有一段数据,想要序列化为&str
,需要保证&str
的生命周期和数据的生命周期一致。但是我们使用的RESPType
全部都是独占数据,不需要考虑这个问题。类似地实现from_reader
。
最后实现的项目开源在Github上,还有许多需要改进的内容(如实现宏)。仓库地址