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>John Doe</name>

    <email>[email protected]</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: "[email protected]

}

(== 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("[email protected]");

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

 


發佈了75 篇原創文章 · 獲贊 32 · 訪問量 80萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章