本文是對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的類型,和其他語言類型的對比如下:
修飾符
如果一個字段被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)值
- string類型的默認(rèn)值是空字符串
- bytes類型的默認(rèn)值是空字節(jié)
- bool類型的默認(rèn)值是false
- 數(shù)字類型的默認(rèn)值是0
- enum類型的默認(rèn)值是第一個定義的枚舉值
- message類型(對象,如上文的SearchRequest就是message類型)的默認(rèn)值與 語言 相關(guān)
- 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é)省空間。
選項
選項不對message的定義產(chǎn)生任何的效果,只會在一些特定的場景中起到作用,下面是一部分例子,完整的選項列表可以前往google/protobuf/descriptor.proto
查看(Java語言可以在jar包中找到):
-
option java_package = "com.example.foo";
編譯器為以此作為生成的Java類的包名,如果沒有該選項,則會以pb的package
作為包名。 -
option java_multiple_files = true;
該選項為true
時,生成的Java類將是包級別的,否則會在一個包裝類中。 -
option optimize_for = CODE_SIZE;
該選項會對生成的類產(chǎn)生影響,作用是根據(jù)指定的選項對代碼進(jìn)行不同方面的優(yōu)化。 -
int32 old_field = 6 [deprecated=true];
把字段標(biāo)為過時的。
Java例子
最后,用Java寫了一個簡單的例子:Github
謝謝。