深入 ProtoBuf - 序列化源碼解析

在上一篇 深入 ProtoBuf - 編碼 中原探,我們詳細(xì)解析了 ProtoBuf 的編碼原理乱凿。

有了這個(gè)知識儲備,我們就可以深入 ProtoBuf 序列化咽弦、反序列化的源碼徒蟆,從代碼的層面理解 ProtoBuf 具體是如何實(shí)現(xiàn)對數(shù)據(jù)的編碼(序列化)和解碼(反序列化)的。

我們重新復(fù)習(xí)一下型型, ProtoBuf 的序列化使用過程:

  • 定義 .proto 文件
  • protoc 編譯器編譯 .proto 文件生成一系列接口代碼
  • 調(diào)用生成的接口實(shí)現(xiàn)對 .proto 定義的字段的讀取以及 message 對象的序列化段审、反序列化方法

具體調(diào)用代碼如下:

Example1 example1;
example1.set_int32val(val);
example1.set_stringval("hello,world");
example1.SerializeToString(&output);

調(diào)用 SerializeToString 函數(shù)將 example1 對象序列化(編碼)成字符串。我們的目的就是了解 SerializeToString 函數(shù)里到底發(fā)生了什么闹蒜,是怎么一步一步得到最終的序列化結(jié)果的寺枉。

注意:并非編碼成字符串?dāng)?shù)據(jù),string 只是作為編碼結(jié)果的容器

我們在 .proto 文件中定義的 message 在最終生成的對應(yīng)語言的代碼中绷落,例如在 C++ (xxxx.pb.h姥闪、xxxx.pb.cpp) 中每一個(gè)在 .proto 文件中定義的 message 字段都會在代碼中構(gòu)造成一個(gè)類,且這些 message 消息類繼承于 ::google::protobuf::Message砌烁,而 ::google::protobuf::Message 繼承于一個(gè)更為輕量的 MessageLite 類筐喳。其相關(guān)的類圖如下所示:

protobuf-class-analysis.png

而我們經(jīng)常調(diào)用的序列化函數(shù) SerializeToString 并定義在基類 MessageLite 中。

編碼

當(dāng)某個(gè) Message 調(diào)用 SerializeToString 時(shí)函喉,經(jīng)過一層層調(diào)用最終會調(diào)用底層的關(guān)鍵編碼函數(shù) WriteVarint32ToArray 或 WriteVarint64ToArray避归,整個(gè)過程如下圖所示:

ProtoBuf 序列化_反序列化時(shí)序圖.png

WriteVarint32ToArray 函數(shù)可在源碼目錄下的 google.protobuf.io 包下的 coded_stream.h 中找到。在上一篇 深入 ProtoBuf - 編碼 中我們解析了 Varint 編碼原理和詳細(xì)過程管呵,WriteVarint32ToArray(以及 WriteVarint64ToArray)便是 Varint 編碼的核心梳毙。

可以對照上一篇指出的 Varints 編碼的幾個(gè)關(guān)鍵點(diǎn)來閱讀以下代碼,可以看出編碼實(shí)現(xiàn)確實(shí)優(yōu)雅捐下,代碼如下:

inline uint8* CodedOutputStream::WriteVarint32ToArray(uint32 value, uint8* target) {
  // 0x80 -> 1000 0000
  // 大于 1000 0000 意味這進(jìn)行 Varints 編碼時(shí)至少需要兩個(gè)字節(jié)
  // 如果 value < 0x80顿天,則只需要一個(gè)字節(jié)堂氯,編碼結(jié)果和原值一樣,則沒有循環(huán)直接返回
  // 如果至少需要兩個(gè)字節(jié)
  while (value >= 0x80) {
    // 如果還有后續(xù)字節(jié)牌废,則 value | 0x80 將 value 的最后字節(jié)的最高 bit 位設(shè)置為 1,并取后七位
    *target = static_cast<uint8>(value | 0x80);
    // 處理完七位啤握,后移鸟缕,繼續(xù)處理下一個(gè)七位
    value >>= 7;
    // 指針加一,(數(shù)組后移一位)   
    ++target;
  }
  // 跳出循環(huán)排抬,則表示已無后續(xù)字節(jié)懂从,但還有最后一個(gè)字節(jié)

  // 把最后一個(gè)字節(jié)放入數(shù)組
  *target = static_cast<uint8>(value);
  // 結(jié)束地址指向數(shù)組最后一個(gè)元素的末尾
  return target + 1;
}

