prtotocol buffer是google於2008年開源的一款非常優秀的序列化反序列化工具,它最突出的特點是輕便簡介,而且有很多語言的接口(官方的支持C++,Java,Python,C,以及第三方的Erlang, Perl等)。本文從protobuf如何將特定結構體序列化為二進制流的角度,看看為什么Protobuf如此之快。
一,示例
從例子入手是學習一門新工具的最佳方法。下面我們通過一個簡單的例子看看我們如何用protobuf的C++接口序列化反序列化一個結構體。
1,編輯您將要序列化的結構體描述文件Hello.proto
每個結構體必須用message來描述,其中的每個字段的修飾符有required, repeated和optional三種,required表示該字段是必須的,repeated表示該字段可以重復出現,它描述的字段可以看做C語言中的數組,optional表示該字段可有可無。
同時,必須人為地為每個字段賦予一個標號field_number,如上圖中的1,2,3,4所示。更詳細的proto文件的編寫規則見這里。
2,用protoc工具“編譯”Hello.proto
protoc工具使用的一般格式是:
protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/xxx.proto
其中SRC_DIR是proto文件所在的目錄,DST_DIR是編譯proto文件后生成的結構體處理文件的目錄
之后會生成對結構體Hello.proto中描述的各字段做序列化反序列化的類
3, 編寫序列化進程write.cc
我們用set方法為結構體中的每個成員賦值,然后調用SerializeToOstream將結構體序列化到文件log中。
並編譯它:
4,編寫反序列化進程reader.cc
用ParseFromIstream將文件中的內容序列化到類Hello的對象msg中。
並編譯它:
5,做序列化和反序列化操作
上面只是一個簡單的例子,並沒有對protobuf的性能做測試,protobuf的性能測試詳見這里。
二,protocol buffer的數據類型
從第一節中的例子可以看出,用Protocol buffer時需要用戶自定義自己的結構體,而且結構體中的定義規則要符合google制定的規則。結構體中每個字段都需要一個數據類型,protocol buffer支持的數據類型在源代碼wire_format_lite.h中定義:
其中:
VARINT類數據表示要用variant編碼對所傳入的數據做壓縮存儲,variant編碼細節見下一節。
FIXED32和FIXED64類數據不對用戶傳入的數據做variant壓縮存儲,只存儲原始數據。
LENGTH_DELIMITED類數據主要針對string類型、repeated類型和嵌套類型,對這些類型編碼時需要存儲他們的長度信息。
START_GROUP是一個組(該組可以是嵌套類型,也可以是repeated類型)的開始標志。
END_GROUP是一個組(該組可以是嵌套類型,也可以是repeated類型)的結束標志。
每類數據包含的具體數據類型如下表所示:
WireType 表示類型
VARINT int32,int64,uint32,uint64,sint32,sint64,bool,enum
FIXED64 fixed64,sfixed64,double
LENGTH_DELIMITED string,bytes,embedded messages, packed repeadted field
START_GROUP group的開始標志
END_GROUP group的結束標志
FIXED32 fixed32,sfixed32,float
三,protocol buffer的編碼
一言以蔽之,ProtocolBuffer的編碼是盡其所能地將字段的元信息和字段的值壓縮存儲,並且字段的元信息中含有對這個字段描述的所有信息。
整個結構體序列化后抽象地看起來像下圖這樣:
可以看到,整個消息是以二進制流的方式存儲,在這個二進制流中,逐個字段以定義的順序緊緊相鄰。每個字段中由元信息tag和字段的值value組成。
其中tag是這樣編碼的:
1)field_number << 3 | wire_type
2)對上面得到的無符號類型整數做variant編碼
其中field_number第一節中提到的每個字段的標號,wire_type是第二節中提到的該字段的數據類型。
1,variant編碼
variant是一種緊湊型數字編碼,將元數據跟數字保存在一起,如下圖所示是數字131415的variant編碼:
其中第一個字節的高位msb(Most Significant Bit )為1表示下一個字節還有有效數據,msb為0表示該字節中的后7為是最后一組有效數字。踢掉最高位后的有效位組成真正的數字。
從上面可以看出,variant編碼存儲比較小的整數時很節省空間,小於等於127的數字可以用一個字節存儲。但缺點是對於大於
268,435,455(0xfffffff)的整數需要5個字節來存儲。但是一般情況下(尤其在tag編碼中)不會存儲這么大的整數。
對一個整數的variant編碼的代碼位於
./src/google/protobuf/io/coded_stream.cc:WriteVarint32FallbackToArrayInline()函數中,摘錄如下;
inline uint8* CodedOutputStream::WriteVarint32FallbackToArrayInline( uint32 value, uint8* target) { target[0] = static_cast<uint8>(value | 0x80); if (value >= (1 << 7)) { target[1] = static_cast<uint8>((value >> 7) | 0x80); if (value >= (1 << 14)) { target[2] = static_cast<uint8>((value >> 14) | 0x80); if (value >= (1 << 21)) { target[3] = static_cast<uint8>((value >> 21) | 0x80); if (value >= (1 << 28)) { target[4] = static_cast<uint8>(value >> 28); return target + 5; } else { target[3] &= 0x7F; return target + 4; } } else { target[2] &= 0x7F; return target + 3; } } else { target[1] &= 0x7F; return target + 2; } } else { target[0] &= 0x7F; return target + 1; } }
整個結構體的序列化過程如下:
a, 調用Hello類的ByteSize()計算出序列化后的長度,分配該長度的空間,以備以后將每個字段填充到該空間中,示例中的長度計算公式是:
1+Int32Size()+1+4+1+StringSize()
b, 調用Hello類的SerializeWithCachedSizes()對每個元素序列化
下面是對每一類元素的序列化編碼詳解
2 int32/int64/uint32/uint64類型的編碼
a,計算長度 1 + Int32Size(值);
b,調用WireFormatLite::WriteInt32(…)將該字段的元信息和字段值寫入到新空間中:
例如用戶為int32傳入值123,則該字段的存儲如下:
第一個字節variant(1<<3|0) 第二個字節variant(123)
3,String類型的編碼
a, 計算長度 1 + variant(stringLength)+stringLength
b, 調用WireFormatLite::WriteString(…)將該字段的元信息、長度和值寫入到新空間中
例如用戶為string傳入值“hello”,則該字段的存儲如下:
第一個字節variant(2<<3|2) ,第二個字節variant(5) ,剩余的字節 “hello”
4,float類型的編碼
a, 計算長度 1+4
b,調用WireFormatLite::WriteFloat(…)將該字段的元信息和值寫入到新空間中
其中寫float內存拷貝的代碼非常精煉:
inline float WireFormatLite::DecodeFloat(uint32 value) { union {float f; uint32 i;}; i = value; return f; }
5, 嵌套結構體 編碼
a, 1 + variant32(embedded長度)+embedded的長度
b,調用WireFormatLite::WriteMessageMaybeToArray(…)將該字段的元信息、長度和值寫入到新空間中
6,repeated類型字段編碼
a,計算長度 1*repeated個數 + variant32(repeated長度)+repeated長度
b,調用WireFormatLite::WriteMessageMaybeToArray(…)將下圖所示編碼的值寫入到新空間中
7,sint32, sint64類型字段編碼
從int32編碼中可以看出,當int32傳入-1時所耗的空間很大,所以結構體定義中引入了sint32和sint64類型的數據,這種數據采用一種叫zigzag的編碼方式,使絕對值比較小的整數也占用比較小的字節。
zigzag編碼的映射關系圖如下
它將原始類型為int32的數用uint32的數表示,當一個數的絕對值比較小時,將其用uint32表示,再采用variant編碼存儲就會比較節省空間。
對一個整數的zigzag編碼也很巧妙:
inline uint32 WireFormatLite::ZigZagEncode32(int32 n) { // Note: the right-shift must be arithmetic return (n << 1) ^ (n >> 31); }
四 總結
從上面的編碼可以看出, protocol buffer壓榨每一個沒有真正用到的字節,使之序列化后的字節盡量少,清晰的數據編碼和諸多的位操作使之變得很輕便簡潔高效。同時它提供了很多編程語言的接口,可以廣泛應用於RPC系統中。
但是,由於它將元信息編碼到二進制位中,使得序列化后的數據可讀性非常差(其實是沒有可讀性 ^.^)。
五 參考文獻
https://developers.google.com/protocol-buffers/ protobuf官方首頁
https://developers.google.com/protocol-buffers/docs/encoding 詳細講述了protobuf的編碼細節(有些地方比本文還詳細)
http://code.google.com/p/thrift-protobuf-compare/wiki/Benchmarking protobuf性能
http://code.google.com/p/protobuf/wiki/ThirdPartyAddOns提供了其他眾多語言實現的protocol buffer,但是安全性和效率不能保證
http://www.cppblog.com/colorful/archive/2012/05/05/173761.html提供了安裝protobuf的方法,並給出了一個小例子