protobuf协议基础介绍

Protocol Buffers

介绍主题

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>JohnDoe</name>

<email>jdoe@example.com</email>

</person>

(==69bytes,5­10mstoparse)

System.out.println(person.getElementsByTagName("name").getElementText());

System.out.println(person.getElementsByTagName("email").getElementText());

Person{

name:"JohnDoe"

email:"jdoe@example.com"

}

(==28bytes,100­200nstoparse)

System.out.println(person.name());

System.out.println(person.email());

message示例

package tutorial;

option java_package = "com.example.tutorial";

option java_outer_classname = "AddressBookProtos";

messagePerson{

requiredstringname=1;

requiredint32id=2;

optionalstringemail=3;

enumPhoneType{

MOBILE=0;

HOME=1;

WORK=2;

}

messagePhoneNumber{

requiredstringnumber=1;

optionalPhoneTypetype=2[default=HOME];

}

repeatedPhoneNumberphone=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

如何将该消息序列化为该格式呢?

基于128的Varints

Varints是一种将整数采用1个或多个字节序列的方法。越小的数据需要采用更少的字节。

每个Byte的最高位(msb)是标志位,如果该位为1,表示该Byte后面还有其它Byte,如果该位为0,表示该Byte是最后一个Byte。每个Byte的低7位是用来存数值的位。

Varints方法用Litte-Endian(小端)字节序。

示例:

1:0000 0001

300:1010 1100 00000010

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文件中设置;


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值