Protocol Buffers是谷歌定義的一種跨語言、跨平台、可擴展的數據傳輸及存儲的協議,因為將字段協議分別放在傳輸兩端,傳輸數據中只包含數據本身,不需要包含字段說明,所以傳輸數據量小,解析效率高。一條消息用protobuf序列化后的大小是json的10分之一。類似的序列化框架還有Thrift、avro。thrift和avro都提供rpc服務和序列化,而protocol buffer只是提供序列化功能。
二、安裝
安裝Google的protoc編譯器,這個工具可以把proto文件中定義的Message轉換為各種編程語言中的類。下載release版本直接編譯安裝。
https://github.com/google/protobuf/
3.1.0及以下版本,不支持PHP,需要安裝插件
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選項
三、應用
1、限定修飾符
Required
表示是一個必須字段,必須相對於發送方,在發送消息之前必須設置該字段的值,對於接收方,必須能夠識別該字段的意思。發送之前沒有設置required字段或者無法識別required字段都會引發編解碼異常,導致消息被丟棄。
Optional(singular)
表示是一個可選字段,在發送消息時,可以有選擇性的設置或者不設置該字段的值。對於接收方,如果能夠識別可選字段就進行相應的處理,如果無法識別,則忽略該字段,消息中的其它字段正常處理。
因為optional字段的特性,很多接口在升級版本中都把后來添加的字段都統一的設置為optional字段,這樣老的版本無需升級程序也可以正常的與新的軟件進行通信,只不過新的字段無法識別而已,因為並不是每個節點都需要新的功能,因此可以做到按需升級和平滑過渡。
Repeated
表示該字段可以包含0~N個元素。其特性和optional一樣,但是每一次可以包含多個值。可以看作是在傳遞一個數組的值。
如果沒有給optional和repeated字段賦值,那么字段是不會出現在序列化后的數據中的。
2、數據類型
3、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();
4、序列化解析
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這種格式。比如數據類型里的int32、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編碼的數據類型包括fixed64、sfixed64和double;使用32-bit編碼的數據類型包括fixed32、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格式來編碼字段值。
5、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。
6、其他
Any、oneOf、Maps、Packages、Json Mapping
END