// Varint64 同理
inline uint8* CodedOutputStream::WriteVarint64ToArray(uint64 value,
                                                      uint8* target) {
  while (value >= 0x80) {
    *target = static_cast<uint8>(value | 0x80);
    value >>= 7;
    ++target;
  }
  *target = static_cast<uint8>(value);
  return target + 1;
}

在上面已添加詳細(xì)注釋,這里再強(qiáng)調(diào)幾個(gè)關(guān)鍵點(diǎn)蹲蒲。

  • value | 0x80:xxx ... xxxx xxxx | 000 ... 1000 0000 的結(jié)果其實(shí)就是將最后一個(gè)字節(jié)的第一個(gè) bit(最高位) 置 1番甩,其他位不變,即 xxx ... 1xxx xxxx届搁。注意 target 是 uint8 類型的指針缘薛,這意味它只會截?cái)喃@取最后一個(gè)字節(jié),即 1xxx xxxx卡睦,這里的 1 意味著什么宴胧?這個(gè) 1 就是所謂的 msb 了,意味著后續(xù)還有字節(jié)表锻。之后就是右移 7 位(去掉最后 7 位)恕齐,處理下一個(gè) 7位。
  • 通過這里的代碼應(yīng)該可以體會到為什么 Varints 編碼結(jié)果是低位排在前面了瞬逊。

了解了最底層 IO 包中的編碼函數(shù)显歧,再結(jié)合上篇文章介紹的編碼原理,對 ProtoBuf 的編碼應(yīng)該有了更深入的認(rèn)識确镊。

Varints 類型序列化實(shí)現(xiàn)

int32士骤、int64、uint32骚腥、uint64

int32 類型編碼函數(shù)對應(yīng)為 WriteInt32ToArray敦间,源碼如下:

// WriteTagToArray 函數(shù)將 Tag 部分寫入
// WriteInt32NoTagToArray 函數(shù)將 Value 部分寫入
// WriteTagToArray 和 WriteInt32NoTagToArray 底層
// 均調(diào)用 coded_stream.h 中的 WriteVarint32ToArray
//因?yàn)?ProtoBuf 中的 Tag 均采用 Varint 編碼
// int32 的 Value 部分也采用 Varint 編碼
inline uint8* WireFormatLite::WriteInt32ToArray(int field_number, int32 value,
                                                uint8* target) {
  target = WriteTagToArray(field_number, WIRETYPE_VARINT, target);
  return WriteInt32NoTagToArray(value, target);
}

int64、uint32束铭、uint64 類型與 int32 類型同理廓块,只是處理位數(shù)有所不同。

uint32 和 uint64 也是采用 Varint 編碼契沫,所以底層編碼實(shí)現(xiàn)與 int32带猴、int64 一致。

sint32懈万、sint64

這兩種類型編碼函數(shù)對應(yīng)為 WriteSInt32ToArray 和 WriteSInt64ToArray 拴清。

在上一篇文章 深入 ProtoBuf - 編碼 中我們已經(jīng)介紹過 Varint 編碼在負(fù)數(shù)的情況下編碼效率很低靶病,固對于 sint32、sint64 類型我們會采用 ZigZag 編碼將負(fù)數(shù)映射成正數(shù)然后再進(jìn)行 Varint 編碼口予,而這種映射并非采用存儲的 Map娄周,而是使用移位實(shí)現(xiàn)。sint32 的 ZigZag 源碼實(shí)現(xiàn)如下:

