protobuf协议基础介绍
l Protocol Buffers简介
l 定义一个.proto文件
l Message的使用
l 消息的编码机制
l 使用时注意事项
什么是ProtocolBuffers?
l Google定义的一种序列化的协议格式;
l Google内部几乎所有的RPC调用及文件格式;
(据称当前google已经定义了12,183个.proto文件,共有48,162种不同的message类型。它们用于RPC系统或各种存储系统中进行数据的存储)
l 目标:
Ø 简单性
Ø 兼容性
Ø 高性能
XML与Protobuf的比较
易读性 <->二进制格式;
自描述语言 <->没有.proto文件根据就是无用的;
文件大<-> 文件小(3-10倍);
解析及序列化较慢<->快(20- 100倍);
.xsd(复杂)<->.proto(简单,无二义性);
访问简单<->访问容易;
示例如下:
<person> <name>John Doe</name> <email>jdoe@example.com</email> </person> (== 69 bytes, 510ms to parse) System.out.println(person.getElementsByTagName("name").getElementText()); System.out.println(person.getElementsByTagName("email").getElementText()); |
Person { name: "John Doe" email: "jdoe@example.com" } (== 28 bytes, 100200ns to parse) System.out.println(person.name()); System.out.println(person.email()); |
message示例
package tutorial; option java_package = "com.example.tutorial"; option java_outer_classname = "AddressBookProtos";
message Person { required string name = 1; required int32 id = 2; optional string email = 3; enum PhoneType { MOBILE = 0; HOME = 1; WORK = 2; } message PhoneNumber { required string number = 1; optional PhoneType type = 2 [default = HOME]; } repeated PhoneNumber phone = 4; } message AddressBook { repeated Person person = 1; } |
从.proto文件到运行时
在.proto文件中定义消息;
用protoc编译器将其编译成源代码
Ø C++
Ø Java
Ø Python
在代码中直接使用接口
通过网络进行传输或存储
Message的定义
在.proto文件中定义Message消息;
语法格式:message [message name] { … }
消息可以内嵌(枚举或消息)
将会被转化为其它语言;
消息的内容
每个消息的内容又包含如下的格式:
消息类型;
枚举类型:
Enum<name> {
Valuename = value;
}
域;
域的格式定义如下:
<rule><type> <name> = <id> {[options]}
域的修饰符 rules
Required
该值是必须要传的,具有唯一性。(msg.fieldname())
Optional
该值可以有零个或一个,可以查询其存在与否。(msg.has_fieldname())
Repeated
该值相当于一个数组或有序列表,查询时可取其长度。(msg.fieldname_size())
可以使用选项packed = true来进行高效的编码。
Required是必须的
在用required修饰符时一定要谨慎;
一旦域被required修饰,该值就必须要进行传递,在版本升级或兼容时可能存在问题;
Googe工程师不建议使用required修饰符;
域id(标识)
每个域都有唯一的标识(id) (1-2^29)
注:不可以使用其中的[19000-19999]的标识号, Protobuf协议实现中对这些进行了预留。
变量采用的是可变长的编码方式
[1,15]之内的标识号在编码的时候会占用一个字节。[16,2047]之内的标识号则占用2个字节。应该为那些频繁出现的消息元素保留[1,15]之内的标识号。
在二进制格式的数据中唯一标识该域
域的名字在数据编码时不会使用到,编码中完全采用id来进行域的标识。
选项,命名空间及消息导入
Options:
[default = value] -> 为该域设置一个默认值 (默认值是不需编码的)
如:
optional uint32 ad_bid_count = 4[default = 2];
[packed =false / true]->采用更紧凑的编码方式
如:
repeated int32 samples = 4[packed=true];
[deprecated =false/true]->标识该域是否已经被弃用
如:
optional int32 old_field = 6[deprecated=true];
[optimize_for= SPEED/CODE/LITE_RUNTIME]:影响代码生成
Package:
命名空间,影响java的包名及生成的类名;
如:packagecom.example.message
Import:
导入其它文件中的message
如:import “myfile/message1.proto”
Message的使用
从.proto到具体代码
Protoc编码器根据.proto文件产生约定语言对应的代码;
如:protoc -I=C:\protobuf\test\ --java_out=C:\protobuf\test\C:\protobuf\test\addressbook.proto
运行完上述命令后,会在C:\protobuf\test\目录下生成一个类文件,即com.example.tutorial.AddressBookProtos.java,该类中有关于People和AddressBook的类文件;
为消息设置具体的值
public static Person addPerson() { Person.Builder person = Person.newBuilder(); Person.PhoneNumber.Builder phoneNumber=Person.PhoneNumber.newBuilder(); person.setId(Integer.parseInt("123456")); person.setName("zhaozheng"); person.setEmail("zheng.zhaoz@alibaba-inc.com"); phoneNumber.setNumber("15926467660"); phoneNumber.setType(PhoneType.valueOf("MOBILE")); person.addPhone(phoneNumber); return person.build(); }
|
序列化及解析数据
序列化:
addressBook.build().writeTo(OutputStream); addressBook.build().toByteArray(); |
解析:
addressBook.mergeFrom(InputStream); addressBook.parseFrom(InputStream) |
获取消息的具体值
public void Print(AddressBook addressBook) { for (Person person : addressBook.getPersonList()) { System.out.println("Person ID: " + person.getId()); System.out.println(" Name: " + person.getName()); if (person.hasEmail()) { System.out.println(" E-mail address: " + person.getEmail()); } for (Person.PhoneNumber phoneNumber : person.getPhoneList()){ switch (phoneNumber.getType()) { case MOBILE: System.out.print(" Mobile phone #: "); break; case HOME: System.out.print(" Home phone #: "); break; case WORK: System.out.print(" Work phone #: "); break; } System.out.println(phoneNumber.getNumber()); } } } |
消息编码机制
l 一个简单的消息编码;
l 基于128的Varints;
l 消息结构
l 其它值类型
一个简单的消息编码
消息格式定义如下:
message Test1 { required int32 a = 1; } |
在一个应用程序中,创建了一个Test1消息,并将其中的a设置为150。序列化该消息将可以看到3个字节。
0896 01
如何将该消息序列化为该格式呢?
Varints是一种将整数采用1个或多个字节序列的方法。越小的数据需要采用更少的字节。
每个Byte的最高位(msb)是标志位,如果该位为1,表示该Byte后面还有其它Byte,如果该位为0,表示该Byte是最后一个Byte。每个Byte的低7位是用来存数值的位。
Varints方法用Litte-Endian(小端)字节序。
示例:
1:0000 0001
300:1010 1100 0000 0010
1010 1100 0000 0010 → 010 1100 000 0010 (去msb) 000 0010 010 1100 (反转) → 000 0010 ++ 010 1100 (拼接) → 100101100 (计算) → 256 + 32 + 8 + 4 = 300 |
消息的结构
每条消息(message)都是由一系列的key-value对组成的。
key由两部分组成,一部分是在定义消息时字段的编号(field_num),另一部分是字段的类型(wire_type)。
类型 | 含义 | 使用场景 |
0 | Varint | int32, int64, uint32, uint64, sint32, sint64, bool, enum |
1 | 64-bit | fixed64, sfixed64, double |
2 | Length-delimited | string, bytes, embedded messages, packed repeated fields |
3 | Start group | groups (deprecated) |
4 | End-group | groups (deprecated) |
5 | 32-bit | fixed32, sfixed32, float |
对于流消息中的每个key值,也是采用varint方式来表示其值的,计算格式如下(field_number << 3) | wire_type),也就是说最后的2位表示的是字段类型信息。
分析之前的示例编码
08
它采用varint的方式来存储key,其值为08,由于是丢弃了msb,所以它的表示如下:
0000 1000 ->000 1000(去msb) ->000 0001(向右移3位) |
将最低3位数据取开,并将剩余的bit向右移3位,将得到0001,即表示的是该域对应的标识号。
96 01 = 1001 0110 0000 0001 → 000 0001 ++ 001 0110 (丢弃msb 并按7 bits进行反转) → 10010110 → 2 + 4 + 16 + 128 = 150 |
消息编码-ZigZag
Int32来存储负整数时,会使得编码特别长;
有符号整型可以采用ZigZag机制进行编码;
ZigZag编码是将有符号整型映射成为无符号整型,对于绝对值小的负数将使用小的varint进行编码。
0 -> 0 -1-> 1 1 -> 2 -2-> 3 …… 2147483647 -> 4294967294 -2147483648 -> 4294967295 |
sint32类型的值的编码如下:
(n << 1) ^ (n >> 31) |
sint64类型的值的编码如下:
(n << 1) ^ (n >> 63) |
注意事项
使用message的建议:
在protobuf协议中切记message的兼容性是首要的;
只有在非常必要时,才使用required关键词;
对于常用的值可以选择域为1-15的标识号(有效编码)
根据可能出现的期望值选择合适的数据类型;
更新message的建议:
将域声明为repeated或optional时,设置一些默认值(向后兼容性);
不要随意更改域标识,不能循环使用域标识;
有一些数据类型是可以改变的;(如ints)
当修改default时,切记默认值是不进行编码的,但在.proto文件中设置;