protobuf3 語法指南(轉)

[轉]Protobuf3 語法指南

目錄 [−]

  1. 定義一個消息類型
    1. 指定字段類型
    2. 分配標識號
    3. 指定字段規則
    4. 添加更多消息類型
    5. 添加註釋
    6. 保留標識符(Reserved)
    7. 從.proto文件生成了什麼?
  2. 標量數值類型
  3. 默認值
  4. 枚舉
  5. 使用其他消息類型
    1. 導入定義
    2. 使用proto2消息類型
  6. 嵌套類型
  7. 更新一個消息類型
  8. Any
  9. Oneof
    1. 使用Oneof
    2. Oneof 特性
    3. 向後兼容性問題
  10. Map
    1. 向後兼容性問題
  11. Package
    1. 包及名稱的解析
  12. 定義服務(Service)
  13. JSON 映射
  14. 選項
    1. 自定義選項
  15. 生成訪問類

以前我翻譯了 Protobuf2 語法指南,現在 千念飛羽把protobuf3的語法指南也翻譯了,我也轉載一下,讀者可以有個參考。 譯文地址是: Protobuf3語言指南

英文原文:
Language Guide (proto3)
中文出處:
Protobuf語言指南
[譯]Protobuf 語法指南
中文出處是proto2的譯文,proto3的英文出現後在原來基礎上增改了,水平有限,還請指正

這個指南描述瞭如何使用Protocol buffer 語言去描述你的protocol buffer 數據, 包括 .proto文件符號和如何從.proto文件生成類。包含了proto2版本的protocol buffer語言:對於老版本的proto3 符號,請見Proto2 Language Guide(以及中文譯本,抄了很多這裏的感謝下老版本的翻譯者)

本文是一個參考指南——如果要查看如何使用本文中描述的多個特性的循序漸進的例子,請在教程中查找需要的語言的教程。

定義一個消息類型

先來看一個非常簡單的例子。假設你想定義一個“搜索請求”的消息格式,每一個請求含有一個查詢字符串、你感興趣的查詢結果所在的頁數,以及每一頁多少條查詢結果。可以採用如下的方式來定義消息類型的.proto文件了:

1
2
3
4
5
6
7
syntax = "proto3";
 
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
  • 文件的第一行指定了你正在使用proto3語法:如果你沒有指定這個,編譯器會使用proto2。這個指定語法行必須是文件的非空非註釋的第一個行。
  • SearchRequest消息格式有3個字段,在消息中承載的數據分別對應於每一個字段。其中每個字段都有一個名字和一種類型。

指定字段類型

在上面的例子中,所有字段都是標量類型:兩個整型(page_number和result_per_page),一個string類型(query)。當然,你也可以爲字段指定其他的合成類型,包括枚舉(enumerations)或其他消息類型。

分配標識號

正如你所見,在消息定義中,每個字段都有唯一的一個數字標識符。這些標識符是用來在消息的二進制格式中識別各個字段的,一旦開始使用就不能夠再改變。注:[1,15]之內的標識號在編碼的時候會佔用一個字節。[16,2047]之內的標識號則佔用2個字節。所以應該爲那些頻繁出現的消息元素保留 [1,15]之內的標識號。切記:要爲將來有可能添加的、頻繁出現的標識號預留一些標識號。

最小的標識號可以從1開始,最大到2^29 - 1, or 536,870,911。不可以使用其中的[19000-19999]( (從FieldDescriptor::kFirstReservedNumber 到 FieldDescriptor::kLastReservedNumber))的標識號, Protobuf協議實現中對這些進行了預留。如果非要在.proto文件中使用這些預留標識號,編譯時就會報警。同樣你也不能使用早期保留的標識號。

指定字段規則

所指定的消息字段修飾符必須是如下之一:

  • singular:一個格式良好的消息應該有0個或者1個這種字段(但是不能超過1個)。
  • repeated:在一個格式良好的消息中,這種字段可以重複任意多次(包括0次)。重複的值的順序會被保留。

在proto3中,repeated的標量域默認情況蝦使用packed。

你可以瞭解更多的pakced屬性在Protocol Buffer 編碼

添加更多消息類型

在一個.proto文件中可以定義多個消息類型。在定義多個相關的消息的時候,這一點特別有用——例如,如果想定義與SearchResponse消息類型對應的回覆消息格式的話,你可以將它添加到相同的.proto文件中,如:

1
2
3
4
5
6
7
8
9
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
 
message SearchResponse {
...
}

