google protobuf序列化原理解析 (PHP示例)

1、簡介php


Protocol Buffers是谷歌定義的一種跨語言、跨平臺、可擴展的數據傳輸及存儲的協議,由於將字段協議分別放在傳輸兩端,傳輸數據中只包含數據自己,不須要包含字段說明,因此傳輸數據量小,解析效率高。一條消息用protobuf序列化後的大小是json的10分之一。相似的序列化框架還有Thrift、avro。thrift和avro都提供rpc服務和序列化,而protocol buffer只是提供序列化功能。git

 

 

2、安裝github


安裝Google的protoc編譯器,這個工具能夠把proto文件中定義的Message轉換爲各類編程語言中的類。下載release版本直接編譯安裝。編程

https://github.com/google/protobuf/json

3.1.0及如下版本,不支持PHP,須要安裝插件vim

https://github.com/bramp/protoc-gen-php、https://github.com/chobie/protoc-gen-php、https://github.com/drslump/Protobuf-PHP數組

/usr/local/protobuf/bin/protoc --help 查看有沒有--php_out選項app

 

 

3、應用框架


一、限定修飾符編程語言

 

 

Required

​ 表示是一個必須字段,必須相對於發送方,在發送消息以前必須設置該字段的值,對於接收方,必須可以識別該字段的意思。發送以前沒有設置required字段或者沒法識別required字段都會引起編解碼異常,致使消息被丟棄。

Optional(singular)

​ 表示是一個可選字段,在發送消息時,能夠有選擇性的設置或者不設置該字段的值。對於接收方,若是可以識別可選字段就進行相應的處理,若是沒法識別,則忽略該字段,消息中的其它字段正常處理。

​ 由於optional字段的特性,不少接口在升級版本中都把後來添加的字段都統一的設置爲optional字段,這樣老的版本無需升級程序也能夠正常的與新的軟件進行通訊,只不過新的字段沒法識別而已,由於並非每一個節點都須要新的功能,所以能夠作到按需升級和平滑過渡。

 

 

Repeated

​ 表示該字段能夠包含0~N個元素。其特性和optional同樣,可是每一次能夠包含多個值。能夠看做是在傳遞一個數組的值。

若是沒有給optional和repeated字段賦值,那麼字段是不會出如今序列化後的數據中的。

 

 

 

二、數據類型

 

 

 

 

 

三、PHP示例應用

 

 

1)、編寫proto文件,結構化數據被稱爲 Message

vim user.proto

syntax = "proto3";

message userInfo{

​ int32 id = 1;

​ string name = 2;

}

 

 

2)、編譯成目標語言類文件(PHP)


/usr/local/protobuf/bin/protoc user.proto --php_out=/pb/php/

 

 

3)、PHP調用

require(…);

$pbUserInfo = new userInfo();

$pbUserInfo->setId(1);

$pbUserInfo->setName("echo");

$pbRs = $pbUserInfo->encode();

 

 

四、序列化解析

Protobuf消息由字段(field)構成,每一個字段有其規則(rule)、數據類型(type)、字段名(name)、tag,以及選項(option)。序列化時,消息字段會按照tag順序,以key+val的格式,編碼成二進制數據。

即一個消息就是多個字段的序列拼接成的一個二進制字節流,這種方式就像Key-Value的方式。但這種方式組織的數據並不須要額外的分隔符來劃分數據,因此其能夠減低序列化結果的大小。

 

 

 

Protobuf消息序列化以後,會產生二進制數據。這些數據(精確到bit)按照含義不一樣,能夠劃分爲6個部分:MSB flag、tag、編碼後數據類型(wire type)、長度(length)、字段值(value)、以及填充(padding)

 

1)、key-value

value

value根據不一樣的類型採用的編碼方式也不一樣,若是是整型,採用二進制表示;若是是字符,會直接原樣寫入文件或者字符串(即不編碼)。

key是以Varint編碼存儲

一個message的key由兩部分組成,一部分是在定義消息時對字段的編號(field_num),另外一部分是字段類型(wire_type)。

key = tag << 3 | wire_type。也就是說,key的第一個字節後3個位是wire type,剩下的位是tag值。

因此,第一個字節還剩下4個二進制位(8-1-3)用於表示tag的值,若是tag值大於15則需增長字節來表示。

由於只用3個二進制位表示wire type,因此最多隻能支持8種,目前有6種。Protobuf支持豐富的數據類型,可是編碼以後,只剩下Varint(0)、64-bit(1)、Length-delimited(2)、satrt group(3)、end group(4)和32-bit(5)類型。

 

2)、wire Type


每種數據類型都有對應的wire_type:

Type Meaning Used For
0 Varint int32, int64, uint32, uint64, sint32, sint64, bool, enum
1 64-bit fixed64, sfixed64, double
2 Length-delimi string, bytes, embedded messages, packed repeated fields
3 Start group Groups (deprecated)
4 End group Groups (deprecated)
5 32-bit fixed32, sfixed32, float


3)、Varint


是一種緊湊的表示數字的方法。它用一個或多個字節來表示一個數字,值越小的數字使用越少的字節數。

Varint中的每一個 字節 的最高位 有特殊的含義,若是該位爲 1,表示後續的字節也是該數字的一部分,若是該位爲 0,則結束。其餘的 7 個 位都是用來表示數字。所以小於等於 127 的數字均可以用一個 byte 表示。大於等於 127 的數字,

