序列化与反序列化¶
在将内存的数据发送到网络中前,需要进行序列化操作;而在接收到网络数据后,需要进行反序列化操作。数据在内存中也是以字节形式表现,发送到网络也是一串字节,为什么还要序列化和反序列化呢?例如有这么一个结构体:
struct MyData {
int id;
std::unique_ptr<Object> pointer_to_some_object;
std::vector<int> numbers;
};
其中,id
是 int
类型,但是在内存中,它有特定的字节序,可能和对方的字节序是相反的。而像 pointer_to_some_object
是一个指向别的对象的指针,我们直接把指针发送出去肯定是不行的。而 std::vector
则更复杂,它的大小是不固定的,而且里面也有指向存储的指针,又该怎么发送呢?
为了解决机器间的差异以及指针问题,需要指定序列化协议,确保收发双方都能理解网络上传输的字节数据。序列化协议一般可以分为两类:纯文本和二进制。
纯文本¶
纯文本协议很好理解,就是将对象转换成一段文本,例如,一个整数,我们就将它转换成十进制表示 123
,如果是数组,我们就用某种符号标识开始和结束,至于对象,我们就递归地进行序列化。为了对应上对象的字段,我们可以把字段名称也编码成字符串,和具体的值一一对应。
当然,我们不需要自己设计协议,可以使用现有的通用协议。例如 JSON、XML 等。
纯文本协议方便人类调试,我们只需要文本编辑器就能编写一个对象,在网络抓包时,也不需要其他工具就能理解数据的内容。但是纯文本协议对于计算机来说解析起来比较复杂,耗费计算资源。它的编码效率也不高,浪费带宽,而且结构往往不确定,对于静态类型语言不太友好。
二进制¶
二进制协议则类似于把内存中的数据直接发送出去,不过发送前统一处理好兼容性问题。例如,整数来说,就先统一转换成大端序,对于数组,同样以某种字节来标识开始和结束,而对象则进行递归的序列化。
二进制协议一般用于对性能有比较高要求的场合,一般会使用单独的语言来描述一个二进制对象的结构,然后使用工具生成代码。计算机解析二进制协议比较方便,大多数时候仅需要做简单的转换即可。但是对应的缺点就是人类想要调试或者编辑的时候必须借助工具,网络抓包的时候也需要解码工具才能解析传输的信息。
当然,二进制协议也不需要我们自己设计,同样有现有的通用协议。例如 Protocol Buffers、Flat Buffers 等。其中 Protocol Buffers 也提供了编码为 JSON 的功能。
总结¶
一般来说,直接选择 Protocol Buffers 作为序列化协议即可,它语言无关,能生成多种语言的代码,节省工作量。也同时支持二进制和 JSON 编码,方便切换。