序列化是將數據結構或對象轉換成二進制字節流的過程。
Protobuf對於不一樣的字段類型採用不一樣的編碼方式和數據存儲方式對消息字段進行序列化,以確保獲得高效緊湊的數據壓縮。
Protobuf序列化過程以下:
(1)判斷每一個字段是否有設置值,有值才進行編碼。
(2)根據字段標識號與數據類型將字段值經過不一樣的編碼方式進行編碼。
(3)將編碼後的數據塊按照字段類型採用不一樣的數據存儲方式封裝成二進制數據流。數據結構
反序列化是將在序列化過程當中所生成的二進制字節流轉換成數據結構或者對象的過程。
Protobuf反序列化過程以下:
(1)調用消息類的parseFrom(input)解析從輸入流讀入的二進制字節數據流。
(2)將解析出來的數據按照指定的格式讀取到Java、C++、Phyton對應的結構類型中。ide
Varint編碼是一種變長的編碼方式,編碼原理是用字節表示數字,值越小的數字,使用越少的字節數表示。所以,能夠經過減小表示數字的字節數進行數據壓縮。
對int32類型的數字,通常須要4個字節表示。若是採用Varint編碼,對於很小的int32類型數字,則能夠用1個字節來表示;雖然大的數字會須要5個字節來表示,但大多數狀況下,消息都不會有很大的數字,因此採用Varint編碼方式老是能夠用更少的字節數來表示數字。
Varint編碼後每一個字節的最高位都有特殊含義:
A、若是是1,表示後續的字節也是數字的一部分。
B、若是是0,表示本字節是最後一個字節,且剩餘7位都用來表示數字。
當使用Varint解碼時時,只要讀取到最高位爲0的字節時,表示本字節是一個值經Varint編碼後獲得的字節流的最後一個字節。
在計算機內,負數通常會被表示爲很大的整數 ,由於計算機定義負數的符號位爲數字的最高位,若是採用Varint編碼方式表示一個負數,那麼必定須要5個byte(由於負數的最高位是1,會被當作很大的整數處理)
Protobuf定義了sint32 / sint64類型表示負數,經過先採用Zigzag編碼(將有符號數轉換成無符號數),再採用Varint編碼,從而用於減小編碼後的字節數。
對於一個int32類型的值300的Varint編碼以下:
300的二進制編碼爲:100101100(256+32+8+4)
從字節流末尾取出7bit並在最高位增長1構成一個字節:[1]010 1100
從字節流末尾取出7bit並在最高位增長1構成一個字節,若是是最後一個字節增長0:[0]0000010
兩字節爲:[0]0000010 [1]010 1100
轉換爲小端模式:10101100 00000010
編碼結果:1010 1100 0000 0010性能
Zigazg編碼是一種變長的編碼方式,其編碼原理是使用無符號數來表示有符號數字,使得絕對值小的數字均可以採用較少字節來表示,特別對錶示負數的數據能更好地進行數據壓縮。
Zigzag編碼對Varint編碼在表示負數時不足的補充,從而更好的幫助Protobuf進行數據的壓縮。所以,若是提早預知字段值是可能取負數的時候,須要採用sint32/sint64數據類型。
Protobuf經過Varint和Zigzag編碼後,大大減小了字段值佔用字節數。
-2的Zigzag過程以下:ui
T-L-V(Tag - Length - Value),即標識符-長度-字段值的存儲方式,其原理是以標識符-長度-字段值表示單個數據,最終將全部數據拼接成一個字節流,從而實現數據存儲的功能。
其中Length可選存儲,如儲存Varint編碼數據就不須要存儲Length,此時爲T-V存儲方式。
T-L-V 存儲方式的優勢:
A、不須要分隔符就能分隔開字段,減小了分隔符的使用。
B、各字段存儲得很是緊湊,存儲空間利用率很是高。
C、若是某個字段沒有被設置字段值,那麼該字段在序列化時的數據中是徹底不存在的,即不須要編碼,相應字段在解碼時纔會被設置爲默認值。編碼
消息字段的標識號、數據類型、字段值通過Protobuf採用Varint和Zigzag編碼後,以T-V(Tag-Value)方式進行數據存儲。
對於Varint與Zigzag編碼方式編碼的數據,省略了T-L-V中的字節長度Length。
Tag是消息字段標識符和數據類型經Varint與Zigzag編碼後的值,所以Tag存儲了字段的標識符(field_number)和數據類型(wire_type),即Tag = 字段數據類型(wire_type) + 標識號(field_number)。
Tag佔用一個字節的長度(若是標識符大於15,則佔用多一個字節的位置),字段數據類型(wire_type)佔用3個bit,字段標識符(field_number)佔用4個bit,最高位用於Varint編碼保留。3d
Tag = (field_number << 3) | wire_type enum WireType { WIRETYPE_VARINT = 0, WIRETYPE_FIXED64 = 1, WIRETYPE_LENGTH_DELIMITED = 2, WIRETYPE_START_GROUP = 3, WIRETYPE_END_GROUP = 4, WIRETYPE_FIXED32 = 5 };
解碼時,Protobuf根據Tag將Value對應於消息中的字段。code
message person { required int32 id = 1; // wire type = 0,field_number =1 required string name = 2; // wire type = 2,field_number =2 }
對於Person消息的name字段的Tag編碼以下:對象
nameTag = 2 << 3 | 2 nameTag = 0001 0010
根據Tag解碼獲得filed_number、wire_type:blog
nameTag = 0001 0010 field_number = nameTag >> 3 field_number = 0010 wire_type = nameTag & 3 wire_type = 010
Protobuf對於數據存儲的三大原則:
(1)Protocol Buffer將消息中的每一個字段進行編碼後,利用T - L - V 存儲方式進行數據的存儲,最終獲得一個二進制字節流。
(2)ProtoBuf對於不一樣數據類型採用不一樣的序列化方式(數據編碼方式與數據存儲方式)
Protobuf對於不一樣的字段類型採用不一樣的編碼和數據存儲方式對消息字段進行序列化,以確保獲得高效緊湊的數據壓縮。不一樣類型的數據採用的編碼方式和存儲方式以下:
對於Varint編碼數據的存儲,不須要存儲字節長度Length,使用T-V存儲方式進行存儲;對於採用其它編碼方式(如LENGTH_DELIMITED)的數據,使用T-L-V存儲方式進行存儲。
(3)ProtoBuf對於數據字段值的獨特編碼方式與T-L-V數據存儲方式,使得 ProtoBuf序列化後數據量體積極小。input
WireType=0的類型包括int32,int64,uint32,unint64,bool,enum以及sint32和sint64。
編碼方式採用Varint編碼(若是爲負數,採用Zigzag輔助編碼),數據存儲方式使用T-V方式存儲二進制字節流。
WireType=1的類型包括fixed64,sfixed64,double。
編碼方式採用64bit編碼(編碼後數據大小爲64bit,高位在後,低位在前),數據存儲方式使用T-V方式存儲二進制字節流。
WireType=2的類型包括string,bytes,嵌套消息,packed repeated字段。
對於編碼方式,標識符Tag採用Varint編碼,字節長度Length採用Varint編碼,string類型字段值採用UTF-8編碼,嵌套消息類型的字段值根據嵌套消息內部的字段數據類型進行選擇,
數據存儲方式使用T-L-V方式存儲二進制字節流。
WireType=5的類型包括fixed32,sfixed32,float。
編碼方式採用32bit編碼(編碼後數據大小爲32bit,高位在後,低位在前),數據存儲方式使用T-V方式存儲二進制字節流。
String類型字段的值使用UTF-8編碼。消息數據流以下:
message Test { required string str = 2; } // 將str設置爲:testing Test.setStr(「testing」) // 通過protobuf編碼序列化後的數據以二進制的方式輸出 // 輸出爲:18, 7, 116, 101, 115, 116, 105, 110, 103
嵌套消息類型採用T-L-V的存儲方式,外部消息的V即爲嵌套消息的字段
,在T-L-V的V中嵌套了一系列的T-L-V。
編碼方式:字段值(即V)根據字段的數據類型採用不一樣編碼方式。
message Test2 { required string str = 1; required int32 id1 = 2; } message Test3 { required Test2 c = 1; } // 將Test2中的字段str設置爲:testing // 將Test2中的字段id1設置爲:296 // 編碼後的字節爲:10 ,12 ,18,7,116, 101, 115, 116, 105, 110, 103,16,-88,2
message Test { repeated int32 Car = 4 ; // 表達方式1:不帶packed=true repeated int32 Car = 4 [packed=true]; // 表達方式2:帶packed=true } Test.setCar(3); Test.setCar(270); Test.setCar(86942);
若是序列化時對多個 T - V對存儲(不帶packed=true),則會致使Tag的冗餘,即相同的Tag存儲屢次。
爲了解決Tag數據冗餘,採用帶packed=true的repeated字段存儲方式,即將相同的Tag只存儲一次、添加repeated字段下全部字段值的長度Length、連續存儲repeated字段值,組成一個大的Tag - Length - Value -Value -Value對,即T - L - V - V - V對。
經過採用帶packed=true 的 repeated字段存儲方式,從而更好地壓縮序列化後的數據長度。
基於Protobuf序列化原理分析,爲了有效下降序列化後數據量的大小,能夠採用如下措施:
(1)多用 optional或 repeated修飾符
若optional 或 repeated 字段沒有被設置字段值,那麼該字段在序列化時的數據中是徹底不存在的,即不須要進行編碼,但相應的字段在解碼時會被設置爲默認值。
(2)字段標識號(Field_Number)儘可能只使用1-15,且不要跳動使用
Tag是須要佔字節空間的。若是Field_Number>16時,Field_Number的編碼就會佔用2個字節,那麼Tag在編碼時就會佔用更多的字節;若是將字段標識號定義爲連續遞增的數值,將得到更好的編碼和解碼性能。
(3)若須要使用的字段值出現負數,請使用sint32/sint64,不要使用int32/int64。
採用sint32/sint64數據類型表示負數時,會先採用Zigzag編碼再採用Varint編碼,從而更加有效壓縮數據。
(4)對於repeated字段,儘可能增長packed=true修飾
增長packed=true修飾,repeated字段會採用連續數據存儲方式,即T - L - V - V -V方式。
參考文獻:Carson_Ho:Protocol Buffer序列化原理大揭祕