1. 简介
https://developers.google.com/protocol-buffers?hl=en
数据序列化和结构化的方案常见的有:xml, json, yaml等以及protobuf。
序列化:将结构数据或对象转换成能够被存储和传输(例如网络传输)的格式,同时应当要保证这个序列化结果在之后(可能在另一个计算环境中)能够被重建回原来的结构数据或对象。数据序列化侧重于效率和压缩。
结构化:侧重数据的可读性。标记性语言更多侧重于数据的结构化。
protobuf能够很好的压缩数据,但是可读性上相对较差。
2. protobuf的优势
https://www.jianshu.com/p/a24c88c0526a
- 可以在python,c++, java等主流编程语言中共享数据。使用protobuf语法编写的.proto文件(定义存储的数据结构)可以由protobuf的编译器编译生成不同语言的类文件,用于访问以及生成数据,类文件中提供各种相关的api。
- 序列化后的数据可跨语言共享:序列化后的数据(.prototxt)在各语言中格式一致,并能够被各语言的protobuf api读取以及访问,从而实现的数据的跨语言共享。
- 简单来说:protobuf被多语言支持(提供了生成多语言类文件的编译器,这些类文件可以生成并读写数据),比XML更加轻量易读。
3. protobuf的使用
1.定义message
https://developers.google.com/protocol-buffers/docs/proto3
=1,=2
tag用于在encoding的二进制文件中代表不同的属性,因此将repeated的属性用小的tag值,将不常用的属性用大的tag值,能够在一定程度上优化存储。
optional
属性若不设定则为其默认值,而required属性则必须设定。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| syntax = "proto2"; package tutorial;
message Person { required string name = 1; required int32 id = 2; optional string email = 3;
enum PhoneType { MOBILE = 0; HOME = 1; WORK = 2; } message PhoneNumber { required string number = 1; optional PhoneType type = 2 [default = HOME]; } repeated PhoneNumber phones = 4; } message AddressBook { repeated Person people = 1; }
|
2.编译protobuf
使用protobuf的编译器protoc编译.proto文件,生成对应的class。对应python_out,有不同对应的语言支持。
1 2
| protoc -I=$SRC_DIR --python_out=$DST_DIR $SRC_DIR/addressbook.proto # protoc -I=./ --python_out=./ ./addressbook.proto
|
生成address_pb2.py文件,_pb2为添加的后缀。
- 不同于其他的语言,python不直接生成对应的class,而是使用特殊的descriptor的方法描述message,可以理解成一个生成 class的模版。通过metaclass方法,如type,使用descriptor定义的属性字典,动态生成python class。
1 2 3 4 5 6 7 8 9 10 11 12 13
| class Person(message.Message): __metaclass__ = reflection.GeneratedProtocolMessageType
class PhoneNumber(message.Message): __metaclass__ = reflection.GeneratedProtocolMessageType DESCRIPTOR = _PERSON_PHONENUMBER DESCRIPTOR = _PERSON
class AddressBook(message.Message): __metaclass__ = reflection.GeneratedProtocolMessageType DESCRIPTOR = _ADDRESSBOOK
|
- descriptor的定义:实际上是attr dict
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| _ADDRESSBOOK = _descriptor.Descriptor( name='AddressBook', full_name='tutorial.AddressBook', filename=None, file=DESCRIPTOR, containing_type=None, create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='people', full_name='tutorial.AddressBook.people', index=0, number=1, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], nested_types=[], enum_types=[ ], serialized_options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=249, serialized_end=296, )
|
通过**_reflection.GeneratedProtocolMessageType()**方法生成message对应的python模块与api
**_reflection.GeneratedProtocolMessageType()**实际上是类似于type()的metaclass方法,用于动态生成python的class
1 2 3 4 5 6 7 8 9
| AddressBook = _reflection.GeneratedProtocolMessageType('AddressBook', (_message.Message,), { 'DESCRIPTOR' : _ADDRESSBOOK, '__module__' : 'address_pb2' }) """ https://github.com/protocolbuffers/protobuf/blob/87dd07b4367b7676af42105d0d102f4e536c248b/python/google/protobuf/internal/python_message.py class GeneratedProtocolMessageType(type) 动态创建对应的class对象 """
|
3.读写protobuf
https://developers.google.com/protocol-buffers/docs/reference/python-generated
1 2 3 4 5 6 7 8 9 10 11 12 13
| import addressbook_pb2 person = addressbook_pb2.Person() print(person.id) person.id = 1234
person.not_exist_field = 10 person.name = "John Doe" person.email = "jdoe@example.com" phone = person.phones.add() phone.number = "555-4321"
phone.type = addressbook_pb2.Person.HOME
|
1 2 3 4 5
| s = person.SerializeToString()
new_person = addressbook_pb2.Person() new_person.ParseFromString(s)
|
4. protobuf的编码
https://izualzhy.cn/protobuf-encode-varint-and-zigzag
https://developers.google.com/protocol-buffers/docs/encoding
1 2 3 4 5 6 7 8 9
| """varint编码简介 a = 197, varint(a) = 0xC5_01, 转为二进制 0b11000101_00000001 转换过程: 1. 每个byte的最高位代表后续的byte是否属于这个数字,比如0b11000101_00000001中 第一个字节的最高位1表示第二个字节属于本数,第二个字节最高位0代表本数字的结束 2. 去掉每个字节的最高位,并调换字节的顺序后拼接 7位 7位 0000001_1000101 = 197 """
|
varint编码实现了不定长的整形数据编码,能够为序列化节省一定的空间。
1 2 3 4 5 6 7 8 9 10 11
| """ message Test1 { optional int32 a = 1; } 并设定 a = 197, 序列化后得到 b'\x08\xc5\x01' \xc5\x01 代表a的值为197,是197的varint编码
protobuf message是一系列的键值对,键为(field_number << 3) | wire_type 因此,0x08表示wire_type = 0, field_number = 1 """
|
ZigZag是将有符号数统一映射到无符号数的一种编码方案,对于无符号数0 1 2 3 4
,映射前的有符号数分别为0 -1 1 -2 2
,负数以及对应的正数来回映射到从0变大的数字序列里,这也是”zig-zag”的名字来源。
也就是说
1 2 3 4
| if n >= 0 : return 2 * n if n < 0: return 2 * abs(n) - 1
|
转换后可以使用varint编码,绝对值较小的负数占用较少的空间。