protobuf那些事(一)

protobuf是什麼

protobuf是Google建立的,是一種語言無關、平臺無關、可擴展的序列化結構化數據的方法,可用於通訊協議、數據存儲等。
在序列化結構化數據的機制中,是靈活、高效、自動化的。相比於XML,更小、更快、更簡單。java

爲何不直接用XML

在序列化方面,protobuf具備如下優點:編程

  1. 更簡單
  2. 數據體積小3-10倍
  3. 反序列化速度快20-100倍
  4. 生成更容易以編程方式使用的數據訪問類

舉個例子:我們爲一個具備nameemaiperson建模。
在XML中,咱們是這樣寫的:json

<person>
    <name>John Doe</name>
    <email>jdoe@example.com</email>
</person>

在protocol buffers中,咱們是這樣寫的:數組

# Textual representation of a protocol buffer.
# This is *not* the binary format used on the wire.
person {
  name: "John Doe"
  email: "jdoe@example.com"
}

從性能上看,protocol buffers通過編碼後,用二進制的方式傳輸,只要可能有28個字節長,且須要大約100-200納秒來解析。即使刪除空白,那麼XML至少是69字節長,解析大約須要5000-10000納秒。
在編碼方面,protocol buffers也是更簡潔的。
protocol buffers讀取是這樣的:ide

cout << "Name: " << person.name() << endl;
cout << "E-mail: " << person.email() << endl;

XML讀取是這樣的:性能

cout << "Name: "
       << person.getElementsByTagName("name")->item(0)->innerText()
       << endl;
cout << "E-mail: "
       << person.getElementsByTagName("email")->item(0)->innerText()
       << endl;

固然,相比於protobuf,XML依然也有本身的優點的,好比基於文本的使用標記(例如 HTML)建模。並且XML對於咱們來講,是可讀的,可編輯的,它具備自解釋性。protobuf是二進制的形式,因此只有.proto定義,咱們才能夠進行解讀。測試

準備工做

我ide是idea的,安裝了兩個插件,GenProtobuf是用來生成java文件的,Protobuf Support是用來高亮、語法檢查等。
image.png
GEnProtobuf設置,打開菜單:
image.png
設置protoc.exe的路徑,以及生成的文件路徑
image.png
在idea點擊.proto文件,右鍵,選擇quick gen protobuf rules,就能夠在咱們指定的地方生成java文件,若是選擇quick gen protobuf here就會在當前目錄生成java文件。
image.png
pom文件以下:ui

<dependencies>
    <dependency>
        <groupId>com.google.protobuf</groupId>
        <artifactId>protobuf-java</artifactId>
        <version>3.9.1</version>
    </dependency>
    <dependency>
        <groupId>com.google.protobuf</groupId>
        <artifactId>protobuf-java-util</artifactId>
        <version>3.9.1</version>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.9</version>
        <scope>test</scope>
    </dependency>
</dependencies>

proto3

官方雖然將繼續支持proto2,但鼓勵新代碼使用Proto3,由於它更容易使用,支持更多的語言。google

一個簡單的message

Person.proto:編碼

syntax = "proto3";//指定了用的是proto3語法,不寫默認proto2語法
package ch0;//定義proto的包名,能夠經過package防止命名衝突。
/*
Person包含name和age兩個屬性
*/
message Person {
    string name = 1;
    int32 age =2;
}

