工作中用到了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的理解了吧。接下來需要找下時間瞭解下反射機制了。