protobuf协议基础介绍

13年前
介绍主题

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, 5­10ms 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, 100­200ns 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

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

 

基于128的Varints

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