inline uint32 WireFormatLite::ZigZagEncode32(int32 n) {
  // 右移為算數(shù)右移
  // 左移時(shí)需要先將 n 轉(zhuǎn)成 uint32 類型沪停,防止溢出
  // 當(dāng) n 為正數(shù)時(shí) result = 2 * n
  // 當(dāng) n 為負(fù)數(shù)時(shí) result = - (2 * n + 1)
  return (static_cast<uint32>(n) << 1) ^ static_cast<uint32>(n >> 31);
}

經(jīng)過 ZigZagEncode32 編碼之后煤辨,數(shù)字成為一個(gè)正數(shù),之后等同于 int32 或 int64 進(jìn)行完全相同的編碼處理木张。

bool 與 enum

bool 和 enum 本質(zhì)就是整型众辨,編碼處理與 int32、int64 相同舷礼。

32-bit鹃彻、64-bit

fixed32/fixed64

fixed32 類型對應(yīng) WriteFixed32ToArray 函數(shù),32-bit妻献、64-bit類型的字段比起上述 Varint 類型則要簡單的多蛛株,因?yàn)槊總€(gè)數(shù)字均是固定字節(jié),源碼如下:

inline uint8* WireFormatLite::WriteFixed32ToArray(int field_number,
                                                  uint32 value, uint8* target) {
  // WriteTagToArray: Tag 依然是 Varint 編碼旋奢,與上一節(jié) Varint 類型是一致的
  // WriteFixed32NoTagToArray:固定寫四個(gè)字節(jié)即可
  target = WriteTagToArray(field_number, WIRETYPE_FIXED32, target);
  return WriteFixed32NoTagToArray(value, target);
}

fixed64 與 fixed32 同理泳挥,不再贅述。

sfixed32/sfixed64

sfixed32 類型對應(yīng) WriteSFixed32ToArray 函數(shù)至朗,源碼如下:

inline uint8* WireFormatLite::WriteSFixed32ToArray(int field_number,
                                                   int32 value, uint8* target) {
  target = WriteTagToArray(field_number, WIRETYPE_FIXED32, target);
  return WriteSFixed32NoTagToArray(value, target);
}

其中 WriteSFixed32NoTagToArray 源碼如下:

inline uint8* WireFormatLite::WriteSFixed32NoTagToArray(int32 value,
                                                        uint8* target) {
  return io::CodedOutputStream::WriteLittleEndian32ToArray(
      static_cast<uint32>(value), target);
}

由此可知屉符,對于位數(shù)固定的 sfixed32 是將其轉(zhuǎn)成 uint32 類型,然后使用與 fixed32 相同的函數(shù)寫入锹引。

sfixed64 與 sfixed32 同理矗钟,不贅述。

Length delimited 字段序列化

因?yàn)槠渚幋a結(jié)構(gòu)為 Tag - Length - Value嫌变,所以其字段完整的序列化會稍微多出一些過程吨艇,其中有一些需要我們進(jìn)一步整理。現(xiàn)在以一個(gè) string 類型字段的序列化為例腾啥,來看看其序列化的完整過程东涡,畫出其程序時(shí)序圖(上文出現(xiàn)過)如下:

ProtoBuf 序列化_反序列化時(shí)序圖.png

可對照上述時(shí)序圖來閱讀源碼,其序列化實(shí)現(xiàn)的幾個(gè)關(guān)鍵函數(shù)為:

  • ByteSizeLong:計(jì)算對象序列化所需要的空間大小倘待,在內(nèi)存中開辟相應(yīng)大小的空間
  • WriteTagToArray:將 Tag 值寫入到之前開辟的內(nèi)存中
  • WriteStringWithSizeToArray:將 Length + Value 值寫入到之前開辟的內(nèi)存中

