使用GPB协议
为什么要使用GPB协议
GPB(Google Protocol Buffer)协议,与常用的数据通讯、存储方式相比,优点如下:
- 跨平台(语言):相比于Java序列化、Python的pickle序列化工具,GPB是独立于语言的,如果项目对这方面有要求,GPB就脱颖而出了。序列化还有一个问题,如果你改了数据结构,很容易造成序列化失败。
- 快:采用类似二进制的协议,更加高效。这一点主要是相比于XML、JSON这两种常用的数据标准而言的。
- 扩展性、维护性:在后续扩展协议的时候,GPB可以很好的兼容;协议源文件跟随代码做版本控制,容易维护。
示例
结合Python的google.protobuf模块做一些深入讨论。
首先新建一个文件person.proto
,假设这个是某接口的通讯协议——要传name和id两个Field(都是必填字段)。
1 | message PERSON { |
有了这个协议的源文件,Python还不能直接使用,需要通过工具编译成 py 文件才能用。不像XML、JSON直接用纯文本文件,它还要多经过一步“编译过程”以适应不同的程序语言(C++、Java、Python)。
1 | protoc -I=. --python_out=./GPB GPB/person.proto |
执行后会发现GPB子目录下多了一个文件person_pb2.py
,这个文件就是 person.proto
经过编译得到的文件,可以直接在Python中引用。下面用几行代码实现写入一个PERSON对象,然后再读出来,中途把GPB字符串的原始内容打印出来(注意两个核心方法SerializeToString
、ParseFromString
都是google.protobuf
模块提供的):
1 | from GPB import person_pb2 |
可以看到输出:
1 | 8 '"\x04shaw(\x0f' |
可以看到GPB只用了8个字节传输。如果是JSON格式传输{"name": "shaw","id": 15}
则要23个字节,更不要提XML了!更进一步,为什么GPB能这么省呢,原因也大概猜的到,它没有传 “name” “id”这些键值,而是用一个数字id代替,解析的时候也是通过这个id去关联键值。具体还是从上面序列化后的字符串s下手,它是一个二进制的字符串,我用一个特殊的语法来表示这8个字节:
1 | 0010 0010, 0000 0100, s, h, a, w, 0010 1000, 0000 1111 |
可以看到,是通过 Key Value Key Value 的形式排列的,Key与message中定义的Field的取值及类型有关。定义如下:
(field_number << 3) | wire_type
wire_type 的取值跟类型有关,可以查表获得:
Type | Meaning | Used For |
---|---|---|
0 | Varint | int32, int64, uint32, uint64, sint32, sint64, bool, enum |
1 | 64-bit | fixed64, sfixed64, double |
2 | Length-delimited | string, bytes, embedded messages, packed repeated fields |
5 | 32-bit | fixed32, sfixed32, float |
有了这个公式就知道GPB传输的Key是怎么算出来的了:
第一个字节: 4 << 3 | 2 = 34 (0010 0010)
第二个字节: 4 (后面的字节长度)
第七个字节: 5 << 3 | 0 = 40 (0010 1000,Key)
第八个字节: 15 (0000 1111,Value)
另外,GPB还使用了Varint来表示数字,它用一个或多个字节来表示一个数字,值越小的数字使用越少的字节数。对于可选的Field,如果消息中不存在该field,那么在最终的Message Buffer中就没有该field,这些特性都有助于节约消息本身的大小。
总结
通过上面的例子,应该可以看到,GPB在网络、IO上十分高效,对其跨平台、可扩展性等方面会加深一点认识了。我想比较适合它的场景应该是:
- 对性能有高要求
- 有跨平台需求、对扩展性要求高
同时也应该看到一些缺点:
- 可读性差(相比JSON、XML)
- 需要少量学习成本