在上一篇 深入 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)的類圖如下所示:
而我們經(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è)過程如下圖所示:
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)過)如下:
可對照上述時(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ù)即可涩金。
下一篇
汪
汪