添加註釋

向.proto文件添加註釋,可以使用C/C++/Java風格的雙斜槓(//) 語法格式,如:

1
2
3
4
5
message SearchRequest {
string query = 1;
int32 page_number = 2; // Which page number do we want?
int32 result_per_page = 3; // Number of results to return per page.
}

保留標識符(Reserved)

如果你通過刪除或者註釋所有域,以後的用戶在更新這個類型的時候可能重用這些標識號。如果你使用舊版本加載相同的.proto文件會導致嚴重的問題,包括數據損壞、隱私錯誤等等。現在有一種確保不會發生這種情況的方法就是爲字段tag(reserved name可能會JSON序列化的問題)指定reserved標識符,protocol buffer的編譯器會警告未來嘗試使用這些域標識符的用戶。

1
2
3
4
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}

注:不要在同一行reserved聲明中同時聲明域名字和tag number。

從.proto文件生成了什麼?

當用protocol buffer編譯器來運行.proto文件時,編譯器將生成所選擇語言的代碼,這些代碼可以操作在.proto文件中定義的消息類型,包括獲取、設置字段值,將消息序列化到一個輸出流中,以及從一個輸入流中解析消息。

  • 對C++來說,編譯器會爲每個.proto文件生成一個.h文件和一個.cc文件,.proto文件中的每一個消息有一個對應的類。
  • 對Java來說,編譯器爲每一個消息類型生成了一個.java文件,以及一個特殊的Builder類(該類是用來創建消息類接口的)。
  • 對Python來說,有點不太一樣——Python編譯器爲.proto文件中的每個消息類型生成一個含有靜態描述符的模塊,,該模塊與一個元類(metaclass)在運行時(runtime)被用來創建所需的Python數據訪問類。
  • 對go來說,編譯器會位每個消息類型生成了一個.pd.go文件。
  • 對於Ruby來說,編譯器會爲每個消息類型生成了一個.rb文件。
  • javaNano來說,編譯器輸出類似域java但是沒有Builder類
  • 對於Objective-C來說,編譯器會爲每個消息類型生成了一個pbobjc.h文件和pbobjcm文件,.proto文件中的每一個消息有一個對應的類。
  • 對於C#來說,編譯器會爲每個消息類型生成了一個.cs文件,.proto文件中的每一個消息有一個對應的類。
    你可以從如下的文檔鏈接中獲取每種語言更多API(proto3版本的內容很快就公佈)。API Reference

標量數值類型

一個標量消息字段可以含有一個如下的類型——該表格展示了定義於.proto文件中的類型,以及與之對應的、在自動生成的訪問類中定義的類型:

.proto TypeNotesC++ TypeJava TypePython Type[2]Go TypeRuby TypeC# TypePHP Type
double   double double float float64 Float double float
float   float float float float32 Float float float
int32 使用變長編碼,對於負值的效率很低,如果你的域有可能有負值,請使用sint64替代 int32 int int int32 Fixnum 或者 Bignum(根據需要) int integer
uint32 使用變長編碼 uint32 int int/long uint32 Fixnum 或者 Bignum(根據需要) uint integer
uint64 使用變長編碼 uint64 long int/long uint64 Bignum ulong integer/string
sint32 使用變長編碼,這些編碼在負值時比int32高效的多 int32 int int int32 Fixnum 或者 Bignum(根據需要) int integer
sint64 使用變長編碼,有符號的整型值。編碼時比通常的int64高效。 int64 long int/long int64 Bignum long integer/string
fixed32 總是4個字節,如果數值總是比總是比228大的話,這個類型會比uint32高效。 uint32 int int uint32 Fixnum 或者 Bignum(根據需要) uint integer
fixed64 總是8個字節,如果數值總是比總是比256大的話,這個類型會比uint64高效。 uint64 long int/long uint64 Bignum ulong integer/string
sfixed32 總是4個字節 int32 int int int32 Fixnum 或者 Bignum(根據需要) int integer
sfixed64 總是8個字節 int64 long int/long int64 Bignum long integer/string
bool   bool boolean bool bool TrueClass/FalseClass bool boolean
string 一個字符串必須是UTF-8編碼或者7-bit ASCII編碼的文本。 string String str/unicode string String (UTF-8) string string
bytes 可能包含任意順序的字節數據。 string ByteString str []byte String (ASCII-8BIT) ByteString string

