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