好比 300,會用兩個字節來表示:

1010 1100 0000 0010

去掉兩個最高位MSB flag以後爲:

010 1100 **000 0010**

protobuf字節序是小端字節序,因此這個數字實際是

000 0010 010 1100(100101100 == 300)

因此用varint存儲一個int32的小數值,最可能是能夠節約3個字節。爲了用盡量節約字節編碼消息,Protobuf在多處都使用了Varint這種格式。好比數據類型裏的int3二、int64,以及tag值和後面將要解釋的length值,都使用Varint類型存儲。

Variant編碼也有兩個很差的地方:

 

4)、固定長度編碼(32-bit、64-bit)

第一,不利於表示大數。對於比較小的數來講,以0到127爲例,用Varint很划算。以浪費1bit和少許額外的計算爲代價,只要1個字節就能夠表示。可是對於比較大的數,就不划算了。以int32爲例,大於2^(4*7) - 1的數(每一個字節只有7個位用於存儲),須要用5個字節來表示。好比268435456 (2^28)

$pbUserInfo->setId(268435456);

08 80 80 80 80 01

也就是說,若是某個消息的某個int字段大部分時候都會取比較大的數,那麼這個字段使用Varint這種變長類型來編碼就沒什麼好處。對於這種狀況,Protobuf定義了64-bit和32-bit兩種定長編碼類型。使用64-bit編碼的數據類型包括fixed6四、sfixed64和double;使用32-bit編碼的數據類型包括fixed3二、sfixed32和float。以userInfo消息id字段(float)爲例:

syntax = "proto3";

message userInfo{

​ float id = 1;

​ string name = 2;

}

$pbUserInfo->setId(268435456);

0d 00 00 80 4d

 

5)、ZigZag


第二個缺點是不適合表示負數,

若是負數也使用這種方式表示就會出現一個問題,

int32老是須要5(+1,key佔1個)個字節,int64老是須要10個字節(加上KEY,1個字節)。

syntax = "proto3";

message userInfo{

​ int64 id = 1;

​ string name = 2;

}

$pbUserInfo->setId(-1);

以下圖所示(int64):

爲了克服這個缺陷,Protobuf提供了sint32和sint64兩種數據類型。若是某個消息的某個字段出現負數值的可能性比較大,那麼應該使用sint32或sint64。這兩種數據類型在編碼時,會先使用ZigZag編碼將負數映射成正數,而後再使用Varint編碼。

ZigZag編碼計算公式爲:

sint32

(n << 1) ^ (n >> 31)

sint64

(n << 1) ^ (n >> 63)

ZigZag編碼規則以下圖所示:

 

圖1

圖2

 

6)、Length-delimited


如前所述,64-bit和32-bit是定長編碼格式,長度固定。Varint是變長編碼格式,長度由字節的MSB(最高位)決定。Length-delimited編碼格式則會將數據的length也編碼進最終數據,使用Length-delimited編碼格式的數據類型包括string、bytes和自定義消息。

syntax = "proto3";

message userInfo{

​ int64 id = 1;

​ string name = 2;

}

$pbUserInfo->setName(「hello」);

12 05 68 65 6c 6c 6f

 

7)、repeated

前面討論的字段都是optional類型,最多隻有一個val,可是repeated修飾符,能夠有多個val。

message userInfo{

​ int64 id = 1;

​ string name = 2;

​ repeated int32 prop = 3;

}

$pbUserInfo->getProp(

)[] = 1;

$pbUserInfo->getProp()[] = 2;

$pbUserInfo->getProp()[] = 3;

序列化以後的數據以下圖所示:

18 01 18 02 18 03

repeated字段就是簡單的把每一個字段值依次序列化而已。

 

8)、packed

若是repeated字段包含的val比較多,那麼每一個val都帶上key是比較浪費的

message userInfo{

​ int64 id = 1;

​ string name = 2;

​ repeated int32 prop = 3 [packed=true];

}

序列化以後的數據以下圖所示:

1a 03 01 02 03

 

 

 

若是repeated字段設置了packed選項,則會使用Length-delimited格式來編碼字段值。

 

 

 

五、proto3和proto2區別

 

1)、proto文件中的第一行非空白非註釋行syntax = 「proto3"表示使用proto3的語法,不然默認使用proto2的語法

2)、字段移除required,將optional更名爲singular。若是不加repeated,默認就是singular的。

 

3)、語言增長了Go,Ruby,JavaNano等的支持將來還計劃支持PHP等

4)、移除了default選項在proto2中,可以使用default爲field指定默認值。在proto3中,field的默認值只依賴於field的類型,再也不可以被指定。當field的value爲默認值時,該field不會被序列化,可節省空間。不要依賴於字段的默認值的行爲,由於沒法區分是指定爲默認值,仍是未定義值。

5)、枚舉類型的第一個枚舉值必須是0,proto3中必須提供一個枚舉值爲0做爲枚舉的默認值。爲了和proto2兼容(proto2使用第一個枚舉值做爲默認值),所以規定一個枚舉值爲0。

6)、再也不支持group,proto2中已經不推薦使用group。proto3中再也不支持group。group能夠用embedded message來實現。

7)、再也不支持Extension,新增Any關鍵字proto3中再也不支持Extension, 除了用在custom option。

 

 

六、其餘

Any、oneOf、Maps、Packages、Json Mapping

END

相關文章
相關標籤/搜索