你可以在文章Protocol Buffer 編碼中,找到更多“序列化消息時各種類型如何編碼”的信息。

  1. 在java中,無符號32位和64位整型被表示成他們的整型對應形式,最高位被儲存在標誌位中。
  2. 對於所有的情況,設定值會執行類型檢查以確保此值是有效。
  3. 64位或者無符號32位整型在解碼時被表示成爲ilong,但是在設置時可以使用int型值設定,在所有的情況下,值必須符合其設置其類型的要求。
  4. python中string被表示成在解碼時表示成unicode。但是一個ASCIIstring可以被表示成str類型。
  5. Integer在64位的機器上使用,string在32位機器上使用

默認值

當一個消息被解析的時候,如果被編碼的信息不包含一個特定的singular元素,被解析的對象鎖對應的域被設置位一個默認值,對於不同類型指定如下:

  • 對於string,默認是一個空string
  • 對於bytes,默認是一個空的bytes
  • 對於bool,默認是false
  • 對於數值類型,默認是0
  • 對於枚舉,默認是第一個定義的枚舉值,必須爲0;
  • 對於消息類型(message),域沒有被設置,確切的消息是根據語言確定的,詳見generated code guide

對於可重複域的默認值是空(通常情況下是對應語言中空列表)。

注:對於標量消息域,一旦消息被解析,就無法判斷域釋放被設置爲默認值(例如,例如boolean值是否被設置爲false)還是根本沒有被設置。你應該在定義你的消息類型時非常注意。例如,比如你不應該定義boolean的默認值false作爲任何行爲的觸發方式。也應該注意如果一個標量消息域被設置爲標誌位,這個值不應該被序列化傳輸。

查看generated code guide選擇你的語言的默認值的工作細節。

枚舉

當需要定義一個消息類型的時候,可能想爲一個字段指定某“預定義值序列”中的一個值。例如,假設要爲每一個SearchRequest消息添加一個 corpus字段,而corpus的值可能是UNIVERSAL,WEB,IMAGES,LOCAL,NEWS,PRODUCTS或VIDEO中的一個。 其實可以很容易地實現這一點:通過向消息定義中添加一個枚舉(enum)並且爲每個可能的值定義一個常量就可以了。

在下面的例子中,在消息格式中添加了一個叫做Corpus的枚舉類型——它含有所有可能的值 ——以及一個類型爲Corpus的字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;
}

如你所見,Corpus枚舉的第一個常量映射爲0:每個枚舉類型必須將其第一個類型映射爲0,這是因爲:

  • 必須有有一個0值,我們可以用這個0值作爲默認值。
  • 這個零值必須爲第一個元素,爲了兼容proto2語義,枚舉類的第一個值總是默認值。

你可以通過將不同的枚舉常量指定位相同的值。如果這樣做你需要將allow_alias設定位true,否則編譯器會在別名的地方產生一個錯誤信息。

1
2
3
4
5
6
7
8
9
10
11
enum EnumAllowingAlias {
option allow_alias = true;
UNKNOWN = 0;
STARTED = 1;
RUNNING = 1;
}
enum EnumNotAllowingAlias {
UNKNOWN = 0;
STARTED = 1;
// RUNNING = 1; // Uncommenting this line will cause a compile error inside Google and a warning message outside.
}

枚舉常量必須在32位整型值的範圍內。因爲enum值是使用可變編碼方式的,對負數不夠高效,因此不推薦在enum中使用負數。如上例所示,可以在 一個消息定義的內部或外部定義枚舉——這些枚舉可以在.proto文件中的任何消息定義裏重用。當然也可以在一個消息中聲明一個枚舉類型,而在另一個不同 的消息中使用它——採用MessageType.EnumType的語法格式。

當對一個使用了枚舉的.proto文件運行protocol buffer編譯器的時候,生成的代碼中將有一個對應的enum(對Java或C++來說),或者一個特殊的EnumDescriptor類(對 Python來說),它被用來在運行時生成的類中創建一系列的整型值符號常量(symbolic constants)。

在反序列化的過程中,無法識別的枚舉值會被保存在消息中,雖然這種表示方式需要依據所使用語言而定。在那些支持開放枚舉類型超出指定範圍之外的語言中(例如C++和Go),爲識別的值會被表示成所支持的整型。在使用封閉枚舉類型的語言中(Java),使用枚舉中的一個類型來表示未識別的值,並且可以使用所支持整型來訪問。在其他情況下,如果解析的消息被序列號,未識別的值將保持原樣。

關於如何在你的應用程序的消息中使用枚舉的更多信息,請查看所選擇的語言generated code guide