跟咱們java同樣,用///* ... */來註釋單行和一段。
文件的第一個非空、非註釋行,指定了使用了proto3語法,若是沒指定,默認是proto2語法。
message Person{}指定了這個Message的名稱是Person,裏面每一行都有三項,字段類型、字段名稱、字段編號。
字段類型與各個語言的關係圖以下:
image.png
第二個是字段名稱,名稱確定要惟一,這個沒什麼好說的。
repeated關鍵字,重複的意思,相似於數組或List。
第三個是字段編號,每一個字段都有一個唯一的編號。在上面例子中,假設name的值是張三,在轉換二進制的時候,不會是name:張三這種形式的,而是用字段編號1:張三這種形式。在解析二進制的時候,也是獲取1:張三,而後1跟上面Message對應取到name,再轉爲name,這樣二進制字節就會變短了。
這些字段編號,考慮到需求變化致使多個版本字段可能變更,因此儘可能不要修改字段編號,否則有可能1.0版本1對應的是name,1.1版本卻對應着age,系統就混亂了。因爲1到15的字段編號只須要一個字節進行編碼,因此用的比較頻繁的字段,建議都要這個範圍。
字段編號的範圍是1到2^29-1(536,870,911),中間19000到19999是做爲保留的數字,咱們不可以使用的,在編譯的時候就會報錯。並且,也不能使用以前的保留數字,這個後面會講字段刪除修改時要怎麼處理。
經過GenProtobuf生成的java文件,這邊沒有指定文件夾和類名,具體的文件生成規則後面講。爲了方便,後面我都指定文件夾和類名。
image.png
咱們能夠用生成的java類,調用name和age的set和get方法。

@Test
public void test1(){
    PersonOuterClass.Person.Builder person =  PersonOuterClass.Person.newBuilder();
    person.setAge(18);
    person.setName("張三");
    person.getAge();
    person.getName();
}

多個Message

一個.proto文件中定義多個Message。好比多個有關聯的Message,就能夠添加到相同的.proto:

syntax = "proto3";

package ch2;
option java_package = "com.example.ch2";
option java_outer_classname = "Animo";
message Cat{
    string name = 1;
    int32 age =2;
}

message Dog{
    string name = 1;
    int32 age =2;
}

測試代碼

@Test
public void test2(){
    Animo.Cat.Builder cat = Animo.Cat.newBuilder();
    cat.setAge(1);
    cat.setName("kitty");
    cat.getName();
    cat.getAge();

    Animo.Dog.Builder dog = Animo.Dog.newBuilder();
    dog.setAge(2);
    dog.setName("wangcai");
    dog.getName();
    dog.getAge();

}

合併後的java類實際上是同一個,只是經過不一樣的Builder來獲取。

保留字段

上面有提過,當咱們需求變動時,可能有不用的字段,那要怎麼處理呢?若是直接刪掉或者註釋掉,有可能其餘版本的應用或其餘應用會用到這個字段,那就會致使嚴重的問題,好比數據損壞、隱私bug等。所以,咱們就好保留這些不用的字段名稱和字段編號。
編譯器會提示咱們錯誤,好比2經過reserved標記爲保留的字段編號,在age使用的時候,就會提示了:
image.png
注意,不能在同一個保留語句中混合字段名和字段號。須要分開填寫。

枚舉

syntax = "proto3";
package ch4;
option java_package = "com.example.ch4";
option java_outer_classname = "MyEnum";

message Goods {
    string name = 1;
    enum Colors {
        red = 0;
        green = 1;
        blue = 2;
    }
    Goods.Colors color = 2;
    Sizes size = 3;
}

enum Sizes {
    option allow_alias = true;
    X = 0;
    XL = 1;
    XXL = 1;
}

用枚舉的時候,爲了與proto2兼容,必須有一個0值,且0值必須是第一個元素。
在上面的例子中,定義了兩個枚舉,一個是在Message中,一個是Message外,若是在其餘Message中,須要用Message的名稱加枚舉獲取,我這裏演示的是本身用本身的,因此能夠把Goods.去掉也是能夠的。枚舉的常數,必須在32位整數範圍內,不推薦使用負值。
能夠看到,Sizes有兩個值都是1,這個時候,須要設置allow_alias爲true。咱們看看測試代碼和運行結果:

@Test
public void test4() {
    MyEnum.Goods.Builder goods = MyEnum.Goods.newBuilder();
    goods.setColorValue(1);
    goods.setSizeValue(1);
    System.out.println(goods.getColor());
    System.out.println(goods.getColorValue());
    System.out.println(goods.getSize());
    System.out.println(goods.getSizeValue());
}

image.png

在反序列化期間,沒法識別的enum值將保留在Message中,不一樣的語言有不一樣的處理。好比C和Go,未識別的enum值只是做爲其基礎整數表示形式存儲。在Java中,未識別的enum值會標識沒法識別的值。在任何一種狀況下,若是消息被序列化,未被識別的值仍將與消息一塊兒序列化。
枚舉的保留值寫法跟保留字段同樣。

