前言

有时候我需要写一些简单的网络协议,需要序列化对象为二进制形式以便在网络传输。有时候我不会选择 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_bytesfrom_bytes,叫 message_2_bytes 什么的都有可能。

这样一来就调用就很麻烦,这也是所谓的抽象不足。

解决

定义一个 trait,能够解决这个抽象不足的问题。

简单来说可以定义一个 EncoderDecoder 的 trait,让 Peer 和所有成员实现,使用的时候直接调用 trait 的方法 encodedecode

bytecodec crate 可以帮我们做这件事,而且它提供了更全面的功能。

bytecodec

bytecodec 是一个用于实现基于字节的编解码协议的框架,它提供了一些方便的 trait 和扩展来帮助开发者实现它们的字节协议。我们借助这个 crate 来实现 Peer 的编解码。

就如上面提到过的,可以定义一个 EncoderDecoder 的 trait,bytecodec 里已经有了定义好的 trait:EncodeDecode。当实现了这两个 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 的成员 emailString 类型,在 Rust 里我们不能为第三发类型实现第三方的 trait,虽然我们可以包装成自己的类型再去实现,但 bytecodec 里也提供了一些已经实现好的 Encoder。

如果你想对 String 编码的话可以直接使用 bytecodec 的 Utf8Encoder。此外,bytecodec 提供了其他基本的类型的 Encoder 实现,像:I8EncoderU8Encoder 等。

如果像 Peer 的成员 nat_type,bytecodec 里没有合适的 Encoder 实现提供,就需要我们实现。假设类型是 NatType,那么它也就是需要实现 Encoder。像另一个成员 pub_addr 也是。

这里,我定义了 NatTypeEncoderPubAddrEncoder

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,就是先调用 emailstart_encodeing,再调用 nat_typestart_encodeing,再调用 pub_addrstart_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,就是先调用 emailencode,再调用 nat_typeencode,再调用 pub_addrencode

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 也是同样的流程。