使用其他消息類型

你可以將其他消息類型用作字段類型。例如,假設在每一個SearchResponse消息中包含Result消息,此時可以在相同的.proto文件中定義一個Result消息類型,然後在SearchResponse消息中指定一個Result類型的字段,如:

1
2
3
4
5
6
7
8
9
message SearchResponse {
repeated Result results = 1;
}
 
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}

導入定義

在上面的例子中,Result消息類型與SearchResponse是定義在同一文件中的。如果想要使用的消息類型已經在其他.proto文件中已經定義過了呢?
你可以通過導入(importing)其他.proto文件中的定義來使用它們。要導入其他.proto文件的定義,你需要在你的文件中添加一個導入聲明,如:

1
import "myproject/other_protos.proto";

默認情況下你只能使用直接導入的.proto文件中的定義. 然而, 有時候你需要移動一個.proto文件到一個新的位置, 可以不直接移動.proto文件, 只需放入一個僞 .proto 文件在老的位置, 然後使用import public轉向新的位置。import public 依賴性會通過任意導入包含import public聲明的proto文件傳遞。例如:

1
2
// 這是新的proto
// All definitions are moved here
1
2
3
4
// 這是久的proto
// 這是所有客戶端正在導入的包
import public "new.proto";
import "other.proto";
1
2
3
// 客戶端proto
import "old.proto";
// 現在你可以使用新舊兩種包的proto定義了。

通過在編譯器命令行參數中使用-I/--proto_pathprotocal 編譯器會在指定目錄搜索要導入的文件。如果沒有給出標誌,編譯器會搜索編譯命令被調用的目錄。通常你只要指定proto_path標誌爲你的工程根目錄就好。並且指定好導入的正確名稱就好。

使用proto2消息類型

在你的proto3消息中導入proto2的消息類型也是可以的,反之亦然,然後proto2枚舉不可以直接在proto3的標識符中使用(如果僅僅在proto2消息中使用是可以的)。

嵌套類型

你可以在其他消息類型中定義、使用消息類型,在下面的例子中,Result消息就定義在SearchResponse消息內,如:

1
2
3
4
5
6
7
8
message SearchResponse {
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
repeated Result results = 1;
}

如果你想在它的父消息類型的外部重用這個消息類型,你需要以Parent.Type的形式使用它,如:

1
2
3
message SomeOtherMessage {
SearchResponse.Result result = 1;
}

當然,你也可以將消息嵌套任意多層,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
message Outer { // Level 0
message MiddleAA { // Level 1
message Inner { // Level 2
int64 ival = 1;
bool booly = 2;
}
}
message MiddleBB { // Level 1
message Inner { // Level 2
int32 ival = 1;
bool booly = 2;
}
}
}

更新一個消息類型

如果一個已有的消息格式已無法滿足新的需求——如,要在消息中添加一個額外的字段——但是同時舊版本寫的代碼仍然可用。不用擔心!更新消息而不破壞已有代碼是非常簡單的。在更新時只要記住以下的規則即可。

  • 不要更改任何已有的字段的數值標識。
  • 如果你增加新的字段,使用舊格式的字段仍然可以被你新產生的代碼所解析。你應該記住這些元素的默認值這樣你的新代碼就可以以適當的方式和舊代碼產生的數據交互。相似的,通過新代碼產生的消息也可以被舊代碼解析:只不過新的字段會被忽視掉。注意,未被識別的字段會在反序列化的過程中丟棄掉,所以如果消息再被傳遞給新的代碼,新的字段依然是不可用的(這和proto2中的行爲是不同的,在proto2中未定義的域依然會隨着消息被序列化)
  • 非required的字段可以移除——只要它們的標識號在新的消息類型中不再使用(更好的做法可能是重命名那個字段,例如在字段前添加“OBSOLETE_”前綴,那樣的話,使用的.proto文件的用戶將來就不會無意中重新使用了那些不該使用的標識號)。
  • int32, uint32, int64, uint64,和bool是全部兼容的,這意味着可以將這些類型中的一個轉換爲另外一個,而不會破壞向前、 向後的兼容性。如果解析出來的數字與對應的類型不相符,那麼結果就像在C++中對它進行了強制類型轉換一樣(例如,如果把一個64位數字當作int32來 讀取,那麼它就會被截斷爲32位的數字)。
  • sint32和sint64是互相兼容的,但是它們與其他整數類型不兼容。
  • string和bytes是兼容的——只要bytes是有效的UTF-8編碼。
  • 嵌套消息與bytes是兼容的——只要bytes包含該消息的一個編碼過的版本。
  • fixed32與sfixed32是兼容的,fixed64與sfixed64是兼容的。
  • 枚舉類型與int32,uint32,int64和uint64相兼容(注意如果值不相兼容則會被截斷),然而在客戶端反序列化之後他們可能會有不同的處理方式,例如,未識別的proto3枚舉類型會被保留在消息中,但是他的表示方式會依照語言而定。int類型的字段總會保留他們的

