prtobuf的原理
好处:
在protobuf里定义完Message,自动生成C++的一系列属性字段,处理字段的方法,处理类的方法(序列化/反序列化),后面两个都是吃力不讨好(不算难,但比较麻烦)
它有一个非常棒的特性,即“向后”兼容性好,人们不必破坏已部署的、依靠“老”数据格式的程序就可以对数据结构进行升级。这样可以不必担心因为消息结构的改变而造成的大规模的代码重构或者迁移的问题。因为添加新的消息中的 field 并不会引起已经发布的程序的任何改变。
Protbuf 与 XML 相比也有不足之处。它功能简单,无法用来表示复杂的概念。
XML 已经成为多种行业标准的编写工具,Protobuf 只是 Google 公司内部使用的工具,在通用性上还差很多。
由于文本并不适合用来描述数据结构,所以 Protobuf 也不适合用来对基于文本的标记文档(建模。
另外,由于 XML 具有某种程度上的自解释性,它可以被人直接读取编辑,在这一点上 Protobuf 不行,它以二进制的方式存储,除非你有 .proto 定义,否则你没法直接读出 Protobuf 的任何内容
为什么protobuf序列化后的数据更小?
protobuf信息的表示非常紧凑,这意味着消息的体积减少,自然需要更少的资源。
比如网络上传输的字节数更少,需要的 IO 更少等,从而提高性能。
原因
采用Varint 编码:一个或多个字节来表示一个数字,值越小的数字使用越少的字节数。这能减少用来表示数字的字节数。
Varint 编码
int32 类型的数字,一般需要 4 个 byte 来表示。但是采用 Varint,对于很小的 int32 类型的数字,则可以用 1 个 byte 来表示。当然凡事都有好的也有不好的一面,采用 Varint 表示法,大的数字则需要 5 个 byte 来表示。从统计的角度来说,一般不会所有的消息中的数字都是大数,因此大多数情况下,采用 Varint 后,可以用更少的字节数来表示数字信息
Varint 中的每个bit的最高位 bit 有特殊的含义,如果该位为 1,表示后续的bit 也是该数字的一部分,如果该位为 0,则结束。其他的 7 个 bit 都用来表示数字。因此小于 128 的数字都可以用一个 byte 表示。大于 128 的数字,比如 300,会用两个字节来表示:1010 1100 0000 0010。下图演示了 Google Protocol Buffer 如何解析两个 bytes
这样来看,小于128的剩了三个字节,还是挺多的
消息经过序列化后会成为一个二进制数据流。如下图所示
采用这种 TLV结构无需使用分隔符来分割不同的 Field。
对于可选的 Field,如果消息中不存在该 field,那么在最终的 Message Buffer 中就没有该 field(可拔插的感觉),这些特性都有助于节约消息本身的大小。
ZigZag 编码
负数protobuf采用ZigZag 编码,绝对值小的数字,无论正负都可以采用较少的 byte 来表示,充分利用了 Varint 这种技术
Encoding过程
采用了 TLV(tag-length-value) 编码格式。
- 每个字段都有唯一的 tag 值,它是字段的唯一标识
- length 表示 value 数据的长度,length 不是必须的,对于固定长度的 value,是没有 length 的
- value是采用Varint和Zigzag编码后的消息字段的值(一堆数字)
计算公式为tag=field_number<<3 | wire_type, 然后在对其采用 Varint 编码
file_number为之前讲的每个消息的唯一的消息编号
wire_type则是数据类型,是int还是string
TVL都要采取ZigZag 编码或Varint 编码,如果是val是string就采取utf-8
为什么普通的类型int不用length?
因为 Varint 和 Zigzag 编码可以自解析内容的长度,所以可以省略长度项。
TLV 存储简化为了 TV 存储,不需 length 项
可以自解析内容的长度,也就不用分割符了
怎么就可以自解析长度了?
Varint 中的每个 byte 的最高位 bit 有特殊的含义,如果该位为 1,表示后续的 byte 也是该数字的一部分,如果该位为 0,则结束。
你一个byte一个byte读,10的最高位为0,则下一个byte开始就是数据了
数据从前往后逐个字节读,到最后一个字节的首位为0,数据读取完毕,接下来的就是下一个字段了
最高位的bit有特殊含义+一个bit一个bit读
反序列化的大概过程
- 从文件首字节开始往后一个字节一个字节读,有个字节首位为0的标志读完tag字段
- 如果是有length的就和上面一样,一个字节一个字节读,有个字节首位为0的标志读完length字段
- 没有length的话和上面一样,一个字节一个字节读,有个字节首位为0的标志读完val字段
- 解析放进C++类中的对象的成员变量中