This article introduces the basic concepts of Mysql.
Oh Sorry!
This blog has’t been translated to English, please wait for a little while…
二进制序列化结构数据,时间和空间效率都比XML和json高。 proto3比proto2支持更多语言,且更简洁,去掉一些复杂的语法和特性。建议使用proto3 基于现有proto进行自定义扩展如新增字段是可行的,但删除旧字段需要小心 使用基于SerializeToString
和ParseFromString
两个核心操作 protocol buffers 是一种语言无关、平台无关、可扩展的序列化结构数据 的方法,它可用于(数据)通信协议、数据存储等。可类比 XML ,但是比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更为简单。可自定义数据的结构,使用各种语言进行编写和读取结构数据。也可以更新数据结构,而不破坏由旧数据结构编译的已部署程序。 proto3比proto2支持更多语言如go、ruby等,且更简洁,去掉一些复杂的语法和特性。 在第一行非空白非注释行写:syntax = "proto3"
proto3移除了required
,新增Any
类型 移除default
选项:字段默认值根据字段类型由系统生成。注意:默认值不会参与序列化 枚举第一个字段为必须为0 …… 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
syntax = "proto3" ; // 必须加上,否则默认使用proto2
message SearchRequest {
string query = 1 ; // 字段编号为1的字符串类型
int32 page_number = 2 ;
int32 result_per_page = 3 ;
enum Corpus {
UNIVERSAL = 0 ;
WEB = 1 ;
IMAGES = 2 ;
LOCAL = 3 ;
NEWS = 4 ;
PRODUCTS = 5 ;
VIDEO = 6 ;
}
Corpus corpus = 4 ;
}
message SearchResponse {
...
}
注意:每一个字段都有一个字段编号,用于二进制格式标识字段。1-15编号需要1个byte编码,16-2047编号需要两个字节。所以高频使用字段放在1~15编号中 singular
是默认字段规则,具有0/1个;repeated
重复0~任意次reserved
预留字段;或enum
中的保留值import
导入其他文件的定义,注意:导入proto2是可行的,但enum
不能直接使用支持嵌套类型 Any
需要import "google/protobuf/any.proto"
,可以在没有指定proto定义情况下作为一个嵌套类型使用oneof
:针对多个可选字段且只有一个字段会被设置时使用,可以节省内存map
:关联映射如map<string, Project> projects = 3;
package
:可选添加,防止不同消息类型有命名冲突…… 可以在不破坏现有代码的情况下扩展proto,只需要满足以下条件:
不改变任何现有字段号 如果添加新字段,则在新代码中注意旧消息的默认值 ;旧代码中会视为未知字段 并包含在序列化输出中(3.5及更晚版本) 如果删除某个字段,确保该字段不在新proto中使用 int32
、uint32
int64
、uint64
、bool
是兼容的,可以直接转换而不会破坏兼容性。(注意:64位数字用32位读取会产生截断)sint32
、sint64
是兼容的但不与其他int
类型兼容string
和bytes
是兼容的,只要bytes
是utf-8
编码嵌套消息和bytes
是兼容的,只要bytes
包含该消息的一个编码过的版本 fixed32
、sfixed32
是兼容的,fixed64
、sfixed64
是兼容的对于string
、bytes
、message
,optional
与repeated
兼容 enum
与int32
、uint32
int64
、uint64
兼容(注意如果值不相兼容则会被截断),但客户端反序列化后可能需要不同的处理方式。给oneof
添加一个单独字段是安全且二进制兼容的,添加多个字段则需要小心只能有一个字段被设置。给任何现有的oneof
添加任何字段都需要谨慎,这未必安全。 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
// adb.proto
syntax = "proto3" ;
message Person {
string name = 1 ;
int32 id = 2 ;
string email = 3 ;
// 电话类型枚举
enum PhoneType {
MOBILE = 0 ;
HOME = 1 ;
WORK = 2 ;
}
// 电话号码嵌套message
message PhoneNumber {
string number = 1 ;
PhoneType type = 2 ;
}
// 一个人可能有多个电话,使用repeated
repeated PhoneNumber phones = 4 ;
}
// 电话簿
message AddressBook {
repeated Person people = 1 ;
}
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
import adb_pb2
def PromptForAddress ( person ):
person . id = 1
person . name = "ywt"
person . email = "ywt@sensetime.com"
phone_number = person . phones . add ()
phone_number . number = "12345678912"
phone_number . type = adb_pb2 . Person . MOBILE
def write_test ():
address_book = adb_pb2 . AddressBook ()
address_book_file = "./book.txt"
try :
f = open ( address_book_file , "rb" )
address_book . ParseFromString ( f . read ())
f . close ()
except IOError :
print ( address_book_file + "Could not open file. Creating a new one." )
PromptForAddress ( address_book . people . add ())
f = open ( address_book_file , "wb" )
f . write ( address_book . SerializeToString ())
f . close ()
if __name__ == "__main__" :
write_test ()
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
import adb_pb2
def ListPeople ( address_book ):
for person in address_book . people :
print ( "Person ID:" , person . id )
print ( " Name:" , person . name )
print ( " E-mail address:" , person . email )
for phone_number in person . phones :
if phone_number . type == adb_pb2 . Person . MOBILE :
print ( " Mobile phone #: " , phone_number . number )
elif phone_number . type == adb_pb2 . Person . HOME :
print ( " Home phone #: " , phone_number . number )
elif phone_number . type == adb_pb2 . Person . WORK :
print ( " Work phone #: " , phone_number . number )
def read_test ():
address_book = adb_pb2 . AddressBook ()
address_book_file = "./book.txt"
f = open ( address_book_file , "rb" )
address_book . ParseFromString ( f . read ())
f . close ()
ListPeople ( address_book )
if __name__ == "__main__" :
read_test ()
要点:
varint、zigzag和packed编码方式 proto:tag、message的编码结构 默认值的缺省优化 protobuf采用和TLV (Tag-Length-Value)相似的结构,又有所不同。
message
如图所示,每个message 由一个个field 构成,每个field都是TLV结构。
注意此处Length 是可选的(如果没有length,将会使用Varint 编码)
Tag
Tag由field_number (字段编号)和wire_type (编码类型)构成,使用Varint 编码方案进行编码。
wire_type有3bit,因此支持最多8种类型,目前已经使用了6种(注意:3和4 group 已经被遗弃)
阅读本部分前,请先阅读附录的Varint 和Zigzag 编码
当使用int32、int64、uint32、uint64、bool、enum
作为字段类型时使用Varint编码
编码结构:TV(Tag-Value),不需要Length 字段
以int32为例
1
2
3
4
5
syntax = "proto3" ;
message INT32 {
int32 int32Val = 1 ;
}
1
2
0x08 0x01 // tag-(Varints)0#000 1000,value-(Varints)0#000 0001
// 注:field number为1,type为000
1
2
// 0666的补码:000 ... 101 0011010
0x08 0x9a 0x05 // tag-(Varints)0#000 1000 + value-(Varints)1#0011010 0#000 0101
1
2
0x08 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0x01
// tag-(Varints)0#000 1000 + value-(Varints)1#1111111 ... 0#000 0001
int64、uint32、uint64 与 int32 同理
1
2
3
4
5
syntax = "proto3" ;
message BOOL {
bool boolVal = 1 ;
}
1
0x08 0x01 // tag-(Varints)0#000 1000 + value-(Varints)0#000 0001
可以看到和int32的1编码结果相同,是如何区分的呢?
经过测试,是通过生成的pb2文件中的的类型进行区分的。
如通过INT32
类型读0x08 0x01
结果就是1,通过BOOL
类型读0x08 0x01
结果便是True
测试代码:
1
2
3
4
5
6
7
8
9
10
BOOL = test_pb2 . BOOL ()
INT = test_pb2 . INT32 ()
BOOL . boolVal = True # 0/1/False等可选
INT . int32Val = 1
print ( BOOL . SerializeToString ())
print ( INT . SerializeToString ())
BOOL . ParseFromString ( b ' \x08\x01 ' )
INT . ParseFromString ( b ' \x08\x01 ' )
print ( BOOL . boolVal )
print ( INT . int32Val )
结果:
1
2
3
4
b ' \x08\x01 '
b ' \x08\x01 '
True
1
有一个有意思的点:如果将bool值设置为0或False,那么结果为b''
,因为bool的默认值为false,规定解析时发现字段值为空会将字段设置为默认值,以此节省空间。
1
2
3
4
5
6
7
8
9
10
11
12
13
syntax = "proto3" ;
message Enum {
enum COLOR {
YELLOW = 0 ;
RED = 1 ;
BLACK = 2 ;
WHITE = 3 ;
BLUE = 4 ;
}
// 枚举常量必须在 32 位整型值的范围
// 使用 Varints 编码,对负数不够高效,因此不推荐在枚举中使用负数
COLOR colorVal = 1 ;
}
将字段设置为BLACK,编码结果为:
1
0x08 0x02 // tag-(Varints)0#000 1000 + value-(Varints)0#000 0010
sint32
、sint64
采用zigzag编码,会多一个映射的过程,映射过程参照附录内容
如将字段设置为负数,结果为
1
2
3
4
// 字段设置为-1
0x08 0x01 // tag-(Varints)0#000 1000 + value-(Varints)0#000 0001
// 字段设置为-2
0x08 0x03 // tag-(Varints)0#000 1000 + value-(Varints)0#000 0011
趣闻:开始protobuf只有int
类型,后面因为int
编码负数效率过低衍生出sint
,而sint
表达的正数有一部分是负数,所以又提供了uint
适应全部是正数的场景。
该类型与采用的编码结构为Tag-Value ,不同的是不管数字大小,64-bit编码为8字节,32-bit存储为4字节。
这是因为Varints 编码大数的时候使用的字节数更多,可能达到10字节之多。所以这两种类型用于大量大数的场景,反而能够节省空间。
以64-bit和double为例
1
2
3
4
5
6
7
syntax = "proto3" ;
message Example {
fixed64 fixed64Val = 1 ;
sfixed64 sfixed64Val = 2 ;
double doubleVal = 3 ;
}
设置字段结果为:
1
2
3
4
5
6
7
8
b ' \t\x01\x00\x00\x00\x00\x00\x00\x00 ' # fixed64=1
b ' \x11\xff\xff\xff\xff\xff\xff\xff\xff ' # sfixed64=-1(没有 ZigZag 编码)
b ' \x11\x01\x00\x00\x00\x00\x00\x00\x00 ' # sfixed64=1
b ' \x19\x00\x00\x00\x00\x00\x00\xf0 ?' # double = 1
b ' \x19 333333 \xf3 ?' # double=1.2
b ' \x19\x00\x00\x00\x00\x00\x00\xf4 ?' # double=1.25
b ' \x19\x00\x00\x00\x00\x00\x00\xf0\xbf ' # double = -1
b ' \x19 H \xe1 z \x14\x0e\xb3\xc3 @' # double = 10086.11
fixed
和sfixed
数据都是按照固定字节数存储,double
和float
根据数据不同可能有所变化。
此类型结构为TLV(Tag-Length-Value)
1
2
3
4
5
6
7
8
9
10
11
12
syntax = "proto3" ;
message Example {
string stringVal = 1 ;
bytes bytesVal = 2 ;
message EmbeddedMessage {
int32 int32Val = 1 ;
string stringVal = 2 ;
}
EmbeddedMessage embeddedExample1 = 3 ;
repeated int32 repeatedInt32Val = 4 ;
repeated string repeatedStringVal = 5 ;
}
设置相应值,得到结果如下(python)
1
2
3
4
5
6
7
8
9
10
11
12
WRITE = test_pb2 . Example ()
WRITE . stringVal = 'hello,world' # b'\n\x0bhello,world'
WRITE . bytesVal = b 'are you ok?' # b'\x12\x0bare you ok?'
WRITE . embeddedExample1 . int32Val = 1 # b'\x1a\x02\x08\x01'
WRITE . embeddedExample1 . stringVal = "embeddedInfo" # b'\x1a\x0e\x12\x0cembeddedInfo'
WRITE . repeatedInt32Val . append ( 2 ) # b'"\x01\x02'
WRITE . repeatedInt32Val . append ( 3 ) # 一起读:b'"\x02\x02\x03'
WRITE . repeatedStringVal . append ( "repeated1" ) # b'*\trepeated1'
WRITE . repeatedStringVal . append ( "repeated2" ) # 一起读:b'*\trepeated1*\trepeated2'
print ( WRITE . SerializeToString ())
# b'\n\x0bhello,world\x12\x0bare you ok?\x1a\x10\x08\x01\x12\x0cembeddedInfo"\x02\x02\x03*\trepeated1*\trepeated2'
可以看到,有几个特点:
如果是以python为编码,并不会将字符串完全序列化为二进制。如果是C++的结果是: 1
2
3
4
5
6
// 参考https://www.jianshu.com/p/73c9ed3a4877
0 A 0 B 68 65 6 C 6 C 6F 2 C 77 6F 72 6 C 64 // hello,world
12 0 B 61 72 65 20 79 6F 75 20 6F 6 B 3F // are you ok?
1 A 10 08 01 12 0 C 65 6 D 62 65 64 64 65 64 49 6 E 66 6F // EmbeddedMessage
22 02 02 03 // repeated int
2 A 09 72 65 70 65 61 74 65 64 31 2 A 09 72 65 70 65 61 74 65 64 32 // repeated string
默认packed=True ,所以EmbeddedMessage 和repeated 可对数据进一步压缩 要点:
每个字节使用1个bit msb 作为标识符 用更少的字节表示小的正数 负数编码反而使用更多的字节 规则:
在每个字节开头的 bit 设置了 msb (most significant bit),标识是否需要继续读取下一个字节
存储数字对应的二进制补码
补码的低位排在前面
实例:
以1的简单例子引入,注意此时只使用了1个字节 ,而正常的int32要4个字节
1
2
3
4
int32 val = 1 ; // 设置一个 int32 的字段的值 val = 1; 这时编码的结果如下
原码: 0000 ... 0000 0001 // 1 的原码表示
补码: 0000 ... 0000 0001 // 1 的补码表示
Varints 编码: 0 # 000 0001 ( 0x01 ) // 1 的 Varints 编码,其中第一个字节的 msb = 0
说明666的例子,注意此时补码低位排在前面 ,只使用了2个字节
1
2
3
4
5
int32 val = 666 ; // 设置一个 int32 的字段的值 val = 666; 这时编码的结果如下
原码: 000 ... 101 0011010 // 666 的原码
补码: 000 ... 101 0011010 // 666 的补码
Varints 编码: 1 # 0011010 0 # 000 0101 ( 0x9a 0x05 ) // 666 的 Varints 编码
说明-1的例子,-1的补码8个字节都要使用,加上msb的额外开销,最终反而使用了10个字节
1
2
3
4
int32 val = - 1 // int32出于兼容性考虑,protobuf扩展到了8个字节(针对负数)
原码: 1000 ... 0001 // 注意这里是 8 个字节
补码: 1111 ... 1111 // 注意这里是 8 个字节
Varints 编码: 1 # 1111111 ... 0 # 000 0001 ( FF FF FF FF FF FF FF FF FF 01 )
如何解决varint编码针对负数效率低下的问题呢?protobuf给出了sint32
/sint64
的解决方案
使用zigzag编码,即:有符号整数映射到无符号整数,然后再使用 Varints 编码
注意:此处的映射是以移位 实现而非映射表实现的
1
2
3
// 注意右移是arithmetic shift即考虑符号的右移
( n << 1 ) ^ ( n >> 31 ) // sint32
( n << 1 ) ^ ( n >> 63 ) // sint64
在proto3中为默认设置,只用于primitive类型
packed 将repeated的T-L-V-T-L-V……
结构转化为T-L-V-V-V-V……
例如:上文中的repeated int读出b'"\x02\x02\x03'
因为整个message只有一个repeated int,没有tag可能是因为"
缺省tag标识,0x02
表示长度为2,后面0x02
和0x03
表示数字2和3
如果我们再append数字6,则会读出b'"\x03\x02\x03\x06'
如果整个message有多个repeated int呢?
1
2
3
4
message Example {
repeated int32 repeatedInt32Val = 4 ;
repeated int32 repeatedInt32Val2 = 6 ;
}
那么则会根据大小顺序读出不一样的值,如repeatedInt32Val2
读出b'2\x03\x02\x03\x06'