Any

Any類型消息允許你在沒有指定他們的.proto定義的情況下使用消息作爲一個嵌套類型。一個Any類型包括一個可以被序列化bytes類型的任意消息,以及一個URL作爲一個全局標識符和解析消息類型。爲了使用Any類型,你需要導入import google/protobuf/any.proto

1
2
3
4
5
6
import "google/protobuf/any.proto";
 
message ErrorStatus {
string message = 1;
repeated google.protobuf.Any details = 2;
}

對於給定的消息類型的默認類型URL是type.googleapis.com/packagename.messagename。

不同語言的實現會支持動態庫以線程安全的方式去幫助封裝或者解封裝Any值。例如在java中,Any類型會有特殊的pack()unpack()訪問器,在C++中會有PackFrom()UnpackTo()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Storing an arbitrary message type in Any.
NetworkErrorDetails details = ...;
ErrorStatus status;
status.add_details()->PackFrom(details);
 
// Reading an arbitrary message from Any.
ErrorStatus status = ...;
for (const Any& detail : status.details()) {
if (detail.Is<NetworkErrorDetails>()) {
NetworkErrorDetails network_error;
detail.UnpackTo(&network_error);
... processing network_error ...
}
}

目前,用於Any類型的動態庫仍在開發之中
如果你已經很熟悉proto2語法,使用Any替換擴展

Oneof

如果你的消息中有很多可選字段, 並且同時至多一個字段會被設置, 你可以加強這個行爲,使用oneof特性節省內存.

Oneof字段就像可選字段, 除了它們會共享內存, 至多一個字段會被設置。 設置其中一個字段會清除其它字段。 你可以使用case()或者WhichOneof() 方法檢查哪個oneof字段被設置, 看你使用什麼語言了.

使用Oneof

爲了在.proto定義Oneof字段, 你需要在名字前面加上oneof關鍵字, 比如下面例子的test_oneof:

1
2
3
4
5
6
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}

然後你可以增加oneof字段到 oneof 定義中. 你可以增加任意類型的字段, 但是不能使用repeated 關鍵字.

在產生的代碼中, oneof字段擁有同樣的 getters 和setters, 就像正常的可選字段一樣. 也有一個特殊的方法來檢查到底那個字段被設置. 你可以在相應的語言API指南中找到oneof API介紹.

Oneof 特性

  • 設置oneof會自動清楚其它oneof字段的值. 所以設置多次後,只有最後一次設置的字段有值.
1
2
3
4
5
SampleMessage message;
message.set_name("name");
CHECK(message.has_name());
message.mutable_sub_message(); // Will clear name field.
CHECK(!message.has_name());
  • 如果解析器遇到同一個oneof中有多個成員,只有最會一個會被解析成消息。
  • oneof不支持repeated.
  • 反射API對oneof 字段有效.
  • 如果使用C++,需確保代碼不會導致內存泄漏. 下面的代碼會崩潰, 因爲sub_message 已經通過set_name()刪除了
1
2
3
4
SampleMessage message;
SubMessage* sub_message = message.mutable_sub_message();
message.set_name("name"); // Will delete sub_message
sub_message->set_... // Crashes here
  • 在C++中,如果你使用Swap()兩個oneof消息,每個消息,兩個消息將擁有對方的值,例如在下面的例子中,msg1會擁有sub_message並且msg2會有name。
1
2
3
4
5
6
7
SampleMessage msg1;
msg1.set_name("name");
SampleMessage msg2;
msg2.mutable_sub_message();
msg1.swap(&msg2);
CHECK(msg1.has_sub_message());
CHECK(msg2.has_name());

向後兼容性問題

當增加或者刪除oneof字段時一定要小心. 如果檢查oneof的值返回None/NOT_SET, 它意味着oneof字段沒有被賦值或者在一個不同的版本中賦值了。 你不會知道是哪種情況,因爲沒有辦法判斷如果未識別的字段是一個oneof字段。

