工作中用到了protobuf,然后之前在面试的时候面试官就问了一个问题,如果将int32类型的字段的值设置为0,那还会将该值进行序列化吗?当时是懵了的,因为自己还没有研究这部分。当时给的结果是不会,猜测protobuf中int32的默认值是0,既然默认值是0的,那应该就不会进行序列化了。
那次面试之后就觉得自己应该了解一下这部分了,结果这两天了解完之后,发现自己猜错了。好记性不如烂笔头,也顺便记录下这两天了解到的吧。如果觉得写得有点乱了,请原谅。这里使用的是protobuf版本是2.6.1。
1. protobuf简单介绍
即Protocol Buffer,是一个灵活的、高效的、自动化的用于对结构化数据进行序列化的协议,与XML相比,Protocol buffers序列化后的码流更小、速度更快、操作更简单,还是支持向前兼容和向后兼容的。
protobuf中使用了反射机制,可以根据字段的名字直接得到字段的值,更深入的我还没有了解,所以需要大家去百度下了。后面了解完的话,我会再写篇博客介绍的。
一个简单的proto文件内容如下:
message PbInfo
{
optional uint64 uid = 1;
optional uint32 time = 2;
optional uint32 type = 3;
required string account = 5;
repeated string key = 7;
}
可以看到每个字段都是由字段规则、字段类型、字段值、字段的编号组成,字段的规则有三种:optional fields(可选字段)、required fields(必须字段)、repeated fields(可重复字段),message内每个字段的编号都要是唯一的。
那protobuf是怎么做到向前及向后兼容的呢?靠的就是这个字段的编号,在反序列化的时候,protobuf会从输入流中读取出字段编号,然后再设置message中对应的值。如果读出来的字段编号是message中没有的,就直接忽略,如果message中有字段编号是输入流中没有的,则该字段不会被设置。所以即使通信的两端存在一方比另一方多出编号,也不会影响反序列化。但是如果两端同一编号的字段规则或者字段类型不一样,那就肯定会影响反序列化了。所以一般调整proto文件的时候,尽量选择加字段或者删字段,而不是修改字段编号或者字段类型。
2. protobuf怎么知道哪些字段需要序列化
下面的代码,是上面的文件编译生成c文件的一部分。
inline bool PbInfo::has_uid() const {
return (_has_bits_[0] & 0x00000001u) != 0;
}
inline void PbInfo::set_has_uid() {
_has_bits_[0] |= 0x00000001u;
}
inline void PbInfo::clear_has_uid() {
_has_bits_[0] &= ~0x00000001u;
}
inline void PbInfo::clear_uid() {
uid_ = GOOGLE_ULONGLONG(0);
clear_has_uid();
}
inline void PbInfo::set_uid(::google::protobuf::uint64 value) {
set_has_uid();
uid_ = value;
// @@protoc_insertion_point(field_set:proto.PbInfo.uid)
}
通过上面的代码,我们明显的看到对于每个message,protobuf都会生成一个对应的类,并且类中会有一个_has_bits_的成员变量(位图)来记录哪个字段是被设置过的。如上面的,当调用set_uid设置uid字段的值时,就会调用set_has_uid来设置_has_bits_中对应的位,序列化的时候在根据_has_bits_的值来决定序列化哪些字段。这里字段的顺序决定每个字段对应_has_bits_中哪个位,而不是根据字段的编号。
所以上面面试官问我的那个问题我的回答是错的,即使是设置成0,也会被序列化。
接下来,让我们再继续深入了解下,看看protobuf是怎么序列化及反序列化的。
3. protobuf序列化与反序列化
提前说下,我这里关于序列化与反序列化的介绍,只是大概介绍下流程而已,可能需要结合源码查看。
3.1 序列化
序列化一般是用SerializeToString或者SerializeToArray,这里只跟踪了SerializeToArray函数,其调用的过程如下图:
通过调用过程可以看到,序列化的最后是先用ListFields来获取message中所有被设置过的字段,然后再对每个字段调用SerializeFieldWithCachedSizes进行序列化。ListFields函数定义在generated_message_reflection.cc中,内容如下:
void GeneratedMessageReflection::ListFields(
const Message& message,
vector<const FieldDescriptor*>* output) const {
output->clear();
// Optimization: The default instance never has any fields set.
if (&message == default_instance_) return;
for (int i = 0; i < descriptor_->field_count(); i++) {
const FieldDescriptor* field = descriptor_->field(i);
if (field->is_repeated()) {
if (FieldSize(message, field) > 0) {
output->push_back(field);
}
} else {
if (field->containing_oneof()) {
if (HasOneofField(message, field)) {
output->push_back(field);
}
} else if (HasBit(message, field)) {
output->push_back(field);
}
}
}
...
}
上面去掉了一小部分内容。可以看到,对于字段规则为repeated的,如果长度大于0则会被序列化,containing_oneof目前还没找到是什么作用,但是可以看到有个HasBit的判断,即使判断这个字段是否被设置过,如果被设置过则添加到vector中。
3.2 protobuf反序列化
反序列化一般调用ParseFromArray或者ParseFromString,这里分析了ParseFromArray的调用过程,如下图:
可以看到,InlineMergeFromCodedStream中是让传入的message自己去反序列化输入的数据。而最终的反序列化函数ParseAndMergePartial中会不断调用ReadTag从输入数据中读出一个tag,再从tag中获取字段编号,进而获取到对应field,最终调用ParseAndMergeField来反序列化这个字段的数据。
而在ParseAndMergeField函数中,则会根据对应field的类型(注意这里是根据本地proto文件,对端并不会传送字段类型的信息),调用对应的反序列化代码。例如如果是int32,则调用AddInt32或者SetInt32函数,对于enum类型,则调用SetEnum。但是注意到一点比较奇怪的,SetInt32在解析输入并将值设置到message的时候,没有调用SetBit函数去设置该message中的_has_bits_的对应位,但是在SetString和SetEnum的时候,跟进后可以看到最终都会调用SetBit函数设置_has_bits_。
// generated_message_reflection.cc
inline void GeneratedMessageReflection::SetBit(
Message* message, const FieldDescriptor* field) const {
MutableHasBits(message)[field->index() / 32] |= (1 << (field->index() % 32));
}
inline uint32* GeneratedMessageReflection::MutableHasBits(
Message* message) const {
void* ptr = reinterpret_cast<uint8*>(message) + has_bits_offset_;
return reinterpret_cast<uint32*>(ptr);
}
上面是SetBit和MutableHasBits的函数定义,可以看到在message的对象中,保存了_has_bits_在message空间中的偏移量has_bits_offset_,这样子就可以直接得到_has_bits_了,而每个field中又存有该field对应在_has_bits_中的哪一位(filed->index()),这样子就可以直接通过SetBit函数执行和上面set_has_uid()一样的操作了。这就是映射机制的其中一部分吧。
3.3 序列化的格式
这里仅做简单介绍。
对于String类型的,是直接将字符串数据写入到缓冲区中,使用的是WriteString(io/coded_stream.h),WriteString中调用WriteRaw(io/coded_stream.cc)写入。
对于整数型的,int32的函数如下:
void CodedOutputStream::WriteVarint32(uint32 value) {
if (buffer_size_ >= kMaxVarint32Bytes) {
// Fast path: We have enough bytes left in the buffer to guarantee that
// this write won't cross the end, so we can skip the checks.
uint8* target = buffer_;
uint8* end = WriteVarint32FallbackToArrayInline(value, target);
int size = end - target;
Advance(size);
} else {
// Slow path: This write might cross the end of the buffer, so we
// compose the bytes first then use WriteRaw().
uint8 bytes[kMaxVarint32Bytes];
int size = 0;
while (value > 0x7F) {
bytes[size++] = (static_cast<uint8>(value) & 0x7F) | 0x80;
value >>= 7;
}
bytes[size++] = static_cast<uint8>(value) & 0x7F;
WriteRaw(bytes, size);
}
}
通过上面的函数可以看到protobuf对于整数的序列化方式是用一个字节的低七位来保存数值的七位,第八个位则用来记录下一个字节是否也是属于该数字的,并且是反向的。也就是说原值中的第二个低七位会被保存到下一个字节,这样子不论序列化还是反序列化的时候都很方便。看下面例子即可:
保存值 二进制 实际保存二进制
3 00000 0011 0000 0011
258 1 0000 0010 1000 0010 0000 0010
3.4 字段信息的保存
protobuf是怎么保存字段相关的信息的呢?通过查看上面反序列化时用的ReadTag涉及到的函数,我们就可以很清楚的了解了。
// coded_stream.h
inline uint32 CodedInputStream::ReadTag() {
if (GOOGLE_PREDICT_TRUE(buffer_ < buffer_end_) && buffer_[0] < 0x80) {
last_tag_ = buffer_[0];
Advance(1);
return last_tag_;
} else {
last_tag_ = ReadTagFallback();
return last_tag_;
}
}
// wire_format_lite.h
static const int kTagTypeBits = 3;
static const uitn32 kTagTypeMask = (1 << kTagTypeBits) - 1;
inline WireFormatLite::WireType WireFormatLite::GetTagWireType(uint32 tag) {
{
return static_cast<WireType>(tag & kTagTypeMask);
}
inline int WireFormatLite::GetTagFieldNumber(uint32 tag) {
return static_cast<int>(tag >> kTagTypeBits);
}
可以看到对于读到的一个tag,低三位是字段规则,而除此以外的都是字段编号使用。但是在读取该字段对应的tag的时候,如果tag用到了不止一个字节(和整型值一样的压缩方式),则会调用ReadTagFallback函数读取tag,这里代码就不贴出来了,也很容易知道大概的读取操作了。
4. 总结
以上,就是这段时间根据源码学到的protobuf相关的知识了,有点杂,不过应该能加深下对protobuf的理解了吧。接下来需要找下时间了解下反射机制了。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)