English | 中文版
[TOC]
Google Protocol Buffers(简称Protobuf)是一款非常优秀的库,它定义了一种紧凑(compact,相对于XML和JSON而言)的可扩展二进制消息格式,特别适合网络数据传输。
protobuf分为两个版本:
proto2: 比较老,新项目不推荐proto3: 推荐
classDiagram
MessageLite <|-- Message
Message <-- Message : call prototype->New()
Message <|-- Person
Message <|-- AddressBook
Message <-- MessageFactory : returns default instance
MessageFactory <-- Descriptor : passes to GetPrototype()
Descriptor <-- DescriptorPool : returns
class MessageLite{
+~MessageLite()
+New(): MessageLite*
+GetTypeName(): string
+ByteSize(): int
+ParseFrom*(): bool
+SerializeTo*(...): bool
}
class Message{
+New(): Message*
+GetTypeName(): string
+ByteSize(): int
+GetDescriptor(): const Descriptor*
}
class Person{
+New(): Person*
+ByteSize(): int
+descriptor(): const Descriptor*
+default_instance(): const Person&
}
class AddressBook{
+New(): AddressBook*
+ByteSize(): int
+descriptor(): const Descriptor*
+default_instance(): const AddressBook&
}
MessageFactory <|-- GeneratedMessageFactory
class MessageFactory{
+GetPrototype(const Descriptor*): const Message*
+generated_factory(): MessageFactory*
}
class GeneratedMessageFactory{
~type_map_: hash_map<const Descriptor*, const Message>
+GetPrototype(const Descriptor*): const Message*
+singleton(): GeneratedMessageFactory*
}
class Descriptor{
+name(): string
+full_name(): string
}
class DescriptorPool{
+FindMessageTypeByName(string): const Descriptor*
+generated_pool(): const DescriptorPool*
}
| .proto Type | C++ | Java | Python | Go |
|---|---|---|---|---|
| double | double | double | float | *float64 |
| float | float | float | float | *float32 |
| int32 | int32 | int | int | *int32 |
| int64 | int64 | long | int/long | *int64 |
| uint32 | uint32 | int | int/long | *uint32 |
| uint64 | uint64 | long | int/long | *uint64 |
| sint32 | int32 | int | int | *int32 |
| sint64 | int64 | long | int/long | *int64 |
| fixed32 | uint32 | int | int/long | *uint64 |
| fixed64 | uint64 | long | int/long | *uint64 |
| sfixed32 | int32 | int | int | *int32 |
| sfixed64 | int64 | long | bool | *bool |
| bool | bool | boolean | bool | *bool |
| string | string | String | unicode | *string |
| bytes | string | ByteString | bytes | []byte |
必选的
可选的
重复的
枚举
预留Tag Number给其它模块使用,用于解决字段扩展;
例:
TODO
protoc -I [OPTION] PROTO_FILES
-Iproto文件路径OPTION输出格式--cpp_out--java_out--python_out
# 生成c++代码
protoc -I=xxx.proto --cpp_out=cpp_dir# xxx.proto:proto文件路径, cpp_dir:生成的c++路径TODO
TODO
在 Protobuf 2 中,消息的字段可以加 required 和 optional 修饰符,也支持 default 修饰符指定默认值;默认配置下,一个 optional 字段如果没有设置,或者显式设置成了默认值,在序列化成二进制格式时,这个字段会被去掉。
在 Protobuf 3 中,更进一步,直接去掉了 required 和 optional 修饰符,所有字段都是 optional 的, 而且对于原始数据类型字段,压根不提供 hasXxx() 方法。
那么怎么知道一个值到底是空值(NULL, nil),还是默认值(0/0.0/“”)呢?,有以下方法:
-
用特殊值区分,避免使用null;
-
显式定义boolean字段;
message Account { string name = 1; double profit_rate = 2; bool has_profit_rate = 3; };
-
使用oneof黑科技(skynet-pbc不支持);
message Account { string name = 1; oneof profit_rate { double profit_rate = 2; } };
-
使用warpper类型(推荐)
import "google/protobuf/wrappers.proto" message Account { string name = 1; google.protobuf.DoubleValue profit_rate = 2; }
每个8bit字节的前7个bit用来表示数据,最高位(MSB, Most Significant Bit)用作记号(flag)。
- 步骤一:以7位1组为单位逆序
- 步骤二:判断是否需要下一个字节,如果需要,设置MSB为1
数字1的二进制补码:0000 0000 0000 0000 0000 0000 0000 0001
使用varint编码:
- 步骤一:
000 0001 000 0000 000 0000 000 0000 0000 - 步骤二:
0000 0001 0000 0000 0000 0000 0000 0000
数字1经过variant编码后,二进制表示为:0000 0001,十六进制表示为:0x01
数字251的二进制补码:0000 0000 0000 0000 0000 0000 1111 1011
使用varint编码:
- 步骤一:
111 1011 000 0001 0000 0000 0000 0000 00 - 步骤二:
1111 1011 0000 0001 0000 0000 0000 0000 00
数字251经过variant编码后,二进制表示为:1111 1011 0000 0001,十六进制表示为:0xFB01
数字-1的二进制补码:1111 1111 1111 1111 1111 1111 1111 1111
- 步骤一:
111 1111 111 1111 111 1111 111 1111 1111 - 步骤二:
1111 1111 1111 1111 1111 1111 1111 1111 0111 1000
数字-1经过variant编码后,二进制表示为:1111 1111 1111 1111 1111 1111 1111 1111 0111 1000,十六进制表示为:0xFFFFFFFF78
针对负数类型,使用variant编码效率不升反降
ZigZag主要是为了解决variant编码方式针对负数类型的低效问题,主要的思路是:
编码将负数映射为对应的正数,再采用Varints编码;解码解出正数以后再根据映射关系映射回负数;
ZigZag编码的映射关系:
| Signed Original | Encoded As |
|---|---|
| 0 | 0 |
| -1 | 1 |
| 1 | 2 |
| -2 | 3 |
| 2147483647 | 4294967294 |
| -2147483648 | 4294967295 |
ZigZag的映射关系通过移位来实现:
-
sint32
Zigzag(n) = (n << 1) ^ (n >> 31) -
sint64
Zigzag(n) = (n << 1) ^ (n >> 63)
数字-1的二进制编码:1111 1111 1111 1111 1111 1111 1111 1111
使用Zigzag编码:
- n << 1:
1111 1111 1111 1111 1111 1111 1111 1110 - n >> 31:
1111 1111 1111 1111 1111 1111 1111 1111 (n << 1) ^ (n >> 31):0000 0000 0000 0000 0000 0000 0000 0001- 使用variant编码:
0000 0001 0000 0000 0000 0000 0000 0000
数字-1经过Zigzag+variant编码后,二进制表示为:0000 0001,十六进制表示为:0x01
message结构如下:
field结构:|Tag|Length|Value|或|Tag|Value|
-
Tag(varint编码)结构如下:
wire_type由三位bit构成,能表示8种类型
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 3 Start group groups(deprecated) 4 End group groups(deprecated) 5 32-bit fixed32, sfixed32, float field的类型受wire_type值的影响:
-
0
|Tag|Value|,value采用varint编码,不需要额外的位来表示整个value的长度,因为varint的MSB位标识下一个字节是否是有效的就起到了指示长度的作用。 -
1,5
|Tag|Value|,长度固定为32或64位,不需要额外的位来表示整个value长度。 -
2
|Tag|Length|Value|,表示可变长度的值,需要Length来指示长度。
-
-
Length可选,需要根据
Tag的最后三位wire_type来决定。 -
Value(varint编码)
| 大小(字节) | 类型 |
|---|---|
| 1 | + bool |
| 4 | + float + fixed32 + sfixed32 |
| 8 | + double + fixed64 + sfixed64 |
| 可变长度 | + int64 + uint64 + int32 + uint32 + enum + sint32 + sint64 |
| 带长度值和数据块 | + string + message + bytes |
- int类型针对正数的编码长度与数值大小成正比,针对负数的编码长度固定为最大值(int32和int64都是10字节)
- uint类型编码长度与int类型的正数部分完全一样,支持更大的数值范围
- sint类型编码长度与数值的绝对值大小成正比
Protobuf协议自身支持自举,使得实现起来比较简洁!其中涉及多个协议之间的关系如下:
TODO
- 下载protobuf源码,编译安装
git clone git@github.com:protocolbuffers/protobuf.git
cd protobuf && ./autogen.sh
./configure --prefix=/usr/local/protobuf
make && make check
make install && ldconfig-
定义proto文件
xxx.protomessage Example1 { optional string stringVal = 1; optional bytes bytesVal = 2; message EmbeddedMessage { int32 int32Val = 1; string stringVal = 2; } optional EmbeddedMessage embeddedExample1 = 3; repeated int32 repeatedInt32Val = 4; repeated string repeatedStringVal = 5; }
-
生成c++文件
protoc -I=xxx.proto --cpp_out=./
-
调用生成的c++文件
#include <iostream> #include <fstream> #include <string> #include "xxx.pb.h" int main() { Example example1; example1.set_stringval("hello, world"); example1.set_bytesval("are you ok?"); Example1_EmbeddedMessage *embeddedExample2 = new Example1_EmbeddedMessage(); embeddedExample2->set_int32val(1); embeddedExample2->set_stringval("embeddedInfo"); example1.set_allocated_embeddedexample1(embeddedExample2); example1.add_repeatedint32val(2); example1.add_repeatedint32val(3); example1.add_repeatedstringval("repeated1"); example1.add_repeatedstringval("repeated2"); std::string filename = "single_length_delimited_all_example1_val_result"; std::fstream output(filename, std::ios::out | std::ios::trunc | std::ios::binary); if (!example.SerializeToOstream(&output)) { std::cerr << "Failed to write example1." << std::endl; exit(-1); } return 0; }
(待确认)
对于Protocol Buffer而言,标签值为1到15的字段在编码时可以得到优化,即标签值和类型信息仅占有一个byte,标签范围是16到2047的将占有两个bytes; 而Protocol Buffer可以支持的字段数量则为2的29次方减一。有鉴于此,我们在设计消息结构时,可以尽可能考虑让repeated类型的字段标签位于1到15之间,这样便可以有效的节省编码后的字节数量;
[1] 陈硕.Linux多线程服务端编程-使用muduo c++网络库.