Tag 重用問題:

  • 將字段移入或移除oneof:在消息被序列號或者解析後,你也許會失去一些信息(有些字段也許會被清除)
  • 刪除一個字段或者加入一個字段:在消息被序列號或者解析後,這也許會清除你現在設置的oneof字段
  • 分離或者融合oneof:行爲與移動常規字段相似。

Map

如果你希望創建一個關聯映射,protocol buffer提供了一種快捷的語法:

1
map<key_type, value_type> map_field = N;

其中key_type可以是任意Integer或者string類型(所以,除了floating和bytes的任意標量類型都是可以的)value_type可以是任意類型。

例如,如果你希望創建一個project的映射,每個Projecct使用一個string作爲key,你可以像下面這樣定義:

1
map<string, Project> projects = 3;
  • Map的字段可以是repeated。
  • 序列化後的順序和map迭代器的順序是不確定的,所以你不要期望以固定順序處理Map
  • 當爲.proto文件產生生成文本格式的時候,map會按照key 的順序排序,數值化的key會按照數值排序。
  • 從序列化中解析或者融合時,如果有重複的key則後一個key不會被使用,當從文本格式中解析map時,如果存在重複的key。

生成map的API現在對於所有proto3支持的語言都可用了,你可以從API指南找到更多信息。

向後兼容性問題

map語法序列化後等同於如下內容,因此即使是不支持map語法的protocol buffer實現也是可以處理你的數據的:

1
2
3
4
5
6
message MapFieldEntry {
key_type key = 1;
value_type value = 2;
}
 
repeated MapFieldEntry map_field = N;

Package

當然可以爲.proto文件新增一個可選的package聲明符,用來防止不同的消息類型有命名衝突。如:

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

在其他的消息格式定義中可以使用包名+消息名的方式來定義域的類型,如:

1
2
3
4
5
message Foo {
...
required foo.bar.Open open = 1;
...
}

包的聲明符會根據使用語言的不同影響生成的代碼。

  • 對於C++,產生的類會被包裝在C++的命名空間中,如上例中的Open會被封裝在 foo::bar空間中; - 對於Java,包聲明符會變爲java的一個包,除非在.proto文件中提供了一個明確有java_package;
  • 對於 Python,這個包聲明符是被忽略的,因爲Python模塊是按照其在文件系統中的位置進行組織的。
  • 對於Go,包可以被用做Go包名稱,除非你顯式的提供一個option go_package在你的.proto文件中。
  • 對於Ruby,生成的類可以被包裝在內置的Ruby名稱空間中,轉換成Ruby所需的大小寫樣式 (首字母大寫;如果第一個符號不是一個字母,則使用PB_前綴),例如Open會在Foo::Bar名稱空間中。
  • 對於javaNano包會使用Java包,除非你在你的文件中顯式的提供一個option java_package。
  • 對於C#包可以轉換爲PascalCase後作爲名稱空間,除非你在你的文件中顯式的提供一個option csharp_namespace,例如,Open會在Foo.Bar名稱空間中

包及名稱的解析

Protocol buffer語言中類型名稱的解析與C++是一致的:首先從最內部開始查找,依次向外進行,每個包會被看作是其父類包的內部類。當然對於 (foo.bar.Baz)這樣以“.”分隔的意味着是從最外圍開始的。

ProtocolBuffer編譯器會解析.proto文件中定義的所有類型名。 對於不同語言的代碼生成器會知道如何來指向每個具體的類型,即使它們使用了不同的規則。

定義服務(Service)

如果想要將消息類型用在RPC(遠程方法調用)系統中,可以在.proto文件中定義一個RPC服務接口,protocol buffer編譯器將會根據所選擇的不同語言生成服務接口代碼及存根。如,想要定義一個RPC服務並具有一個方法,該方法能夠接收 SearchRequest並返回一個SearchResponse,此時可以在.proto文件中進行如下定義:

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

最直觀的使用protocol buffer的RPC系統是gRPC,一個由谷歌開發的語言和平臺中的開源的PRC系統,gRPC在使用protocl buffer時非常有效,如果使用特殊的protocol buffer插件可以直接爲您從.proto文件中產生相關的RPC代碼。

如果你不想使用gRPC,也可以使用protocol buffer用於自己的RPC實現,你可以從proto2語言指南中找到更多信息

還有一些第三方開發的PRC實現使用Protocol Buffer。參考第三方插件wiki查看這些實現的列表。

JSON 映射