默認值

proto文件:

syntax = "proto3";
package ch5;
option java_package = "com.example.ch5";
option java_outer_classname = "MyDefault";

message DefaultValue {
    string name = 1;
    int32 age = 2;
    bytes bt = 3;
    bool bl = 4;
    Sizes size = 5;
}

enum Sizes {
    X = 0;
    XL = 1;
}

測試代碼:

@Test
public void test5() {
    MyDefault.DefaultValue.Builder builder = MyDefault.DefaultValue.newBuilder();

    System.out.println(builder.getName());
    System.out.println(builder.getAge());
    System.out.println(builder.getBt());
    System.out.println(builder.getBl());
    System.out.println(builder.getSize());
}

運行結果:
image.png
strings:空的字符串
bytes:長度爲0的ByteString
bools:false
numeric:0
enums:默認第一個

使用其餘Message

用其餘的Message類型做爲字段類型,就好像java的PO中引入了其餘的PO。

syntax = "proto3";
package ch6;
option java_package = "com.example.ch6";
option java_outer_classname = "MyOtherMsg";

message MyMsg {
    MyOther myOther = 1;
}

message MyOther {
    string name = 1;
}

測試代碼:

@Test
public void test6() {
    MyOtherMsg.MyMsg.Builder builder = MyOtherMsg.MyMsg.newBuilder();
    MyOtherMsg.MyOther.Builder myOther =MyOtherMsg.MyOther.newBuilder();
    myOther.setName("張三");
    builder.setMyOther(myOther);
    MyOtherMsg.MyOther myOther2 = builder.getMyOther();
    System.out.println(myOther2.getName());
}

運行結果:
image.png

導入其餘文件

上面的例子中,若是MyMsg和MyOther不在一個文件中呢,那就須要用import引入。
MyMsg.proto

syntax = "proto3";
package ch7;
option java_package = "com.example.ch7";
option java_outer_classname = "MyMsg2";
import "MyOther.proto";

message MyMsg {
    MyOther myOther = 1;
}

MyOther.proto

syntax = "proto3";
package ch7;
option java_package = "com.example.ch7";
option java_outer_classname = "MyOther2";

message MyOther {
    string name = 1;
}

測試代碼:

@Test
public void test7() {
    MyMsg2.MyMsg.Builder builder = MyMsg2.MyMsg.newBuilder();
    MyOther2.MyOther.Builder myOther =MyOther2.MyOther.newBuilder();
    myOther.setName("張三");
    builder.setMyOther(myOther);
    MyOther2.MyOther myOther2 = builder.getMyOther();
    System.out.println(myOther2.getName());
}

運行結果以下:
image.png

嵌套類型

也就是說在Message中定義了Message類型,好比下面在Nest中定義了Inner:

syntax = "proto3";
package ch8;
option java_package = "com.example.ch8";
option java_outer_classname = "NestType";

message Nest {
    message Inner{
        string name = 1;
    }
    Inner inner = 1;
}

測試代碼,有點像內部類

@Test
public void test8() {
    NestType.Nest.Builder builder = NestType.Nest.newBuilder();
    NestType.Nest.Inner.Builder inner = NestType.Nest.Inner.newBuilder();
    inner.setName("張三");
    builder.setInner(inner);
    NestType.Nest.Inner inner2 = builder.getInner();
    System.out.println(inner2.getName());
}

運行結果:
image.png

更新Message

更新message,主要是不能更改任何現有字段的字段編號,以及字段類型的兼容。
好比int3二、uint3二、int6四、uint6四、bool兼容,sint32和sint64兼容,string和bytes(若是bytes是合法的UTF-8)兼容,fixed32與sfixed32兼容,fixed64與sfixed64兼容,enum和int3二、uint3二、int6四、uint64兼容(值不適合會被截斷)。

未知字段

