Protobuf3學(xué)習(xí)筆記

本文是對Protobuf3(以下簡稱pb)官方文檔的學(xué)習(xí)筆記,大部分示例摘自官方。

原文:https://developers.google.com/protocol-buffers/docs/proto3

一個簡單的例子

syntax = "proto3";

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}

版本號

對于一個pb文件而言,文件首個非空、非注釋的行必須注明pb的版本,即syntax = "proto3";,否則默認(rèn)版本是proto2。

Message

一個message類型看上去很像一個Java class,由多個字段組成。每一個字段都由類型、名稱組成,位于等號右邊的值不是字段默認(rèn)值,而是數(shù)字標(biāo)簽,可以理解為字段身份的標(biāo)識符,類似于數(shù)據(jù)庫中的主鍵,不可重復(fù),標(biāo)識符用于在編譯后的二進(jìn)制消息格式中對字段進(jìn)行識別,一旦你的pb消息投入使用,字段的標(biāo)識就不應(yīng)該再改變。數(shù)字標(biāo)簽的范圍是[1, 536870911],其中19000~19999是保留數(shù)字。

類型

每個字段的類型(int32,string)都是scalar的類型,和其他語言類型的對比如下:

類型對比.png

修飾符

如果一個字段被repeated修飾,則表示它是一個列表類型的字段,如下所示:

...
message SearchRequest {
  repeated string args = 1 // 等價于java中的List<string> args
}

如果你希望可以預(yù)留一些數(shù)字標(biāo)簽或者字段可以使用reserved修飾符:

message Foo {
  reserved 2, 15, 9 to 11;
  reserved "foo", "bar";
  string foo = 3 // 編譯報錯,因為‘foo’已經(jīng)被標(biāo)為保留字段
}

默認(rèn)值

  1. string類型的默認(rèn)值是空字符串
  2. bytes類型的默認(rèn)值是空字節(jié)
  3. bool類型的默認(rèn)值是false
  4. 數(shù)字類型的默認(rèn)值是0
  5. enum類型的默認(rèn)值是第一個定義的枚舉值
  6. message類型(對象,如上文的SearchRequest就是message類型)的默認(rèn)值與 語言 相關(guān)
  7. repeated修飾的字段默認(rèn)值是空列表

如果一個字段的值等于默認(rèn)值(如bool類型的字段設(shè)為false),那么它將不會被序列化,這樣的設(shè)計是為了節(jié)省流量。

枚舉

每個枚舉值有對應(yīng)的數(shù)值,數(shù)值不一定是連續(xù)的。第一個枚舉值的數(shù)值必須是0且至少有一個枚舉值,否則編譯報錯。編譯后編譯器會為你生成對應(yīng)語言的枚舉類。

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
  enum Corpus {
    UNIVERSAL = 0;
    WEB = 1;
    IMAGES = 2;
    LOCAL = 3;
    NEWS = 4;
    PRODUCTS = 5;
    VIDEO = 6;
  }
  Corpus corpus = 4;
}

一個數(shù)值可以對應(yīng)多個枚舉值,必須標(biāo)明option allow_alias = true;

enum EnumAllowingAlias {
  option allow_alias = true;
  UNKNOWN = 0;
  STARTED = 1;
  RUNNING = 1;
}

可以使用MessageType.EnumType的形式引用定義在其它message類型中的枚舉。

由于編碼原因,出于效率考慮,官方不推薦使用負(fù)數(shù)作為枚舉值的數(shù)值。

使用其它的message類型

除了上述基本類型,一個字段的類型也可以是其它的message類型:

message SearchResponse {
  repeated Result results = 1;
}

message Result {
  string url = 1;
  string title = 2;
  repeated string snippets = 3;
}

從上面的例子可以看到,一個.proto文件中可以定義多個message。我們也可以引用定義在其它文件中的message:

import "myproject/other_protos.proto"; // 這樣就可以引用在other_protos.proto文件中定義的message

不能導(dǎo)入不使用的.proto文件。

import還有一種特殊的語法,先看下面的例子:

// new.proto
// 原來在old.proto文件中的定義移到這里
// old.proto
import public "new.proto"; // 把引用傳遞給上層使用方
import "other.proto"; // 引用old.proto本身使用的定義
// client.proto
import "old.proto";
// 此處可以引用old.proto和new.proto中的定義,但不能使用other.proto中的定義

從這個例子中可以看到import關(guān)鍵字導(dǎo)入的定義僅在當(dāng)前文件有效,不能被上層使用方引用(client.proto無法使用other.proto中的定義),而import public關(guān)鍵字導(dǎo)入的定義可以被上層使用方引用(client.proto可以使用new.proto中的定義),import public的功能可以看作是import的超集,在import的功能上還具有傳遞引用的作用。