Proto3 支持JSON的編碼規範,使他更容易在不同系統之間共享數據,在下表中逐個描述類型。

如果JSON編碼的數據丟失或者其本身就是null,這個數據會在解析成protocol buffer的時候被表示成默認值。如果一個字段在protocol buffer中表示爲默認值,體會在轉化成JSON的時候編碼的時候忽略掉以節省空間。具體實現可以提供在JSON編碼中可選的默認值。

proto3JSONJSON示例注意
message object {“fBar”: v, “g”: null, …} 產生JSON對象,消息字段名可以被映射成lowerCamelCase形式,並且成爲JSON對象鍵,null被接受併成爲對應字段的默認值
enum string “FOO_BAR” 枚舉值的名字在proto文件中被指定
map object {“k”: v, …} 所有的鍵都被轉換成string
repeated V array [v, …] null被視爲空列表
bool true, false true, false  
string string “Hello World!”  
bytes base64 string “YWJjMTIzIT8kKiYoKSctPUB+”  
int32, fixed32, uint32 number 1, -10, 0 JSON值會是一個十進制數,數值型或者string類型都會接受
int64, fixed64, uint64 string “1”, “-10” JSON值會是一個十進制數,數值型或者string類型都會接受
float, double number 1.1, -10.0, 0, “NaN”, “Infinity” JSON值會是一個數字或者一個指定的字符串如”NaN”,”infinity”或者”-Infinity”,數值型或者字符串都是可接受的,指數符號也可以接受
Any object {“@type”: “url”, “f”: v, … } 如果一個Any保留一個特上述的JSON映射,則它會轉換成一個如下形式:{"@type": xxx, "value": yyy}否則,該值會被轉換成一個JSON對象,@type字段會被插入所指定的確定的值
Timestamp string “1972-01-01T10:00:20.021Z” 使用RFC 339,其中生成的輸出將始終是Z-歸一化啊的,並且使用0,3,6或者9位小數
Duration string “1.000340012s”, “1s” 生成的輸出總是0,3,6或者9位小數,具體依賴於所需要的精度,接受所有可以轉換爲納秒級的精度
Struct object { … } 任意的JSON對象,見struct.proto
Wrapper types various types 2, “2”, “foo”, true, “true”, null, 0, … 包裝器在JSON中的表示方式類似於基本類型,但是允許nulll,並且在轉換的過程中保留null
FieldMask string “f.fooBar,h” 見fieldmask.proto
ListValue array [foo, bar, …]  
Value value   任意JSON值
NullValue null   JSON null

選項

定義.proto文件時能夠標註一系列的option。Option並不改變整個文件聲明的含義,但卻能夠影響特定環境下處理方式。完整的可用選項可以在google/protobuf/descriptor.proto找到。

一些選項是文件級別的,意味着它可以作用於最外範圍,不包含在任何消息內部、enum或服務定義中。一些選項是消息級別的,意味着它可以用在消息定義的內部。當然有些選項可以作用在域、enum類型、enum值、服務類型及服務方法中。到目前爲止,並沒有一種有效的選項能作用於所有的類型。

如下就是一些常用的選項:

  • java_package (文件選項) :這個選項表明生成java類所在的包。如果在.proto文件中沒有明確的聲明java_package,就採用默認的包名。當然了,默認方式產生的 java包名並不是最好的方式,按照應用名稱倒序方式進行排序的。如果不需要產生java代碼,則該選項將不起任何作用。如:
1
option java_package = "com.example.foo";
  • java_outer_classname (文件選項): 該選項表明想要生成Java類的名稱。如果在.proto文件中沒有明確的java_outer_classname定義,生成的class名稱將會根據.proto文件的名稱採用駝峯式的命名方式進行生成。如(foo_bar.proto生成的java類名爲FooBar.java),如果不生成java代碼,則該選項不起任何作用。如:
1
option java_outer_classname = "Ponycopter";
  • optimize_for(文件選項): 可以被設置爲 SPEED, CODE_SIZE,或者LITE_RUNTIME。這些值將通過如下的方式影響C++及java代碼的生成:
    • SPEED (default): protocol buffer編譯器將通過在消息類型上執行序列化、語法分析及其他通用的操作。這種代碼是最優的。
    • CODE_SIZE: protocol buffer編譯器將會產生最少量的類,通過共享或基於反射的代碼來實現序列化、語法分析及各種其它操作。採用該方式產生的代碼將比SPEED要少得多, 但是操作要相對慢些。當然實現的類及其對外的API與SPEED模式都是一樣的。這種方式經常用在一些包含大量的.proto文件而且並不盲目追求速度的 應用中。
    • LITE_RUNTIME: protocol buffer編譯器依賴於運行時核心類庫來生成代碼(即採用libprotobuf-lite 替代libprotobuf)。這種核心類庫由於忽略了一 些描述符及反射,要比全類庫小得多。這種模式經常在移動手機平臺應用多一些。編譯器採用該模式產生的方法實現與SPEED模式不相上下,產生的類通過實現 MessageLite接口,但它僅僅是Messager接口的一個子集。
