protobuf

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是用于生成class的模版
__metaclass__ = reflection.GeneratedProtocolMessageType

class PhoneNumber(message.Message):
__metaclass__ = reflection.GeneratedProtocolMessageType
DESCRIPTOR = _PERSON_PHONENUMBER
# 在load时,GeneratedProtocolMessageType metaclass使用descriptor来生成对应的python module
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,
)
  • 生成class的代码

通过**_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'
# @@protoc_insertion_point(class_scope:tutorial.AddressBook)
})
"""
https://github.com/protocolbuffers/protobuf/blob/87dd07b4367b7676af42105d0d102f4e536c248b/python/google/protobuf/internal/python_message.py
class GeneratedProtocolMessageType(type) 动态创建对应的class对象
"""

3.读写protobuf

  • api

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 # error raised
person.name = "John Doe"
person.email = "jdoe@example.com"
phone = person.phones.add()
phone.number = "555-4321"

# enum的值为int
phone.type = addressbook_pb2.Person.HOME # or addressbook_pb2.Person.PhoneType.HOME
  • 序列化和反序列化
1
2
3
4
5
s = person.SerializeToString()
# 得到二进制的字符串
# s = b'\n\x08John Doe\x10\xd2\t\x1a\x10jdoe@example.com"\x0c\n\x08555-4321\x10\x01'
new_person = addressbook_pb2.Person()
new_person.ParseFromString(s) #反序列化

4. protobuf的编码

  • varint编码方案

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编码

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编码,绝对值较小的负数占用较少的空间。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!