protobuf協議研究
- 什么是protobuf
protocol buffer簡稱protobuf,是google開發的一種語言無關、平台無關、擴展性好的用於通信協議、數據存儲的結構化數據串行化方法。其相關資料文檔都可以在這找到https://developers.google.com/protocol-buffers
附:目前常見的數據序列化方法效率對比見這https://code.google.com/p/thrift-protobuf-compare/wiki/Benchmarking
- protobuf優缺點
優點:相對於xml,json等序列化協議,protobuf更加高效、靈活、簡單,在時間解析和空間壓縮方面比其他大多數協議高效很多;支持大數據量數據傳輸;可以定義自己的數據結構,然后使用代碼生成器生成的代碼來讀寫這個數據結構。你甚至可以在無需重新部署程序的情況下更新數據結構。
缺點:由於protobuf序列化后的是二進制字節的數據,相對於與xml,json來說,視覺上不是那么友好;目前支持僅支持c++,java,python三種語言
- protobuf使用及消息結構
首先定義一個.proto文件,定義你需要的串行化的數據格式,這里定義為Person.proto,表示一個人的信息;
message Person {
required string name = 1;///sdfsfsdffds
required int32 id = 2;
optional string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
repeated PhoneType pt = 7;
message PhoneNumber1 {
required string aaa = 1;
optional uint64 bb =2;
}
repeated PhoneNumber phone = 4;
repeated PhoneNumber1 phone1 = 6;
}
如你所見,protobuf文件看起來有類似於c++,什么int,enum,string;
protobuf消息結構
protobuf消息結構都已“message msgname”開頭,其中msgname是你要定義的消息段名,這里為Persson;
接下來一般每一行都是類似於 “修飾詞 數據類型 數據名字 = 序號 [default=默認值]”,
修飾詞有required,optional,repeated三種,required表示該值是必須要傳的,而且只能出現一個;optional該值可以有零個或一個,可以查詢其存在與否;repeated該值相當於一個數組或有序列表,改值可為0個、1個、或多個,可以使用選項packed = true來進行高效的編碼。
數據類型16種,其中基本數據類型15種加上message嵌套共16種;15種數據格式如下;根據需要選擇相應的數據類型

數據名字就是合法的關鍵字就行了
序號就是該字段在該消息中出現的順序編號,可以從0~2^29-1次方之間(因為每個字段類型都由一個int32的key表示,其中類型字段占后3位,剩下2^29個編號,一般從1往下順序開始,故最大可容納2^29個字段)
[default=默認值]表示可以規定默認取值,該屬性只對optional修飾的字段有效,表示如果沒使用此字段但是規定了默認值,那么序列化的時候就按默認值序列化
- protobuf序列化原理
每一個消息字段序列化可以歸結為key:value的配對,key可以唯一確定該字段類型,序號;value可以確定其值
按照 “修飾詞 數據類型 數據名字 = 序號 [default=默認值]”格式定義來分析其key和value構成
key的構成: 由一個int32的4字節碼表示;其公式為:(序號 << 3) | 字段歸一類型;由varint編碼序列化;字段歸一類型如下所示

字段歸一類型由3位表示,最多能表示8種類型,由上圖知目前已使用6種類型(其中3,4已經在2.0以后棄用,其他不變):注意:在實際實現過程中,repeated fields字段並不是按照2算的,是按照其數據類型算的
如optional int32 test = 1; 反序列化之后是:0x08=0000 1000
如optional string test1 = 2; 反序列化之后是:0x12=0001 0010
value的序列化:采用little endian的存儲方式
Base 128 Varints編碼
varint編碼使用一個字節或多個字節來表示一個整數,是邊長字節編碼,數越小,所以字節越少;每個字節都是varint類型,除了最后一個字節因為最有一個字節有空位;每個字節的低7位來存數據,最高位來表示前后兩個字節是否有關聯,為1表示后面的字節的數據和當前字節數據屬於一個整體;
對於正數編碼:如300,其二進制是100101100;其varint編碼是1010 1100 0000 0010<= 010 1100 + 000 0010 <= 0010 1100 + 0000 0001 litter endian<= 1 + 0010 1100
對於負數編碼,統一采用10個字節表示(最多能表示10*7=70位)32位和64位在最后一個字節上表現有所不同,對於32位的負數最后一個字節采用0x01填充,64采用0x0f填充
如-4,二進制補碼為0xfffffffb;采用32位vartint編碼為:0xfbffffffffffffffff01;采用64位編碼為:0xfbffffffffffffffff0f
綜上:varint適合於正整數編碼,對於負數編碼浪費空間
Signed Integers的ZigZag編碼
對於有符號整數,采用ZigZag編碼;其編碼轉換如下圖

即是:正數的2倍,負數的絕對值2倍-1;按照可得其公式:對於32位(n <<1)^(n >>31),相應的解碼公式是(n>>1)^(n&0x01);對於64位(n<<1)^(n>>63),相應的解碼公式是(n>>1)^(n&0x01);
在protobuf協議中,對sint32,sint64位先采用zigzag編碼, 然后在使用varint編碼得到
相對int32,int64來説,負數能節省空間;
固定字節編碼(fixed編碼自己取名)
采用固定4個字節:fixed32,sfixed32,float
采用固定8個字節:fixed64,sfixed64,double
無符號整數usint編碼
sint32采用varint編碼,所不同的是,對於32位采用5個字節最后一個字節使用0x0f,對於64位采用10個字節,最后一個自己使用0x01
相對於純int32來説,負數能節省空間;
string,bytes編碼(官方手冊上把repeated,embeded message也歸為這一類,個人在2.5上實現經過實踐,不是這樣)
如repeated string test = 2;test被初始化為"testing";那么其序列化后的值是12 07 74 65 73 74 69 6e 67;其中前第一個字節就是key的編碼;第二個字節是字符串長度的varint編碼;后面7個字節就是字符串的ascii
- protobuf使用實例及數據結構分析
先去google開源項目上下載最新的protobuf編譯器https://code.google.com/p/protobuf/downloads/list
使用如下命令把自己的.proto文件編譯生成對應的語言源文件
protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR path/to/file.proto
IMPORT_PATH 指定要編譯的.proto文件目錄,默認當前路勁為指定路徑;可以使用-I=IMPORT_PATH來代替--proto_path=IMPORT_PATH;由於package指定的文件可以在多個地方,故--proto_path可以使用多次來指定多個目錄;
生成的源文件有3種選擇--cpp_out表示生成c++源文件;--java_out表示生成java源文件;--python_out表示生成python源文件;后面跟着生成文件存放的目錄
最后一個參數表示要編譯哪個.proto文件