1
option optimize_for = CODE_SIZE;
  • cc_enable_arenas(文件選項):對於C++產生的代碼啓用arena allocation
  • objc_class_prefix(文件選項):設置Objective-C類的前綴,添加到所有Objective-C從此.proto文件產生的類和枚舉類型。沒有默認值,所使用的前綴應該是蘋果推薦的3-5個大寫字符,注意2個字節的前綴是蘋果所保留的。
  • deprecated(字段選項):如果設置爲true則表示該字段已經被廢棄,並且不應該在新的代碼中使用。在大多數語言中沒有實際的意義。在java中,這回變成@Deprecated註釋,在未來,其他語言的代碼生成器也許會在字標識符中產生廢棄註釋,廢棄註釋會在編譯器嘗試使用該字段時發出警告。如果字段沒有被使用你也不希望有新用戶使用它,嘗試使用保留語句替換字段聲明。
1
int32 old_field = 6 [deprecated=true];

自定義選項

ProtocolBuffers允許自定義並使用選項。該功能應該屬於一個高級特性,對於大部分人是用不到的。如果你的確希望創建自己的選項,請參看 Proto2 Language Guide。注意創建自定義選項使用了拓展,拓展只在proto3中可用。

生成訪問類

可以通過定義好的.proto文件來生成Java,Python,C++, Ruby, JavaNano, Objective-C,或者C# 代碼,需要基於.proto文件運行protocol buffer編譯器protoc。如果你沒有安裝編譯器,下載安裝包並遵照README安裝。對於Go,你還需要安裝一個特殊的代碼生成器插件。你可以通過GitHub上的protobuf庫找到安裝過程

通過如下方式調用protocol編譯器:

1
protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR --go_out=DST_DIR --ruby_out=DST_DIR --javanano_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto
  • IMPORT_PATH聲明瞭一個.proto文件所在的解析import具體目錄。如果忽略該值,則使用當前目錄。如果有多個目錄則可以多次調用--proto_path,它們將會順序的被訪問並執行導入。-I=IMPORT_PATH是--proto_path的簡化形式。
  • 當然也可以提供一個或多個輸出路徑:
    • --cpp_out 在目標目錄DST_DIR中產生C++代碼,可以在C++代碼生成參考中查看更多。
    • --java_out 在目標目錄DST_DIR中產生Java代碼,可以在 Java代碼生成參考中查看更多。
    • --python_out 在目標目錄 DST_DIR 中產生Python代碼,可以在Python代碼生成參考中查看更多。
    • --go_out 在目標目錄 DST_DIR 中產生Go代碼,可以在GO代碼生成參考中查看更多。
    • --ruby_out在目標目錄 DST_DIR 中產生Ruby代碼,參考正在製作中。
    • --javanano_out在目標目錄DST_DIR中生成JavaNano,JavaNano代碼生成器有一系列的選項用於定製自定義生成器的輸出:你可以通過生成器的README查找更多信息,JavaNano參考正在製作中。
    • --objc_out在目標目錄DST_DIR中產生Object代碼,可以在Objective-C代碼生成參考中查看更多。
    • --csharp_out在目標目錄DST_DIR中產生Object代碼,可以在C#代碼生成參考中查看更多。
    • --php_out在目標目錄DST_DIR中產生Object代碼,可以在PHP代碼生成參考中查看更多。

作爲一個方便的拓展,如果DST_DIR以.zip或者.jar結尾,編譯器會將輸出寫到一個ZIP格式文件或者符合JAR標準的.jar文件中。注意如果輸出已經存在則會被覆蓋,編譯器還沒有智能到可以追加文件。

  • 你必須提議一個或多個.proto文件作爲輸入,多個.proto文件可以只指定一次。雖然文件路徑是相對於當前目錄的,每個文件必須位於其IMPORT_PATH下,以便每個文件可以確定其規範的名稱。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章