博客:blog.shinelee.me | 博客园 | CSDNc++
在上一篇文章中咱们提到,对于序列化后字节流,须要回答的一个重要问题是“从哪里到哪里是哪一个数据成员”。app
message中每个field的格式为:
required/optional/repeated FieldType FieldName = FieldNumber(a unique number in current message)
在序列化时,一个field对应一个key-value对,整个二进制文件就是一连串紧密排列的key-value对,key也称为tag,先上图直观感觉一下,图片来自Encoding and Evolution:
函数
key由wire type和FieldNumber两部分编码而成, 具体地key = (field_number << 3) | wire_type
,field_number 部分指示了当前是哪一个数据成员,经过它将cc和h文件中的数据成员与当前的key-value对应起来。ui
key的最低3个bit为wire type,什么是wire type?以下表所示:this
wire type被如此设计,主要是为了解决一个问题,如何知道接下来value部分的长度(字节数),若是google
接下来,咱们直接看一下example.pb.cc及相关的源码,看下key-value对是如何解析的。解码过程相对简单,理解了解码过程,编码也就比较显然了。编码
// example.proto package example; message Person { required string name = 1; required int32 id = 2; optional string email = 3; }
// in example.pb.cc bool Person::MergePartialFromCodedStream( ::google::protobuf::io::CodedInputStream* input) { #define DO_(EXPRESSION) if (!PROTOBUF_PREDICT_TRUE(EXPRESSION)) goto failure ::google::protobuf::uint32 tag; // @@protoc_insertion_point(parse_start:example.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) == (10 & 0xFF)) { // 10 = (1 << 3) + 2 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, "example.Person.name"); } else { goto handle_unusual; } break; } // required int32 id = 2; case 2: { if (static_cast< ::google::protobuf::uint8>(tag) == (16 & 0xFF)) { // 16 = (2 << 8) + 0 HasBitSetters::set_has_id(this); DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive< ::google::protobuf::int32, ::google::protobuf::internal::WireFormatLite::TYPE_INT32>( input, &id_))); } else { goto handle_unusual; } break; } // optional string email = 3; case 3: { if (static_cast< ::google::protobuf::uint8>(tag) == (26 & 0xFF)) { DO_(::google::protobuf::internal::WireFormatLite::ReadString( input, this->mutable_email())); ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField( this->email().data(), static_cast<int>(this->email().length()), ::google::protobuf::internal::WireFormat::PARSE, "example.Person.email"); } else { goto handle_unusual; } break; } default: { handle_unusual: if (tag == 0) { goto success; } DO_(::google::protobuf::internal::WireFormat::SkipField( input, tag, _internal_metadata_.mutable_unknown_fields())); break; } } } success: // @@protoc_insertion_point(parse_success:example.Person) return true; failure: // @@protoc_insertion_point(parse_failure:example.Person) return false; #undef DO_ }
整段代码在循环地解析input
流,遇到1个tag
(key),根据其wire type和数据类型调用相应的解析函数,若是是string
,则调用ReadString
,ReadString
会一直调用到ReadBytesToString
,若是是int32
,则调用ReadPrimitive
,ReadPrimitive
中会进一步调用ReadVarint32
。能够看到,生成的example.pb.cc决定了遇到哪一个tag
调用哪一个解析函数,从输入流中解析出值,赋给对应的成员变量,而真正进行解析的代码其实是Protobuf的源码,以下所示:.net
// in wire_format_lit.cc inline static bool ReadBytesToString(io::CodedInputStream* input, string* value) { uint32 length; return input->ReadVarint32(&length) && input->InternalReadStringInline(value, length); } // in wire_format_lit.h template <> inline bool WireFormatLite::ReadPrimitive<int32, WireFormatLite::TYPE_INT32>( io::CodedInputStream* input, int32* value) { uint32 temp; if (!input->ReadVarint32(&temp)) return false; *value = static_cast<int32>(temp); return true; } // in coded_stream.h inline bool CodedInputStream::ReadVarint32(uint32* value) { uint32 v = 0; if (PROTOBUF_PREDICT_TRUE(buffer_ < buffer_end_)) { v = *buffer_; if (v < 0x80) { *value = v; Advance(1); return true; } } int64 result = ReadVarint32Fallback(v); *value = static_cast<uint32>(result); return result >= 0; }
能够看到,若是遇到int32
的tag
,直接读取接下来的数据,若是遇到string
的tag
,会先读一个Varint32的length
,而后再读length
个字节的数据。设计
这里频繁出现了varint,length
是varint,存储的int32
数据也是varint,那varint是什么?
varint是一种可变长编码,使用1个或多个字节对整数进行编码,可编码任意大的整数,小整数占用的字节少,大整数占用的字节多,若是小整数更频繁出现,则经过varint可实现压缩存储。
varint中每一个字节的最高位bit称之为most significant bit (MSB),若是该bit为0意味着这个字节为表示当前整数的最后一个字节,若是为1则表示后面还有至少1个字节,可见,varint的终止位置实际上是自解释的。
在Protobuf中,tag和length都是使用varint编码的。length
和tag
中的field_number都是正整数int32
,这里提一下tag
,它的低3位bit为wire type,若是只用1个字节表示的话,最高位bit为0,则留给field_number只有4个bit位,1到15,若是field_number大于等于16,就须要用2个字节,因此对于频繁使用的field其field_number应设置为1到15。
好比正整数150,其使用varint编码以下(小端存储):
// proto file message Test1 { optional int32 a = 1; } // c++ file // set a = 150 // binary file, in hex // 08 96 01
其中08
为key, 96 01
为150的varint编码,解释以下
有关varint的更多内容,能够参见wiki Variable-length quantity。
至此,key-value的编码方式咱们已经解决了一半,还剩value部分没有解决,接下来看看Protobuf数据部分是如何编码的。
Protobuf中整数也是经过varint进行编码,移除每一个字节的MSB,而后拼接在一块儿,能够获得一个含有数个字节的buffer,这个buffer该怎么解释还须要参考具体的数据类型。
对于int32
或int64
,正数直接按varint编码,数据类型为int32
或int64
的负数统一被编码为10个字节长的varint(补码)。
若是是sint32
或sint64
,则采用ZigZag方式进行编码,以下表所示:
sint32 n
被编码为 (n << 1) ^ (n >> 31)
对应的varint,sint64 n
被编码为 (n << 1) ^ (n >> 63)
对应的varint,这样,绝对值较小的整数只须要较少的字节就能够表示。
至于浮点数,对应的wire type为1或5,直接按小端存储。
主要有3类:string、嵌套message以及packed repeated fields。它们的编码方式统一为 tag + length + 数据
,只是数据部分有所差别。
string的编码为 key + length + 字符,参看开篇的图片已经很清晰了。
嵌套message也很简单,直接将嵌套message部分的编码接在length
后便可,以下所示:
// proto file message Test1 { optional int32 a = 1; } message Test3 { optional Test1 c = 3; } // cpp file // set a = 150 // message Test3 binary file, in hex // 1a 03 08 96 01
其中,1a
为c
的key,03
为c
的长度,接下来的08 96 01
为a
的key+value。
packed repeated fields,指的是proto2中声明了[packed=true]
的repeated varint、32bit or 64bit数据,proto3中repeated默认packed,以下所示
// in proto2 message Test4 { repeated int32 d = 4 [packed=true]; } // in proto3 message Test4 { repeated int32 d = 4; } // 3, 270, 86942压缩存储以下,in hex 22 // key (field number 4, wire type 2), 0x22 = 34 = (4 << 3) + 2 06 // payload size (6 bytes), length 03 // first element (varint 3) 8E 02 // second element (varint 270) 9E A7 05 // third element (varint 86942)
6个字节根据varint的MSB可自动分割成3个数据。对这种packed repeated fields,在Protobuf中会以RepeatedField
对象承载,支持get-by-index、set-by-index和add(添加元素)操做。
至此,二进制文件中key-value对的编码方式已基本介绍完毕,后面将经过一个相对复杂的例子,将这些琐碎的编码方式串起来,以加深理解。