Protobuf 編碼及序列化的記錄

     工作中用到了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函數,其調用的過程如下圖:

protobuf序列化

        通過調用過程可以看到,序列化的最後是先用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的調用過程,如下圖:

protobuf反序列化

        可以看到,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的理解了吧。接下來需要找下時間瞭解下反射機制了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章