Protobuf协议文件定义
选择版本
syntax声明可以选择protobuf的编译器版本(v2和v3)syntax="proto2";选择2版本,各个字段必须明确标注编号以确定序列化后二进制数据字段的位置syntax="proto3";选择3版本,没有强制使用字段编号
字段修饰符
required- 对于required的字段而言,编号初值是必须要提供的,否则字段的便是未初始化的
- 对于修饰符为required的字段,序列化的时候必须给予初始化,否则程序运行会异常
optional- 对于optional的字段而言,如果未进行初始化,那么一个默认值将赋予该字段编号
- 也可以指定默认值,如示例所示.
repeated- 对于repeated的字段而言,该字段可以重复多个,即每个编码单元可能有多个该字段
- 在高级语言里面,我们可以通过数组来实现,而在proto定义文件中可以使用repeated来修饰,从而达到相同目的。
- 当然,出现0次也是包含在内的。
字段类型
| proto Type | C++ Type | Notes |
|---|---|---|
| int32 | int32 | 有符号32位整型数,非固定长度编码,编码效率比sint32低 |
| int64 | int64 | 有符号64位整型数,非固定长度编码 |
| uint32 | uint32 | 无符号32位整型数,非固定长度编码 |
| uint64 | uint64 | 无符号64位整型数,非固定长度编码 |
| sint32 | int32 | 有符号32位整型数,非固定长度编码,效率较高 |
| sint64 | int64 | 有符号64位整型数,非固定长度编码,效率较高 |
| fixed32 | uint32 | 无符号,固定4字节编码,数据大于2^28时,效率比uint32高 |
| fixed64 | uint64 | 无符号,固定8字节编码,数据大于2^56时,效率比uint32高 |
| sfixed32 | int32 | 有符号,固定4字节编码 |
| sfixed64 | int64 | 有符号,固定8字节编码 |
| bool | bool | 布尔值 |
| float | float | 浮点数 |
| double | double | 浮点数 |
| string | string | 必须为UTF-8编码或者7bit ASCII字符串 |
| bytes | string | 字节数组 |
字段类型对应二进制类型
| 字段类型 | 二进制类型 | 二进制编码值 |
|---|---|---|
| int32,int64,uint32,uint64,sint32,sint64,bool,enum | Varint(可变长度int) | 0 |
| fixed64,sfixed64,double | 64bit固定长度 | 1 |
| string,bytes,inner messages(内部嵌套),packaed repeated fields(repeated字段) | Length-delimited | 2 |
| groups(deprecated) | Start group | 3 |
| groups(deprecated) | Endd group | 4 |
| fixed32,sfixed32,float | 32bit固定长度 | 5 |
数据编码原则
1.Varints编码规则
- protobuf编码基础是Varints,Varints是将一个整数序列化为一个或多个Bytes的方法,越小的整数,使用的Bytes越少
- 每个byte最高位(msb)是标志位,0表示是最后一个byte,1表示该字段值还有后续byte
- 每个byte低7位存放数值
- Varints使用Little Endian(小端)字节序
- 转换示例
1 | dec(300) |
2.消息编码规则
- message都是以一组或多组key-value对组成,key和value分别采用不同的编码方式
- 序列化时,将message中所有key-value序列化成二进制字节流。反序列化时,解析出所有key-value对,
如果遇到无法识别的类型,则直接跳过。这种机制保证了旧有的编/解码在协议添加新的字段时,依旧可以正常工作 - key由两部分组成,一部分是在定义消息时对字段的编号(field_num),另一部分是字段类型(wire_type,编号最大不超过
536870911. - key编码方式
filed_num<<3|wire_type,编码后的二进制长度是变长的 - varint(wire_type=0)编码规则:
- int32,int64直接按照varint方法来编码,因此-1,-2这种负数由于补码数表示有很多1,所占的byte也比较多
- sint32,sint64采用
Zigzag方法来避免上述问题 - 首先采用Zigzag方法,将正数、0和负数映射到无符号数上
- 再采用varint编码方法
Zigzag映射规则
1 | Zigzag(n) = (n<<1)^(n>>31) ,n为sint32时 |
- 映射值表
| Original(原始值) | (编码后的值)EncodeAs |
|---|---|
| 0 | 0 |
| -1 | 1 |
| 1 | 2 |
| -2 | 3 |
| 2 | 4 |
| -3 | 5 |
| 2147483647 | 4294967294 |
| -2147483648 | 4294967295 |
- 64bit(wire_type=1)和32bit(wire_type=5)编码是在key后跟上Little Endian字节的数值
string,bytes都属于length-delimited编码,length-delimited(wire_type=2)的编码方式:key+length+content
- key的编码方式是统一的
- length采用varints编码方式
- content就是由length指定的长度的Bytes
完整示例
type.proto
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
26message IntType {
optional int32 a_i32 = 1;
optional int64 b_i64 = 2;
optional sint32 c_s32 = 3;
optional sint64 d_s64 = 4;
optional sfixed32 e_sf32 = 5;
optional sfixed64 f_sf64 = 6;
}
message UIntType {
optional uint32 a_u32 = 1;
optional uint64 b_u64 = 2;
optional fixed32 c_f32 = 3;
optional fixed64 d_f64 = 4;
}
message FType{
optional float a_f = 1;
optional double b_d = 2;
optional bool c_b = 3;
}
message BSType{
optional string a_s = 1;
repeated bytes b_bs = 2;
}
初始值
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//int
type::IntType it;
it.set_a_i32(-1);
it.set_b_i64(-1);
it.set_c_s32(-1);
it.set_d_s64(-1);
it.set_e_sf32(-1);
it.set_f_sf64(-1);
//uint
type::UIntType ut;
ut.set_a_u32(1);
ut.set_b_u64(2);
ut.set_c_f32(1);
ut.set_d_f64(2);
//float
type::FType ft;
ft.set_a_f(0.1);
ft.set_b_d(0.2);
ft.set_c_b(false);
//bytes
type::BSType bt;
bt.set_a_s("hello,world.");
bt.add_b_bs("h");
bt.add_b_bs("0x1");
bt.add_b_bs("d");
bt.add_b_bs("ILV");
bt.add_b_bs("0xf");
二进制表示
IntType
| 类型 | key | EncodedAs key | value | EncodedAs value | EncodedAs String | Notes |
| :–: | :–: | :————: | :—: | :————-: | :—: | :—: |
| int32 | 1 |1<<3 OR 0 = 00001000| -1 | 11111111(9 times) 00000001 | 00001000 11111111(9times) 00000001 | varints编码 |
| int64 | 2 |2<<3 OR 0 = 00010000| -1 | 11111111(9 times) 00000001 | 00010000 11111111(9times) 00000001 | varints编码 |
| sint32 | 3 |3<<3 OR 0 = 00011000| -1 | 00000001 | 00011000 00000001| 无符号变换 |
| sint32 | 4 |4<<3 OR 0 = 00100000| -1 | 00000001 | 00100000 00000001| 无符号变换 |
| sfixed32 | 5 |5<<3 OR 5 = 00101101| -1 | 11111111(4 times) | 00011000 11111111 11111111 11111111 11111111 | 固定4byte长度 |
| sfixed32 | 6 |6<<3 OR 1 = 00110001| -1 | 11111111(8 times) | 00011000 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 | 固定8byte长度 |UIntType
| 类型 | key | EncodedAs key | value | EncodedAs value | EncodedAs String | Notes |
| :–: | :–: | :————: | :—: | :————-: | :—: | :—: |
| uint32 | 1 |1<<3 OR 0 = 00001000| 1 | 00000001 | 00001000 00000001| 无符号 |
| uint64 | 2 |2<<3 OR 0 = 00010000| 2 | 00000010 | 00010000 00000010| 无符号 |
| fixed32 | 3 |3<<3 OR 5 = 00011101| 1 | 00000001 00000000(3 times) | 00011101 00000001 00000000(3 times) | 固定4byte长度 |
| fixed64 | 4 |4<<3 OR 1 = 00100001| 2 | 00000010 00000000(7 times) | 00011101 00000001 00000000(7 times) | 固定8byte长度 |FType
| 类型 | key | EncodedAs key | value | EncodedAs value | EncodedAs String | Notes |
| :–: | :–: | :————: | :—: | :————-: | :—: | :—: |
| float | 1 |1<<3 OR 5 = 00001101| 0.1 | 11001101 11001100 11001100 00111101 | 00001101 11001101 11001100 11001100 00111101 | IEEE浮点数,4byte |
| double | 2 |2<<3 OR 1 = 00010001| 0.2 | 10011010 10011001 10011001 10011001 10011001 10011001 11001001 00111111 | 00010001 10011010 10011001 10011001 10011001 10011001 10011001 11001001 00111111| IEEE浮点数,8byte |
| bool | 3 |3<<3 OR 0 = 00011000| false | 00000000 | 00011000 00000000 | 固定1byte长度 |BSType
| 类型 | key | EncodedAs key | value | EncodedAs value | EncodedAs String | Notes |
| :–: | :–: | :————: | :—: | :————-: | :—: | :—: |
| string | 1 |1<<3 OR 2 = 00001010| “hello,world.” | 00001100(length) 01101000 01100101 01101100 01101100 01101111 00101100 01110111 01101111 01110010 01101100 01100100 00101110(ASCII) | length+content编码 |
| bytes | 2 |2<<3 OR 2 = 00010010| “h”,”0x1”,”d”,”ILV”,”0xf” | 00000001 01101000 00010010 00000011 00110000 01111000 00110001 00010010 00000001 01100100 00010010 00000011 01001001 01001100 01010110 00010010 00000011 00110000 01111000 01100110 | 符号分割 |
3.编解码字段顺序
- 编解码与字段顺序无关,由key-value机制就能保证
- 对于未知的字段,编码的时候会把它写在序列化完的已知字段后面
嵌套与引用
1.嵌套定义
message定义中可以嵌套定义message,enum- 嵌套定义的单元外部可见,引用路径由外到内逐层引用
2.引用
- 定义
package相当于C++的namespace,外部引用时需要package名 - 引用外部message定义,只需要
import外部proto文件即可使用 - 引用使用相对路径,生成文件时protobuf可以自动处理
完整示例
- user.proto
1 | //user.proto |
- room.proto
1 | //room.proto |
Protobuf编译使用
- 由协议生成C++文件:
protoc --cpp_out=${DIR} proto-file... - 多个协议文件依赖关系:
protoc --cpp_out=${DIR} base-proto-file deliver-proto-file... - 依赖问题按相对路径处理import即可解决
cmake自动化编译
1 | FIND_PACKAGE(Protobuf REQUIRED) |
Protobuf序列化与反序列化
1.初始化
set_xxx()设置required,optional字段值add_xxx()添加repeated字段值set_xxx(int,x)设置repeated中元素的值
2.序列化
required字段需要初始化,可以通过IsInitialized来检查是否完成message对象的初始化SerializedAsString(),SerializedToString(std::string*)序列化为std::stringSerializedToArray(void*,int)序列化为byte数组SerializedToOstream(ostream*)序列化到输出流ByteSize()获取二进制字节序的大小,可用于初始化存放容器
3.反序列化
ParseFromString(std::string& data)从字符串中反序列化ParseFromArray(const void *,int)从字节序中反序列化ParseFromIstream(istream*)从输入流中反序列化has_xxx()用于检查相应字段是否存在数据xxx_size()用于确定repeated字段是否存在,0表示未序列化
4.获取对象
xxx()返回required/optional字段的const值,只读模式,返回repeated列表的指针,用于修改mutable_xxx()返回字段指针,用于修改xxx(int)返回repeated字段列表的元素,只读
protobuf实现原理
1.protobuf的cache机制
- protobuf message的clear()操作是存在cache机制的,它并不会释放申请的空间,这导致占用的空间越来越大。
- 如果程序中protobuf message占用的空间变化很大,那么最好每次或定期进行清理。这样可以避免内存不断的上涨。
注意事项
- 嵌套定义时,被嵌套的结构体被解析成A_B形式,需要获取
mutable_b()指针来初始化该字段,使用栈上的对象会导致程序异常 - 应用程序中使用protobuf,需要在退出程序时调用
google::protobuf::ShutdownProtobufLibrary()以清理内存,否则会造成内存泄漏