其序列化代碼的重點(diǎn)過程在上圖的右下角疮跑,先是調(diào)用 WriteTagToArray 函數(shù)將 Tag 值寫入到內(nèi)存,返回指向下一個(gè)字節(jié)的指針以便繼續(xù)寫入凸舵。調(diào)用 WriteStringWithSizeToArray 函數(shù)祖娘,這個(gè)函數(shù)主要又執(zhí)行了兩個(gè)函數(shù),先是執(zhí)行 WriteVarint32ToArray 函數(shù)(注意 WriteTagToArray 內(nèi)部調(diào)用的也是這個(gè)函數(shù)啊奄,因?yàn)?Tag 和 Length 都采用 Varints 編碼)渐苏,此函數(shù)的作用是將 Length 寫入掀潮。執(zhí)行的第二個(gè)函數(shù)為 WriteStringToArray,此函數(shù)的作用是將 Value(一個(gè) UTF-8 string 值) 寫入到內(nèi)存琼富,其中底層調(diào)用了 memcpy() 函數(shù)仪吧。

綜上,對于 Varint 類型的字段自然采用 Varint 編碼鞠眉。

而對于 Length delimited 類型的字段邑商,Tag-Length-Value 中的 Tag 和 Length 依然采用 Varint 編碼,Value 若為 String 等類型凡蚜,則直接進(jìn)行 memcpy。

另外對于 embedded message 或 packed repeated 吭从,則套用上述規(guī)則朝蜘。底層編碼實(shí)現(xiàn)實(shí)際便是遍歷字段下所有內(nèi)嵌字段,然后遞歸調(diào)用編碼函數(shù)即可涩金。

下一篇

深入 ProtoBuf - 反射原理解析

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末谱醇,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子步做,更是在濱河造成了極大的恐慌副渴,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,470評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件全度,死亡現(xiàn)場離奇詭異煮剧,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)将鸵,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評論 3 392
  • 文/潘曉璐 我一進(jìn)店門勉盅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人顶掉,你說我怎么就攤上這事草娜。” “怎么了痒筒?”我有些...
    開封第一講書人閱讀 162,577評論 0 353
  • 文/不壞的土叔 我叫張陵宰闰,是天一觀的道長。 經(jīng)常有香客問我簿透,道長移袍,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,176評論 1 292
  • 正文 為了忘掉前任萎战,我火速辦了婚禮咐容,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘蚂维。我一直安慰自己戳粒,他們只是感情好路狮,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,189評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蔚约,像睡著了一般奄妨。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上苹祟,一...
    開封第一講書人閱讀 51,155評論 1 299
  • 那天砸抛,我揣著相機(jī)與錄音,去河邊找鬼树枫。 笑死直焙,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的砂轻。 我是一名探鬼主播奔誓,決...
    沈念sama閱讀 40,041評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼搔涝!你這毒婦竟也來了厨喂?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,903評論 0 274
  • 序言:老撾萬榮一對情侶失蹤庄呈,失蹤者是張志新(化名)和其女友劉穎蜕煌,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體诬留,經(jīng)...
    沈念sama閱讀 45,319評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡斜纪,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,539評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了故响。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片傀广。...
    茶點(diǎn)故事閱讀 39,703評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖彩届,靈堂內(nèi)的尸體忽然破棺而出伪冰,到底是詐尸還是另有隱情,我是刑警寧澤樟蠕,帶...
    沈念sama閱讀 35,417評論 5 343
  • 正文 年R本政府宣布蛛倦,位于F島的核電站债朵,受9級特大地震影響扳炬,放射性物質(zhì)發(fā)生泄漏锄奢。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,013評論 3 325
  • 文/蒙蒙 一靡狞、第九天 我趴在偏房一處隱蔽的房頂上張望耻警。 院中可真熱鬧,春花似錦、人聲如沸甘穿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽温兼。三九已至秸滴,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間募判,已是汗流浹背荡含。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留届垫,地道東北人释液。 一個(gè)月前我還...
    沈念sama閱讀 47,711評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像装处,于是被迫代替她去往敵國和親均澳。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,601評論 2 353

推薦閱讀更多精彩內(nèi)容