嵌套類型

你可以在一個message類型中定義另一個message類型,并且可以一直嵌套下去,類似Java的內(nèi)部類:

message SearchResponse {
  message Result {
    string url = 1;
    string title = 2;
    repeated string snippets = 3;
  }
  repeated Result results = 1;
}

可以使用Parent.Type的形式引用嵌套的message:

message SomeOtherMessage {
  SearchResponse.Result result = 1;
}

Any

Any類型允許包裝任意的message類型:

import "google/protobuf/any.proto";

message Response {
    google.protobuf.Any data = 1;
}

可以通過pack()unpack()(方法名在不同的語言中可能不同)方法裝箱/拆箱,以下是Java的例子:

People people = People.newBuilder().setName("proto").setAge(1).build();
// protoc編譯后生成的message類
Response r = Response.newBuilder().setData(Any.pack(people)).build();
// 使用Response包裝people

System.out.println(r.getData().getTypeUrl());
// type.googleapis.com/example.protobuf.people.People
System.out.println(r.getData().unpack(People.class).getName());
// proto

Any對包裝的類型會生成一個URL,默認(rèn)是type.googleapis.com/packagename.messagename(在Java中可以通過這個特性進(jìn)行反射操作)。

Oneof

如果你有一些字段同時最多只有一個能被設(shè)置,可以使用oneof關(guān)鍵字來實(shí)現(xiàn),任何一個字段被設(shè)置,其它字段會自動被清空(被設(shè)為默認(rèn)值):

message SampleMessage {
  oneof test_oneof {
    string name = 4;
    SubMessage sub_message = 9;
  }
}

oneof塊中的字段不支持repeated。

Maps

pb中也可以使用map類型(官方并不認(rèn)為是一種類型,此處稱之為類型僅便于理解),絕大多數(shù)scalar類型都可以作為key,除了浮點(diǎn)型和bytes,枚舉型也不能作為key,value可以是除了map以外的任意類型:

// map<key_type, value_type> map_field = N;
map<string, Project> projects = 3;

map類型字段不支持repeated,value的順序是不定的。

map其實(shí)是一種語法糖,它等價于以下形式:

message MapFieldEntry {
  key_type key = 1;
  value_type value = 2;
}

repeated MapFieldEntry map_field = N;

你可以用指定package以避免類型命名沖突:

package foo.bar;
message Open { ... }

然后可以用類型的全限定名來引用它:

message Foo {
  ...
  foo.bar.Open open = 1;
  ...
}

指定包名后,會對生成的代碼產(chǎn)生影響,以Java為例,生成的類會以你指定的package作為包名。

JSON映射

pb支持和JSON互相轉(zhuǎn)換。如果一個字段不存在JSON數(shù)據(jù)中或者為null,那么pb中會被賦為該字段的默認(rèn)值,反之,如果一個字段在pb中是默認(rèn)值,那么不會寫到JSON數(shù)據(jù)中以節(jié)省空間。

PB<>JSON

選項

選項不對message的定義產(chǎn)生任何的效果,只會在一些特定的場景中起到作用,下面是一部分例子,完整的選項列表可以前往google/protobuf/descriptor.proto查看(Java語言可以在jar包中找到):

  1. option java_package = "com.example.foo"; 編譯器為以此作為生成的Java類的包名,如果沒有該選項,則會以pb的package作為包名。
  2. option java_multiple_files = true; 該選項為true時,生成的Java類將是包級別的,否則會在一個包裝類中。
  3. option optimize_for = CODE_SIZE; 該選項會對生成的類產(chǎn)生影響,作用是根據(jù)指定的選項對代碼進(jìn)行不同方面的優(yōu)化。
  4. int32 old_field = 6 [deprecated=true]; 把字段標(biāo)為過時的。

Java例子

最后,用Java寫了一個簡單的例子:Github

謝謝。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 由于工程項目中擬采用一種簡便高效的數(shù)據(jù)交換格式,百度了一下發(fā)現(xiàn)除了采用 xml、JSON 還有 ProtoBuf(...
    黃海佳閱讀 48,874評論 1 23
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,991評論 19 139
  • 什么是protocal buffer? protocal buffer 以下簡稱protobuf是google 的...
    碎念楓子閱讀 3,855評論 8 10
  • 1840年,英國對中國發(fā)動了鴉片戰(zhàn)爭。號稱亞洲第一強(qiáng)國的中國被打得一敗涂地,清朝出動了比英軍多得多的軍隊,但最終還...
    悠悠千古事閱讀 851評論 0 1
  • 我和榕樹閱讀 128評論 0 0