首先,常见的序列化方法主要有以下几种:
- TLV编码及其变体(tag, length, value): 比如ProtoBuf。
- 文本流编码:XML/JSON
- 固定结构编码:基本原理是,协议约定了传输字段类型和字段含义,和TLV类似,但是没有tag和length,只有value,比如TCP/IP
- 内存dump:基本原理是,把内存中的数据直接输出,不做任何序列化操作。反序列化的时候,直接还原内存。
常见序列化方法
- XML可扩展标记语言(eXtensible Markup Language)。是一种通用和重量级的数据交换格式,以文本方式存储。
- JSON(JavaScript ObjectNotation, JS对象简谱)是一种通用和轻量级的数据交换格式,以文本结构进行存储。
- Protocol Buffer是Google的一种独立的轻量级的数据交换格式,以二进制结构进行存储。
序列化结果比较
XML:
JSON:
Protocol Buffer:
ProtoBuf使用
Protocol buffers 在序列化数据方面,它是灵活的,高效的。相比于 XML 来说,Protocol buffers 更加小巧,更加快速,更加简单。⼀旦定义了要处理的数据的数据结构之后,就可以利用 Protocol buffers 的代码生成工具生成相关的代码。甚至可以在无需重新部署程序的情况下更新数据结构。只需使用Protobuf 对数据结构进行⼀次描述,即可利用各种不同语言或从各种不同数据流中对你的结构化数据轻松读写。
Protocol buffers 很适合做数据存储或 RPC 数据交换格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。
ProtoBuf option部分选项
- SPEED: 表示生成的代码运行效率高,但是由此生成的代码编译后会占用更多的空间。
- CODE_SIZE: 和SPEED恰恰相反,代码运行效率低,但是生成的代码编译后占用更少的空间,通常用于资源有限的平台,比如Moblie。
- LITE_RUNTIME: 生成的代码运行效率更高,同时生成的代码编译后所占用的空间也很少。这是牺牲protobuf提供的反射功能为代价。因此在C++中链接Protocol Buffer库时进需要链接libprotobuf-lite,而非libprotobuf。
ProtoBuf编码
Variants编码(变长的类型使用)
为什么设计变长编码:普通的 int 数据类型, 无论其值的大小, 所占用的存储空间都是相等的,比如不管是0x12345678 还是0x12都占用4字节,那能否让0x12在表示的时候只占用1个字节呢?
是否可以根据数值的大小来动态地占用存储空间, 使得值比较小的数字占用较少的字节数, 值相对比较大的数字占用较多的字节数, 这即是变长整型编码的基本思想。
采用变长整型编码的数字, 其占用的字节数不是完全⼀致的, Varints 编码使用每个字节的最高有效位作为标志位, 而剩余的 7 位以⼆进制补码的形式来存储数字值本身, 当最高有效位为 1 时, 代表其后还跟有字节, 当最高有效位为 0 时, 代表已经是该数字的最后的⼀个字节。
在 Protobuf 中, 使用的是 Base128
Varints 编码, 在这种方式中, 使用 7 bit (即2的7次方为128)来存储数字, 在 Protobuf 中, Base128 Varints 采用的是小端序, 即数字的低位存放在高地址, 举例来看, 对于数字 1, 我们假设 int 类型占 4 个字节, 以标准的整型存储, 其⼆进制表示应为
00000000 00000000 00000000 00000001
可见,只有最后一个字节存储了有效数值,前三个字节都为0,若采用Variants编码,其二进制形式为
00000001
因为其没有后续字节,因此最高有效位为0,其余的7位以补码形式存放1,再比如数字666:
Zigzag编码(针对负数)
Varints 编码的实质在于去掉数字开头的 0, 因此可缩短数字所占的存储字节数, 在上面的例子中, 只举例说明了正数的 Varints 编码, 但如果数字为负数, 则采用 Varints 编码会恒定占用 10 个字节, 原因在于负数的符号位为 1, 对于负数其从符号位开始的高位均为 1, 在 Protobuf 的具体实现中, 会将此视为⼀个很大的无符号数。
Varints 编码的实质在于设法移除数字开头的 0 比特, 而对于负数, 由于其数字高位都是 1, 因此 Varints 编码在此场景下失效。
Zigzag 编码便是为了解决这个问题, Zigzag 编码的大致思想是首先对负数做⼀次变换, 将其映射为⼀个正数, 变换以后便可以使用Varints 编码进行压缩, 这里关键的⼀点在于变换的算法, 首先算法必须是可逆的, 即可以根据变换后 的值计算出原始值 , 否则就无法解码, 同时要求变换算法要尽可能简单, 以避免影响Protobuf编码、解码的速度。解码方式为:
数据组织
Protobuf 不是完全自描述的信息描述格式, 接收端需要有相应的解码器(即 proto 定义)才可解析数据格式, 序列化后的 Protobuf 数据不携带字段名, 只使用字段编号来标识⼀个字段, 因此更改 proto 的字段名不会影响数据解析(但这显然不是⼀种好的行为), 字段编号会被编码进⼆进制的消息结构中, 因此我们应尽可能地使用小字段编号。
同时,Protobuf 是⼀种紧密的消息结构, 编码后字段之间没有间隔, 每个字段头由两部分组成: 字段编号和 wire type, 字段头可确定数据段的长度, 因此其字段之前无需加入间隔, 也无需引入特定的数据来标记字段末尾, 因此 Protobuf 的编码长度短, 传输效率高。
最后给大家推荐一个LinuxC/C++高级架构系统教程的学习资源与课程,可以帮助你有方向、更细致地学习C/C++后端开发,具体内容请见 https://xxetb.xetslk.com/s/1o04uB