|
本帖最后由 ancientcc 于 2017-7-30 07:39 编辑
一、Protobuf简介
protobuf是google提供的一个开源序列化框架,所谓序列化简单来说就是将一个类实例转换成字节数组或字节流来表达,这个字节数组或字节流可以通过文件形式保存或者通过网络发送给一个接收方;而另一个程序可以读取文件中的字节或在接收到字节流后反序列化并构造出一个新的和原来相等的实例。类似于XML,JSON这样的数据表示语言,其最大的特点是基于二进制,因此比传统的XML表示高效短小得多。虽然是二进制数据格式,但并没有因此变得复杂,开发人员通过按照一定的语法定义结构化的消息格式,然后送给命令行工具,工具将自动生成相关的类,可以支持PHP、Java、c++、Python等语言环境。通过将这些类包含在项目中,可以很轻松的调用相关方法来完成业务消息的序列化与反序列化工作。
从抽象的角度来说,protobuf框架是类实例序列化和远程调用的一种实现。protobuf使用Message/MessageLite来抽象一个可序列化和反序列化的实例,为了提升性能,用户一般需要定义一些.proto文件,并使用protobuf自带的代码生成器(编译器/protoc)来生成对具体类的序列化和反序列化代码;并且它使用将每个字段编号的方式来排列字段在序列化后的排列顺序以及处理不同版本的兼容性问题。对于远程调用来说,protobuf并没有多少实现,它用RpcChannel接口来抽象通信层的协议,而使用Service接口来抽象每个具体的可调用接口,RpcChannel需要我们自己实现,而具体的Service可以定义在.proto文件中,由protobuf自带的代码生成器生成。代码生成器同时支持BlockingService和Service(Async)的实现,其中异步方式通过注册RpcCallback来获取返回值。
protobuf在google中是一个比较核心的基础库,作为分布式运算涉及到大量的不同业务消息的传递,如何高效简洁的表示、操作这些业务消息在google这样的大规模应用中是至关重要的。而protobuf这样的库正好是在效率、数据大小、易用性之间取得了很好的平衡。
官方文档:http://code.google.com/p/protobuf/
二、Protobuf如何工作
你首先需要在一个 .proto 文件中定义你需要做序列化的数据结构信息。每个ProtocolBuffer信息是一小段逻辑记录,包含一系列的键值对。这里有个非常简单的 .proto 文件定义了个人信息:- syntax = "proto2";
- message Person {
- required string name=1;
- required int32 id=3;
- optional string email=5;
- enum PhoneType {
- MOBILE=0;
- HOME=1;
- WORK=2;
- }
- message PhoneNumber {
- required string number=1;
- optional PhoneType type=2 [default=HOME];
- }
- repeated PhoneNumber phone=9;
- }
复制代码
有如你所见,消息格式很简单,每个消息类型拥有一个或多个特定的数字字段,每个字段拥有一个名字和一个值类型。值类型可以是数字(整数或浮点)、布尔型、字符串、原始字节或者其他ProtocolBuffer类型,还允许数据结构的分级。你可以指定可选字段,必选字段和重复字段。你可以在( http://code.google.com/apis/protocolbuffers/docs/proto.html )找到更多关于如何编写 .proto 文件的信息。
一旦你定义了自己的消息格式(message),你就可以运行ProtocolBuffer编译器,将你的 .proto 文件编译成特定语言的类。假设上面这消息存储在了person.proto。- protoc.exe --cpp_out=. person.proto
复制代码
执行上面命令后将在当前目录生成person.pb.cc、person.pb.h。小结protoc处理过程,它会涉及到三个文件。
文件 | | 作用 | 示例 | <main>.proto | 输入 | 定义消息格式 | debug_dump.proto | <main>.pb.cc | 输出 | cpp文件 | debug_dump.pb.cc | <main>.pb.h | 输出 | 头文件 | debug_dump.pb.h |
*.pb.cc/h存放着生成类,向工程文件添加这两文件,你就可以在app中使用这个类来序列化、从而生成消息数据。你可以这么写代码:- Person person;
- person.set_name("John Doe");
- person.set_id(1234);
- person.set_email("jdoe@example.com");
- fstream.output("myfile",ios::out | ios::binary);
- person.SerializeToOstream(&output);
复制代码
然后,你可以读取消息中的数据:- fstream input("myfile",ios::in | ios:binary);
- Person person;
- person.ParseFromIstream(&input); // ParseFromArray用于从内存块中反序列化
- cout << "Name: " << person.name() << endl;
- cout << "E-mail: " << person.email() << endl;
复制代码
你可以在不影响向后兼容的情况下随意给数据结构增加字段,旧有的数据会忽略新的字段。所以如果使用ProtocolBuffer作为通信协议,你可以无须担心破坏现有代码的情况下扩展协议。
你可以在API参考( http://code.google.com/apis/prot ... rence/overview.html )中找到完整的参考,而关于ProtocolBuffer的消息格式编码则可以在( http://code.google.com/apis/protocolbuffers/docs/encoding.html )中找到。
三、Protobuf消息定义
要通信,必须有协议,否则双方无法理解对方的码流。在protobuf中,协议是由一系列的消息组成的。因此最重要的就是定义通信时使用到的消息格式。
消息由至少一个字段组合而成,类似于C语言中的结构。每个字段都有一定的格式。- 字段格式:限定修饰符① | 数据类型② | 字段名称③ | = | 字段编码值④ | [字段默认值⑤]
复制代码
①.限定修饰符包含 required\optional\repeated
- Required: 表示是一个必须字段,必须相对于发送方,在发送消息之前必须设置该字段的值,对于接收方,必须能够识别该字段的意思。发送之前没有设置required字段或者无法识别required字段都会引发编解码异常,导致消息被丢弃。
- Optional:表示是一个可选字段,可选对于发送方,在发送消息时,可以有选择性的设置或者不设置该字段的值。对于接收方,如果能够识别可选字段就进行相应的处理,如果无法识别,则忽略该字段,消息中的其它字段正常处理。---因为optional字段的特性,很多接口在升级版本中都把后来添加的字段都统一的设置为optional字段,这样老的版本无需升级程序也可以正常的与新的软件进行通信,只不过新的字段无法识别而已,因为并不是每个节点都需要新的功能,因此可以做到按需升级和平滑过渡。
- Repeated:表示该字段可以包含0~N个元素。其特性和optional一样,但是每一次可以包含多个值。可以看作是在传递一个数组的值。
②.数据类型
Protobuf定义了一套基本数据类型。几乎都可以映射到C++\Java等语言的基础数据类型.
protobuf数据类型 | 描述 | 打包 | 对应的C++类型 | bool | 布尔 | 1字节 | bool | double | 64位浮点 | N | double | float | 32位浮点 | N | float | int32 | 32位整数 | N | int | uint32 | 32位无符号整数 | N | unsigned int | int64 | 64位整数 | N | __int64 | uint64 | 64位无符号整数 | N | unsigned __int64 | sint32 | 32位整数,处理负数效率更高 | N | int | sint64 | 64位整数,处理负数效率更高 | N | __int64 | fixed32 | 32位无符号整数 | 4 | unsigned int32 | fixed64 | 64位无符号整数 | 8 | unsigned __int64 | sfixed32 | 32位整数、能以更高的效率处理负数 | 4 | int32 | sfixed64 | 64位整数、能以更高的效率处理负数 | 8 | _int64 | string | 只能处理 ASCII字符 | N | std::string | bytes | 用于处理多字节的语言字符、如中文 | N | std::string | enum | 可以包含一个用户自定义的枚举类型 | N(uint32) | enum | message | 可以包含一个用户自定义的消息类型 | N | object of class |
N 表示打包的字节并不是固定。而是根据数据的大小或者长度。例如int32,如果数值比较小,在0~127时,使用一个字节打包。
枚举的打包方式和uint32相同。
关于message,类似于c语言中的结构包含另外一个结构作为数据成员一样。
关于 fixed32 和int32的区别。fixed32的打包效率比int32的效率高,但是使用的空间一般比int32多。因此一个属于时间效率高,一个属于空间效率高。根据项目的实际情况,一般选择fixed32,如果遇到对传输数据量要求比较苛刻的环境,可以选择int32.
③.字段名称
字段名称的命名与C、C++、Java等语言的变量命名方式几乎是相同的。
protobuf建议字段的命名采用以下划线分割的驼峰式。例如 first_name 而不是firstName.
④.字段编码值
字段编码值用于定位一个字段,在XML和JSON中直接使用字段名来维护序列化后和类实例字段之间的映射,而且它们的字段类型(如果存储的话)一般也以字符串的形式保存在序列化后的字节流中,这样做的一个好处是序列化后的消息和消息的定义是相对独立的,并且消息可读性非常好,然而它的坏处也是比较明显的,序列化后的消息字节数会很大,序列化和反序列化的效率也会降低很多,为了解决这个问题,protobuf采用字段编码值来定位一个字段。
对每个字段,protobuf内部还定义了其类型值,从而在对无法识别的字段编码时可以通过这个类型信息判断使用那种方式解析这个未知字段。字段编码值和类型值一同组成一个int类型的值tag,其中类型值占最低三个bit,字段编码值占剩下29bit,即protobuf最大支持的字段数是2^29-1(536870911)。然而19000(kFirstReservedNumber)到19999(kLastReservedNumber)是系统保留的,因而不可以使用。protobuf在每次写入一个字段时都会先写入这个tag值,而每次读取一个字段时也先读取这个tag值以判断这个字段的编码值以及类型。
protobuf使用可变长的整型编码这个tag值,即对1-15的标识号值只需要一个字节(字段类型占用3个bit),16-2047需要两个字节,因而推荐将1-15的标识号值赋值给使用最频繁的字段,并且推荐保留一些空间给将来添加新的使用比较频繁的字段。
消息中的字段的编码值无需连续,只要是合法的,并且不能在同一个消息中有字段包含相同的编码值。
建议:项目投入运营以后涉及到版本升级时的新增消息字段全部使用optional或者repeated,尽量不实用required。如果使用了required,需要全网统一升级,如果使用optional或者repeated可以平滑升级。
字段编码值、3位类型信息、tag的更多细节参考底下的“四、深入protobuf”
⑤.默认值。当在传递数据时,对于required数据类型,如果用户没有设置值,则使用默认值传递到对端。当接受数据是,对于optional字段,如果没有接收到optional字段,则设置为默认值。
关于import
protobuf 接口文件可以像C语言的h文件一个,分离为多个,在需要的时候通过 import导入需要对文件。其行为和C语言的#include或者java的import的行为大致相同。
关于package
避免名称冲突,可以给每个文件指定一个package名称,对于java解析为java中的包。对于C++则解析为命名空间。
关于message
支持嵌套消息,消息可以包含另一个消息作为其字段。也可以在消息内定义一个新的消息。
关于enum
枚举的定义和C++相同,但是有一些限制。
枚举值必须大于等于0的整数。
使用分号(;)分隔枚举变量而不是C++语言中的逗号(,)
eg.
- enum VoipProtocol {
- H323 = 1;
- SIP = 2;
- MGCP = 3;
- H248 = 4;
- }
复制代码
四、深入protobuf源码
生成的person.pb.cc、person.pb.h会定义一个和消息同名的类:person。类成员除了读写字段外(set_name/name、set_id/id、set_email/email等),较重要还有序列化和反序列化,分别会在类实例被序列化(SerializeToOstream等)、反序列化(ParseFromIstream)时调用。
序列化person中数据到target指示的内存区。- ::google::protobuf::uint8* Person::InternalSerializeWithCachedSizesToArray(
- bool deterministic, ::google::protobuf::uint8* target) const
- {
- ::google::protobuf::uint32 cached_has_bits = 0;
- (void)cached_has_bits;
- cached_has_bits = _has_bits_[0];
- // required string name = 1;
- if (cached_has_bits & 0x00000001u) {
- ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
- this->name().data(), static_cast<int>(this->name().length()),
- ::google::protobuf::internal::WireFormat::SERIALIZE,
- "Person.name");
- target =
- ::google::protobuf::internal::WireFormatLite::WriteStringToArray(
- 1, this->name(), target);
- }
- // required int32 id = 3;
- if (cached_has_bits & 0x00000004u) {
- target = ::google::protobuf::internal::WireFormatLite::WriteInt32ToArray(3, this->id(), target);
- }
- ......
- return target;
- }
复制代码
_has_bits_是个位指示器,相应位是1时表示设置了该字段,否则没设置。代码中bit0对应name,bit2对应id,这个对应关系在protoc时写死了。
WriteStringToArray把字符串类型值写入target,WriteInt32ToArray写int,它们第一个参数都是字段编码值(什么是字段编码值见“三、Protobuf消息定义”)。上面有说字段编码值和类型组成tag,深入WriteStringToArray/WriteInt32ToArray可看到这个生成tag的过程,生成tag最终要调用宏GOOGLE_PROTOBUF_WIRE_FORMAT_MAKE_TAG。
- static const int kTagTypeBits = 3;
- #define GOOGLE_PROTOBUF_WIRE_FORMAT_MAKE_TAG(FIELD_NUMBER, TYPE) \
- static_cast<uint32>( \
- (static_cast<uint32>(FIELD_NUMBER) << ::google::protobuf::internal::WireFormatLite::kTagTypeBits) \
- | (TYPE))
- inline int WireFormatLite::GetTagFieldNumber(uint32 tag) {
- return static_cast<int>(tag >> kTagTypeBits);
- }
复制代码
通过WriteStringToArray、WriteInt32ToArray可看到tag中的类型是怎么个情况。
WireFormatLite成员 | tag类型/WireType | WriteStringToArray | WIRETYPE_LENGTH_DELIMITED | WriteInt32ToArray | WIRETYPE_VARINT |
GetTagFieldNumber作用是从tag提取字段编码值。反序列化input中数据到person到要调用这函数。
- bool Person::MergePartialFromCodedStream(
- ::google::protobuf::io::CodedInputStream* input)
- {
- #define DO_(EXPRESSION) if (!GOOGLE_PREDICT_TRUE(EXPRESSION)) goto failure
- ::google::protobuf::uint32 tag;
- // @@protoc_insertion_point(parse_start:Person)
- for (;;) {
- ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoffNoLastTag(127u);
- tag = p.first;
- if (!p.second) goto handle_unusual;
- switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
- // required string name = 1;
- case 1: {
- if (static_cast< ::google::protobuf::uint8>(tag) ==
- static_cast< ::google::protobuf::uint8>(10u)) {
- DO_(::google::protobuf::internal::WireFormatLite::ReadString(
- input, this->mutable_name()));
- ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
- this->name().data(), static_cast<int>(this->name().length()),
- ::google::protobuf::internal::WireFormat::PARSE,
- "Person.name");
- }
- else {
- goto handle_unusual;
- }
- break;
- }
- // required int32 id = 3;
- case 3: {
- if (static_cast< ::google::protobuf::uint8>(tag) ==
- static_cast< ::google::protobuf::uint8>(24u)) {
- set_has_id();
- DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
- ::google::protobuf::int32, ::google::protobuf::internal::WireFormatLite::TYPE_INT32>(
- input, &id_)));
- }
- else {
- goto handle_unusual;
- }
- break;
- }
- ......
- success:
- // @@protoc_insertion_point(parse_success:Person)
- return true;
- failure:
- // @@protoc_insertion_point(parse_failure:Person)
- return false;
- #undef DO_
- }
复制代码 |
|