Varint編碼規則:
在一個字節的8位里用低7位存儲數字的二進制補碼,第一位為標識位(most significant bit--msb)用來表示下一個字節是否還有意義,是否要繼續讀取下一個字節。
二進制補碼的低位排在編碼序列的前頭(逆序是以7位一組逆序)。這個辦法是為了少存0節省空間,同時也是為了方便移位運算。
舉個例子:
int a = 1; 數字1的二進制補碼表示為: 0000 0000 0000 0000 0000 0000 0000 0001 使用varints編碼后為: 1.以7位1組為單位逆序: 000 0001 000 0000 000 0000 000 0000 0000 2.第一位高位判斷是否需要拼接下一個字節來表示數字1,這里不需要,所以只需一組最高位補0 0000 0001 0000 0000 0000 0000 0000 0000 3.故數字1的vriant編碼: 0000 0001 十六進制表示為: 0x01
舉個復雜的例子:
int a = 251; 數字251的二進制補碼表示為: 0000 0000 0000 0000 0000 0000 1111 1011 使用varint編碼: 1.以7位1組單位逆序: 111 1011 000 0001 0000 0000 0000 0000 00 2.7位1組,第一位高位為msb表示是否需要下一個字節,這里第一個字節需要下一個字節,故第一個字節補高位為1。第二個字節不需要第三個字節了,故第二個字節補高位為0 1111 1011 0000 0001 0000 0000 0000 0000 00 故數字251的varint編碼為: 1111 1011 0000 0001 十六進制表示為: 0xFB01
由此可見Varint編碼方式通過逆序存儲補碼以及犧牲每一個字節的最高位用來表示是否需要讀取下一個字節,可以將4字節的整數類型壓縮至2個字節存儲。
但是因為4個字節中每個字節都少了一個bit位,因此采用Varint編碼4個字節能表示的最大數字就是228-1了而不再是232-1。
因此當數字大於228-1時,采用Varint編碼將需要5個字節,編碼效率反而下降。但是根據統計學的概率,大多數情況下不會用到這么大的數字,因此采用Varint整體效率還是高的。
另外還有一種是負數的情況
由於負數在計算機中天生占據最高位為1,因此假如還是采用Varint編碼方式來編碼-1
int a = -1; 數字-1的二進制補碼表示為: 1111 1111 1111 1111 1111 1111 1111 1111 使用varint編碼: 1.以7位1組單位逆序: 111 1111 111 1111 111 1111 111 1111 1111 2.7位1組,第一位高位為msb表示是否需要下一個字節。 1111 1111 1111 1111 1111 1111 1111 1111 0111 1000 故數字251的varint編碼為: 1111 1111 1111 1111 1111 1111 1111 1111 0111 1000 十六進制表示為: 0xFFFFFFFF78
原本負數只需4個字節就能表示,但是采用Varint編碼以后居然需要5個字節,效率不升反降。聰明的大神們為了盡可能壓榨每一寸空間絕對不會這么干的。
於是針對負數類型,protobuf內指定了sint32和sint64來存儲,采用的是ZigZag編碼的方式。
ZigZag編碼
ZigZag 編碼:有符號整數映射到無符號整數,然后再使用 Varints 編碼
既然負數的編碼效率低,那么在ZigZag編碼方式中。將負數映射為對應的正數,再采用Varints編碼。解碼時,解出是正數以后再根據映射關系映射回負數即可。
ZigZag編碼的映射關系:
不過ZigZag的映射關系不是采用映射表來實現的,而是移位來實現。
Zigzag(n) = (n << 1) ^ (n >> 31), n為sint32時
Zigzag(n) = (n << 1) ^ (n >> 63), n為sint64時
舉個例子:
uint32 a = -1; -1的二進制編碼: 1111 1111 1111 1111 1111 1111 1111 1111 n << 1后為: 1111 1111 1111 1111 1111 1111 1111 1110 n >> 31后為: 1111 1111 1111 1111 1111 1111 1111 1111 故(n << 1) ^ (n >> 31)后為: 1111 1111 1111 1111 1111 1111 1111 1110 1111 1111 1111 1111 1111 1111 1111 1111----兩行執行不進位的半加操作 0000 0000 0000 0000 0000 0000 0000 0001 故:Zigzag(-1) = 1;
Protobuf是如何采用Varint和ZigZag編碼的:
protobuf對message的所有字段的二進制編碼結構如下圖所示:
message由一個個字段組成,每一個字段(包含編號和值)在二進制編碼的時候對應上圖的field。
每個二進制field的結構由Tag-[Length]-Value組成。這里的[Length]是否需要是依據Tag的最后三位wire_type來決定的。
Tag的結構如下圖所示:其中field_number就是字段的編號。整個Tag也是采用Varints編碼的。
wire_type由三位bit構成,故能表示8種類型。
1.當wire_type等於0的時候整個二進制結構為:
Tag-Value
value的編碼也采用Varints編碼方式,故不需要額外的位來表示整個value的長度。因為Varint的msb位標識下一個字節是否是有效的就起到了指示長度的作用。
2.當wire_type等於1、5的時候整個二進制結構也為:
Tag-Value
因為都是取固定32位或者64位,因此也不需要額外的位來表示整個value的長度。
3.當wire_type等於2的時候整個二進制結構為:
Tag-[Length]-Value
因為表示的是可變長度的值,需要有額外的位來指示長度。
Protobuf2語法規則:
從一個簡單例子開始:
message Test{ required string name = 1; optional int32 phonenum = 2;
repeated int32 other = 3; }
一、指定字段規則:
1.required:指定了該規則后必須給該字段賦值,否則執行后protobuf會報錯abort
2.optional:該字段可被賦值或可不賦值;
3.repeated:該字段可出現0次或者多次;
二、指定字段的類型
double、float、uint32、uint64、bool、string、bytes、uint32、uint64、
int32、int64:如果負數指定這兩個類型編碼效率較低。負數應該使用下面的類型。
sint32、sint64:對負數會進行ZigZag編碼提高編碼效率。
fixed32、fixed64:總是4字節和8字節
sfixed32、sfixed64:總是4字節和8字節
因為采用Varint編碼4個字節能表示的最大數字就是228-1了,超過這個值就需要5個字節來表示。因此對於大於228-1的數,采用固定長度fixed32或者fixed64的效率會更高。
三、protobuf編碼實例
第一個例子:理解int32和fixed32的編碼和解碼方式
message Test { required int32 num1 = 1; required fixed32 num2 = 2; }
編寫好Test.proto文件,利用protobuf編譯器生成Test.pb.h和Test.pb.cc文件:
protoc -I=. --cpp_out=. ./Test.proto
編寫main.cc文件:
#include <iostream> #include "Test.pb.h" #include <string> #include <fstream> using namespace std; int main() { string fileName = "BinaryResult"; Test a; a.set_num1(10); a.set_num2(1073741824); fstream output(fileName.c_str(), ios::out | ios::trunc | ios::binary); a.SerializeToOstream(&output); return 0; }
編譯main.cc文件並運行:
g++ main.cc Test.pb.cc -lprotobuf
查看輸出的文件
vim -b BinaryResult
以十六進制查看二進制文件
:%!xxd
二進制文件為:
十六進制表示為:
080a 1500 0000 40 二進制表示為: 00001000000010100001010100000000000000000000000001000000
現在我們對這個二進制進行解碼流程看是否能還原出10和1073741824這兩個值。
解碼的結構如圖:
第一個字段的Tag解碼:
由於Tag也是采用Varint編碼的因此最開始依據第一個msb位讀取第一個字節(00001000)為第一個字段的Tag,最后3位(000)表示wire_type=0指示了接下來Value的編碼采用Varint方式。剩余5位(00001)表示field_number=1表示第一個字段的編號為1;
第一個字段的Value解碼:
由wire_type=0可知Value是采用Varint編碼。故讀取下一個字節(00001010),該字節的第一位msb位為0。故接下來的7位(0001010)表示第一個字段的值。因為只有一個字節因此逆序還是0001010,二進制0001010表示數字為:10
第一個字段解碼完成。
第二個字段的Tag和Value解碼:
由同樣的辦法得第二個字段field_numer=2,wire_type=5。wire_type=5對應fixed32類型的編碼方式。
fixed32類型的編碼方式因為已經固定取32位,因此不需要msb位。但為了移位方便,還是有按字節逆序編碼。因此解碼的時候也要逆序回來。
fixed32的編碼如下: 00000000 00000000 00000000 01000000 按字節逆序回來: 01000000 00000000 00000000 00000000 二進制表示的值為:1073741824
第二個字段解碼完成。
第二個例子:理解Tag-Length-Value的編碼方式
message Test { required string name = 1; }
main.cc
#include <iostream> #include "Test.pb.h" #include <string> #include <fstream> using namespace std; int main() { string fileName = "BinaryResult"; Test a; a.set_name("Steven"); fstream output(fileName.c_str(), ios::out | ios::trunc | ios::binary); a.SerializeToOstream(&output); return 0; }
編譯運行,輸出的二進制文件為:
十六進制: 0a06 5374 6576 656e 二進制表示為: 0000101000000110010100110111010001100101011101100110010101101110
解碼結構如下圖:
Tag-Length-Value方式的結構解碼:
由上圖wire_type(010)=2可知接下來的解碼形式應該是Length-Value:
以Varint編碼的方式讀取Length(00000110)=6指示接下來的Value占據6個字節。
二進制: 010100110111010001100101011101100110010101101110 得到ASCII碼: 83 116 101 118 101 110 對照ASCII碼表得到的字符串為: Steven
Tag-Length-Value編碼方式的解碼流程如上。
第三個例子:理解Tag-Length-Value-……-Value的編碼方式
message { repeated int32 num = 1 [packed=true]; }
main.cc文件:
#include <iostream> #include "Test.pb.h" #include <string> #include <fstream> using namespace std; int main() { string fileName = "BinaryResult"; Test a; a.add_num(10); a.add_num(100); a.add_num(1000); fstream output(fileName.c_str(), ios::out | ios::trunc | ios::binary); a.SerializeToOstream(&output); return 0; }
編譯運行,輸出的二進制文件:
十六進制查看: 0a04 0a64 e807 二進制查看: 000010100000010000001010011001001110100000000111
解碼結構如下圖:
Length(00000100)=4,指示repeated重復字段的編碼都在接下來的4個字節里。區分每一個字段邊界的依舊靠的是Varint編碼的msb位。
第一個字段(00001010)=10
第二個字段(01100100)=100
第三個字段:
11101000 00000111 逆序后並刪除每個字節的msb位的二進制為: 0000111 1101000
0001111101000=1000;
Tag-Length-Value-……-Value編碼方式的解碼如上所示。