前言
有时候我需要写一些简单的网络协议,需要序列化对象为二进制形式以便在网络传输。有时候我不会选择 JSON 这样的方式,而是就地弄一个简单的协议。
比如一个 Peer
对象:
pub struct Peer {
email: String,
nat_type: crate::NatType,
pub_addr: SocketAddr,
}
它表示当前网络节点的一些信息,我们可以选择序列化成 JSON 格式,或者是我选择一种更简单粗暴的方式:一个字段一行:
<email>\n<nat_type>\n<pub_addr>
当我选择一个新的协议的的时候就需要自己去实现它了。
我起初的想法很简单,直接转成 Bytes
格式完事。起初我不借助任何 crate,直接为它定义一个新的方法来编码成 bytes 和从 bytes 解码成对象。但我很快就发现我写了一堆我看不懂的东西!
问题在哪?
越是底层的东西就越需要一种规范来限制代码的写法。上面的对象 Peer
看起来只有三个成员,但当我实现编解码时候我需要为每个成员都实现它们各自的编解码,如果成员有自己的成员也同样需要实现它们的编解码。
很合理的诉求,但当我开始写了之后发现很容易就写的很随意——命名随意,实现随意,方法随意,嵌套随意,调用随意...
比如我可能定义一个编码的方法叫 to_bytes
,再定义一个解码的方法的叫 from_bytes
,每个成员都有。但是,没有任何方式来限制我的命名,我可能一时烦躁就不叫 to_bytes
,from_bytes
,叫 message_2_bytes
什么的都有可能。
这样一来就调用就很麻烦,这也是所谓的抽象不足。
解决
定义一个 trait,能够解决这个抽象不足的问题。
简单来说可以定义一个 Encoder
和 Decoder
的 trait,让 Peer 和所有成员实现,使用的时候直接调用 trait 的方法 encode
,decode
。
bytecodec
crate 可以帮我们做这件事,而且它提供了更全面的功能。
bytecodec
bytecodec 是一个用于实现基于字节的编解码协议的框架,它提供了一些方便的 trait 和扩展来帮助开发者实现它们的字节协议。我们借助这个 crate 来实现 Peer 的编解码。
就如上面提到过的,可以定义一个 Encoder
和 Decoder
的 trait,bytecodec
里已经有了定义好的 trait:Encode
和 Decode
。当实现了这两个 trait 后,bytecodec
会提供一些扩展方法来方便的调用编码解码。
以 Encode
为例子,它的定义如下:
pub trait Encode {
type Item;
fn encode(&mut self, buf: &mut [u8], eos: Eos) -> Result<usize>;
fn start_encoding(&mut self, item: Self::Item) -> Result<()>;
fn requiring_bytes(&self) -> ByteCount;
fn is_idle(&self) -> bool { ... }
}
- item:关联类型,表示你要编码成 bytes 的类型
- encode:方法,编码 item 成 bytes 并写进 buf,如果成功则返回编码进 buf 的字节数
- start_encoding:方法,接收一个要编码的对象,当进行编码的时候,就是对这个对象编码
- requiring_bytes:方法,表示编码这个对象需要多少字节
- is_idle:Provided 方法,表示是否已经完成了所有编码,一般不需要实现
看起来要实现的方法有点迷惑,难道我实现了后,使用起来要调用 start_encode、encode?不是,我们永远不会在使用的时候手动调用上面的方法,当实现了这个 trait 后就可以使用 EncodeExt
提供的扩展方法。
实现 Peer 的 Encode/Decode
首先为我们要编码的的 Peer 构造一个编码器:PeerEncoder,它将用于编码 Peer 为我们需要的字节格式。当它实现了 Encoder
后,我们就可以借助扩展方法这样使用:
let mut encoder = PeerEncoding::default();
let bytes = encoder
.encode_into_bytes(Peer {
email: EXPECTED_EMAIL.to_string(),
nat_type: EXPECTED_NAT_TYPE,
pub_addr: EXPECTED_PUB_ADDR.parse().unwrap(),
})
.unwrap();
encode_into_bytes
是提供的扩展方法。
Peer 的成员 email
是 String
类型,在 Rust 里我们不能为第三发类型实现第三方的 trait,虽然我们可以包装成自己的类型再去实现,但 bytecodec 里也提供了一些已经实现好的 Encoder。
如果你想对 String
编码的话可以直接使用 bytecodec 的 Utf8Encoder
。此外,bytecodec 提供了其他基本的类型的 Encoder 实现,像:I8Encoder
,U8Encoder
等。
如果像 Peer 的成员 nat_type
,bytecodec 里没有合适的 Encoder 实现提供,就需要我们实现。假设类型是 NatType
,那么它也就是需要实现 Encoder
。像另一个成员 pub_addr
也是。
这里,我定义了 NatTypeEncoder
和 PubAddrEncoder
:
pub struct NatTypeEncoder(Utf8Encoder);
impl Encode for NatTypeEncoder {
/* */
}
pub struct PubAddrEncoder(Utf8Encoder);
impl Encode for PubAddrEncoder {
/* */
}
PeerEncoder 的定义如下:
pub struct PeerEncoding {
email: Utf8Encoder, /* 用于编码 Peer 的 email 成员 */
nat_type: NatTypeEncoder, /* 用于编码 Peer 的 nat_type 成员 */
pub_addr: PubAddrEncoder, /* 用于编码 Peer 的 pub_addr 成员 */
}
那么,实现 PeerEncoding 的 start_encodeing
,就是先调用 email
的 start_encodeing
,再调用 nat_type
的 start_encodeing
,再调用 pub_addr
的 start_encodeing
。
fn start_encoding(&mut self, item: Self::Item) -> bytecodec::Result<()> {
track!(self.email.start_encoding(item.email))?;
track!(self.nat_type.start_encoding(item.nat_type))?;
track!(self.pub_addr.start_encoding(item.pub_addr))?;
Ok(())
}
实现 PeerEncoding 的 encode
,就是先调用 email
的 encode
,再调用 nat_type
的 encode
,再调用 pub_addr
的 encode
。
fn encode(&mut self, buf: &mut [u8], eos: bytecodec::Eos) -> bytecodec::Result<usize> {
let mut offset = 0usize;
bytecodec_try_encode!(self.email, offset, buf, eos);
put_newline!(buf, offset);
bytecodec_try_encode!(self.nat_type, offset, buf, eos);
put_newline!(buf, offset);
bytecodec_try_encode!(self.pub_addr, offset, buf, eos);
Ok(offset)
}
其他需要实现的方法也是这个道理。
注意这里需要我们实现 is_idle
,因为只有当所有成员都编码完成后,PeerEncoding 才算编码完成。
fn is_idle(&self) -> bool {
self.email.is_idle() && self.nat_type.is_idle() && self.pub_addr.is_idle()
}
实现 Peer 的 Decode
也是同样的流程。
这篇博客介绍了使用Rust编写网络协议时的编解码问题,并提出了使用bytecodec库来解决这个问题的方法。博客首先介绍了作者在编写网络协议时的需求,即需要将对象序列化为二进制形式以便在网络传输中使用。作者指出,虽然可以选择使用JSON等方式来序列化对象,但有时候也可以选择更简单的方式,例如将每个字段单独一行的形式。
然后,作者提出了在编写编解码逻辑时遇到的问题。作者发现,尽管对象的成员很少,但在实现编解码时需要为每个成员都实现编解码方法,而且如果成员有自己的成员,还需要为它们实现编解码方法。这导致编码和解码的实现变得随意而混乱,命名、实现方式、方法调用等都很随意,缺乏统一的规范和抽象。
为了解决这个问题,作者提出了使用trait来限制编解码的实现。作者介绍了bytecodec库,该库提供了方便的trait和扩展来帮助开发者实现字节协议的编解码。作者通过实现bytecodec库中的Encode和Decode trait,可以使得编码和解码的调用更加方便和统一。
在实现Peer对象的编解码时,作者使用了bytecodec库提供的扩展方法来进行编码和解码。对于Peer对象的成员email,作者使用了bytecodec库中已经实现好的Utf8Encoder来进行编码。对于其他成员,作者自己实现了相应的编码器,然后在PeerEncoder中调用这些编码器的方法进行编码。
整体来说,这篇博客详细介绍了使用bytecodec库来解决编解码问题的方法,同时也给出了具体的实现示例。博客的闪光点在于作者清晰地描述了问题的出现和解决过程,同时也给出了具体的代码示例,使得读者可以更好地理解和实践。改进空间在于博客可以进一步扩展,例如可以介绍更多bytecodec库的功能和用法,或者提供更多实际应用的示例,以帮助读者更好地理解和应用该库。