未知字段是protobuf序列化數據,表示解析器沒法識別的字段。例如,當舊的二進制代碼解析帶有新字段的新二進制代碼發送的數據時,這些新字段將成爲舊二進制代碼中的未知字段。好比咱們Message加了一個新的字段name,那這個name就是未知字段。

Any

Any字段直接用其餘Message類型,相似於泛型,須要導入 google/protobuf/any.proto。

syntax = "proto3";
package ch9;
option java_package = "com.example.ch9";
option java_outer_classname = "MyAny";
import "google/protobuf/any.proto";

message Any {
    string name = 1;
    google.protobuf.Any any = 2;
}

測試代碼,這個Person是第一個例子的。

@Test
public void test9() throws InvalidProtocolBufferException {
    PersonOuterClass.Person.Builder person =  PersonOuterClass.Person.newBuilder();
    person.setAge(18);
    person.setName("張三");

    MyAny.Any.Builder builder = MyAny.Any.newBuilder();
    builder.setAny(Any.pack(person.build()));
    builder.setName("李四");
    PersonOuterClass.Person person2 = builder.getAny().unpack(PersonOuterClass.Person.class);
    System.out.println(person2.getName()+"-"+person2.getAge());
    System.out.println(builder.getName());
}

運行結果以下:
image.png

Oneof

map

須要鍵值對能夠用map,其中key_type能夠是init或string 類型(排除floate和byte)。key_type不能是枚舉。

syntax = "proto3";
package ch11;
option java_package = "com.example.ch11";
option java_outer_classname = "MyMap";

message Map {
    map<string, string> filedMap = 1;
}

測試代碼:

@Test
public void test11() throws InvalidProtocolBufferException {
    MyMap.Map.Builder builder =   MyMap.Map.newBuilder();
    builder.putFiledMap("name","張三");
    builder.putFiledMap("age","18");
    System.out.println(builder.getFiledMapMap().get("name"));
    System.out.println(builder.getFiledMapMap().get("age"));
}

運行結果
image.png
map不能用repeated修飾。

JSON

Proto3支持JSON格式的規範編碼。
proto文件就用第一個例子的。
測試代碼:

@Test
public void test12() throws InvalidProtocolBufferException {
    JsonFormat.Printer printer = JsonFormat.printer();
    JsonFormat.Parser parser = JsonFormat.parser();

    PersonOuterClass.Person.Builder builder = PersonOuterClass.Person.newBuilder();
    builder.setAge(18);
    builder.setName("lilei");

    String jsonStr = printer.print(builder);
    System.out.println(jsonStr);

    PersonOuterClass.Person.Builder builder2 = PersonOuterClass.Person.newBuilder();
    parser.merge(jsonStr,builder2);
    PersonOuterClass.Person person = builder2.build();
    System.out.println(person);
}

運行結果:
image.png
對應關係:
image.png

Services定義

若是要在RPC(遠程過程調用)系統中使用Message,能夠在.proto文件中定義 RPC 服務接口,protocol buffer編譯器將根據所選語言生成服務接口代碼和 stubs。若是咱們定義一個RPC服務,入參是SearchRequest返回值是SearchResponse,就能夠這樣在.proto文件中定義它:

service SearchService {
  rpc Search (SearchRequest) returns (SearchResponse);
}

與protocol buffer一塊兒使用的最直接的RPC系統是gRPC:在谷歌開發的與語言和平臺無關的開放源碼RPC系統。gRPC在protocol buffer中工做得很是好,還能夠容許咱們使用特殊的protocol buffer編譯插件,直接從.proto文件中生成 RPC 相關的代碼。

Options

option用於對文件的聲明,google/protobuf/descriptor.proto定義了option的完整列表。
咱們看看上面的proto文件,第一個Person.proto,沒有用option對文件進行聲明,生成的目錄結果以下:
image.png
能夠看到的是,java文件在根目錄下,並且java類名是PersonOuterClass。
咱們看看Map那個例子的proto文件,跟Person.proto不一樣是下面兩個option定義:

option java_package = "com.example.ch11";
option java_outer_classname = "MyMap";

生成的目錄結果以下:
image.png目錄是由java_package指定的,類名是由java_outer_classname決定的。

相關文章
相關標籤/搜索