使用GPB协议

为什么要使用GPB协议

GPB(Google Protocol Buffer)协议,与常用的数据通讯、存储方式相比,优点如下:

  • 跨平台(语言):相比于Java序列化、Python的pickle序列化工具,GPB是独立于语言的,如果项目对这方面有要求,GPB就脱颖而出了。序列化还有一个问题,如果你改了数据结构,很容易造成序列化失败。
  • 快:采用类似二进制的协议,更加高效。这一点主要是相比于XML、JSON这两种常用的数据标准而言的。
  • 扩展性、维护性:在后续扩展协议的时候,GPB可以很好的兼容;协议源文件跟随代码做版本控制,容易维护。

示例

结合Python的google.protobuf模块做一些深入讨论。

首先新建一个文件person.proto,假设这个是某接口的通讯协议——要传name和id两个Field(都是必填字段)。

1
2
3
4
message PERSON {
required string name = 4;
required int32 id = 5;
}

有了这个协议的源文件,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字符串的原始内容打印出来(注意两个核心方法SerializeToStringParseFromString都是google.protobuf模块提供的):

1
2
3
4
5
6
7
8
9
10
from GPB import person_pb2

person_write = person_pb2.PERSON(name = 'shaw', id = 15)
s = person_write.SerializeToString()

print len(s), "%r" % s

person_read = person_pb2.PERSON()
person_read.ParseFromString(s)
print person_read

可以看到输出:

1
2
3
8 '"\x04shaw(\x0f'
name: "shaw"
id: 15

可以看到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)
  • 需要少量学习成本