天天看點

[翻譯]ProtoBuf 官方文檔(二)- 文法指引(proto2)文法指引(proto2)

翻譯查閱外網資料過程中遇到的比較優秀的文章和資料,一是作為技術參考以便日後查閱,二是訓練英文能力。

此文翻譯自 Protocol Buffers 官方文檔 Language Guide 部分

翻譯為意譯,不會照本宣科的字字對照翻譯

以下為原文内容翻譯

文法指引(proto2)

本指南介紹如何使用 protocol buffer 語言來構造 protocol buffer 資料,包括

.proto

檔案文法以及如何從

.proto

檔案生成資料通路類。它涵蓋了 protocol buffer 語言的 proto2 版本:有關較新的 proto3 文法的資訊,請參閱 Proto3 文法指引。

這是一個參考指南,有關使用本文檔中描述的許多功能的分步示例,請參閱各種語言對應的具體 教程。

定義一個 Message 類型

首先讓我們看一個非常簡單的例子。假設你要定義一個搜尋請求的 message 格式,其中每個搜尋請求都有一個查詢字元串,你感興趣的特定結果頁數(第幾頁)以及每頁的結果數。下面就是定義這個請求的 .proto 檔案:

message SearchRequest {
  required string query = 1;  // 查詢字元串
  optional int32 page_number = 2;  // 第幾頁
  optional int32 result_per_page = 3;  // 每頁的結果數
}
           

SearchRequest message 定義指定了三個字段(名稱/值對),每個字段對應着要包含在 message 中的資料,每個字段都有一個名稱和類型。

指定字段類型

在上面的示例中,所有字段都是 标量類型:兩個整數(

page_number

result_per_page

)和一個字元串(

query

)。但是,你還可以為字段指定複合類型,包括 枚舉 和其它的 message 類型。

配置設定字段編号

如你所見,message 定義中的每個字段都有唯一編号。這些數字以 message 二進制格式 辨別你的字段,并且一旦你的 message 被使用,這些編号就無法再更改。請注意,1 到 15 範圍内的字段編号需要一個位元組進行編碼,編碼結果将同時包含編号和類型(你可以在 Protocol Buffer 編碼 中找到更多相關資訊)。16 到 2047 範圍内的字段編号占用兩個位元組。是以,你應該為非常頻繁出現的 message 元素保留字段編号 1 到 15。請記住為将來可能添加的常用元素預留出一些空間。

你可以指定的最小字段數為 1,最大字段數為 229 - 1 或 536,870,911。你也不能使用 19000 到 19999 範圍内的數字(

FieldDescriptor::kFirstReservedNumber

FieldDescriptor::kLastReservedNumber

),因為它們是為 Protocol Buffers 的實作保留的 - 如果你使用這些保留數字之一,protocol buffer 編譯器會抱怨你的

.proto

。同樣,你也不能使用任何以前定義的 保留 字段編号。

譯者注:

“不能使用任何以前定義的保留字段編号” 指的是使用 reserved 關鍵字聲明的保留字段。

指定字段規則

你指定的 message 字段可以是下面幾種情況之一:

  • required: 格式良好的 message 必須包含該字段一次。
  • optional: 格式良好的 message 可以包含該字段零次或一次(不超過一次)。
  • repeated: 該字段可以在格式良好的消息中重複任意多次(包括零)。其中重複值的順序會被保留。

由于一些曆史原因,标量數字類型的 repeated 字段不能盡可能高效地編碼。新代碼應使用特殊選項 [packed = true] 來獲得更高效的編碼。例如:

你可以在 Protocol Buffer 編碼 中找到更多有關

packed

編碼的資訊。

對 required 的使用永遠都應該非常小心。如果你希望在某個時刻停止寫入或發送 required 字段,則将字段更改為可選字段将會有問題 - 舊讀者會認為沒有此字段的郵件不完整,可能會無意中拒絕或删除它們。你應該考慮為 buffers 編寫特定于應用程式的自定義驗證的例程。谷歌的一些工程師得出的結論是,使用 required 弊大于利;他們更喜歡隻使用 optional 和 repeated。但是,這種觀點并未普及。
譯者注:在 proto3 中已經為相容性徹底抛棄 required。

添加更多 message 類型

可以在單個 .proto 檔案中定義多種 message 類型。這在你需要定義多個相關 message 的時候會很有用 - 例如,如果要定義與搜尋請求相應的搜尋回複 message - SearchResponse message,則可以将其添加到相同的 .proto:

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

message SearchResponse {
 ...
}
           
組合 messages 會導緻膨脹雖然可以在單個 .proto 檔案中定義多種 messages 類型(例如 message,enum 和 service),但是當在單個檔案中定義了大量具有不同依賴關系的 messages 時,它也會導緻依賴性膨脹。建議每個 .proto 檔案包含盡可能少的 message 類型。

添加注釋

為你的 .proto 檔案添加注釋,可以使用 C/C++ 文法風格的注釋 // 和 。

/* SearchRequest represents a search query, with pagination options to
 * indicate which results to include in the response. */

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

Reserved 保留字段

如果你通過完全删除字段或将其注釋掉來更新 message 類型,則未來一些使用者在做他們的修改或更新時就可能會再次使用這些字段編号。如果以後加載相同

.proto

的舊版本,這可能會導緻一些嚴重問題,包括資料損壞,隐私錯誤等。確定不會發生這種情況的一種方法是指定已删除字段的字段編号(有時也需要指定名稱為保留狀态,英文名稱可能會導緻 JSON 序列化問題)為 “保留” 狀态。如果将來的任何使用者嘗試使用這些字段辨別符,protocol buffer 編譯器将會抱怨。

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

請注意,你不能在同一 "reserved" 語句中将字段名稱和字段編号混合在一起指定。

你的 .proto 檔案将生成什麼?

當你在

.proto

上運作 protocol buffer 編譯器時,編譯器将會生成所需語言的代碼,這些代碼可以操作檔案中描述的 message 類型,包括擷取和設定字段值、将 message 序列化為輸出流、以及從輸入流中解析出 message。

  • 對于 C++,編譯器從每個 .proto 生成一個 .h 和 .cc 檔案,其中包含檔案中描述的每種 message 類型對應的類。
  • 對于 Java,編譯器為每個 message 類型生成一個 .java 檔案(類),以及用于建立 message 類執行個體的特殊 Builder 類。
  • Python 有點不同 - Python 編譯器生成一個子產品,其中包含 .proto 中每種 message 類型的靜态描述符,然後與元類一起使用以建立必要的 Python 資料通路類。
  • 對于 Go,編譯器會生成一個 .pb.go 檔案,其中包含對應每種 message 類型的類型。

    你可以按照所選語言的教程了解更多有關各種語言使用 API ​​的資訊。有關更多 API 詳細資訊,請參閱相關的 API 參考。

标量值類型

标量 message 字段可以具有以下幾種類型之一 - 該表顯示 .proto 檔案中指定的類型,以及自動生成的類中的相應類型:

.proto Type Notes C++ Type Java Type Python Type[2] Go Type
double double double float *float64
float float float float *float32
int32 使用可變長度編碼。編碼負數的效率低 - 如果你的字段可能有負值,請改用 sint32 int32 int int *int32
int64 使用可變長度編碼。編碼負數的效率低 - 如果你的字段可能有負值,請改用 sint64 int64 long int/long[3] *int64
uint32 使用可變長度編碼 uint32 int[1] int/long[3] *uint32
uint64 使用可變長度編碼 uint64 long[1] int/long[3] *uint64
sint32 使用可變長度編碼。有符号的 int 值。這些比正常 int32 對負數能更有效地編碼 int32 int int *int32
sint64 使用可變長度編碼。有符号的 int 值。這些比正常 int64 對負數能更有效地編碼 int64 long int/long[3] *int64
fixed32 總是四個位元組。如果值通常大于 228,則比 uint32 更有效。 uint32 int[1] int/long[3] *uint32
fixed64 總是八個位元組。如果值通常大于 256,則比 uint64 更有效。 uint64 long[1] int/long[3] *uint64
sfixed32 總是四個位元組 int32 int int *int32
sfixed64 總是八個位元組 int64 long int/long[3] *int64
bool bool boolean bool *bool
string 字元串必須始終包含 UTF-8 編碼或 7 位 ASCII 文本 string String str/unicode[4] *string
bytes 可以包含任意位元組序列 string ByteString str []byte

在 Protocol Buffer 編碼 中你可以找到有關序列化 message 時這些類型如何被編碼的詳細資訊。

[1] 在 Java 中,無符号的 32 位和 64 位整數使用它們對應的帶符号表示,第一個 bit 位隻是簡單的存儲在符号位中。

[2] 在所有情況下,設定字段的值将執行類型檢查以確定其有效。

[3] 64 位或無符号 32 位整數在解碼時始終表示為 long,但如果在設定字段時給出 int,則可以為int。在所有情況下,該值必須适合設定時的類型。見 [2]。

[4] Python 字元串在解碼時表示為 unicode,但如果給出了 ASCII 字元串,則可以是 str(這條可能會發生變化)。

Optional 可選字段和預設值

如上所述,message 描述中的元素可以标記為可選 optional。格式良好的 message 可能包含也可能不包含被聲明為可選的元素。解析 message 時,如果 message 不包含 optional 元素,則解析對象中的相應字段将設定為該字段的預設值。可以将預設值指定為 message 描述的一部分。例如,假設你要為 SearchRequest 的 result_per_page 字段提供預設值10。

如果未為 optional 元素指定預設值,則使用特定于類型的預設值:對于字元串,預設值為空字元串。對于 bool,預設值為 false。對于數字類型,預設值為零。對于枚舉,預設值是枚舉類型定義中列出的第一個值。這意味着在将值添加到枚舉值清單的開頭時必須小心。有關如何安全的更改定義的指導,請參閱

更新 Message 類型

部分(見下面的

更新 message 類型

)。

枚舉 Enumerations

在定義 message 類型時,你可能希望其中一個字段隻有一個預定義的值清單。例如,假設你要為每個 SearchRequest 添加語料庫字段,其中語料庫可以是 UNIVERSAL,WEB,IMAGES,LOCAL,NEWS,PRODUCTS 或 VIDEO。你可以通過向 message 定義添加枚舉來簡單地執行此操作 - 具有枚舉類型的字段隻能将一組指定的常量作為其值(如果你嘗試提供不同的值,則解析器會将其視為一個未知的領域)。在下面的例子中,我們添加了一個名為 Corpus 的枚舉,其中包含所有可能的值,之後定義了一個類型為 Corpus 枚舉的字段:

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

你可以通過為不同的枚舉常量指定相同的值來定義别名。為此,你需要将 allow_alias 選項設定為true,否則 protocol 編譯器将在找到别名時生成錯誤消息。

enum EnumAllowingAlias {
  option allow_alias = true;
  UNKNOWN = 0;
  STARTED = 1;
  RUNNING = 1;
}
enum EnumNotAllowingAlias {
  UNKNOWN = 0;
  STARTED = 1;
  // RUNNING = 1;  // 取消此行注釋将導緻 Google 内部的編譯錯誤和外部的警告消息
}
           

枚舉器常量必須在 32 位整數範圍内。由于

enum

值線上上使用 varint encoding ,負值效率低,是以不推薦使用。你可以在 message 中定義 enums,如上例所示的那樣。或者将其定義在 message 外部 - 這樣這些

enum

就可以在

.proto

檔案中的任何 message 定義中重用。你還可以使用一個 message 中聲明的

enum

類型作為不同 message 中字段的類型,使用文法 MessageType.EnumType 來實作。

當你在使用

enum

.proto

上運作 protocol buffer 編譯器時,生成的代碼将具有相應的用于 Java 或 C++ 的

enum

,或者用于建立集合的 Python 的特殊

EnumDescriptor

類。運作時生成的類中具有整數值的符号常量。

有關如何在應用程式中使用 enums 的更多資訊,請參閱相關語言的 代碼生成指南

保留值

如果你通過完全删除枚舉條目或将其注釋掉來更新枚舉類型,則未來使用者可能在對 message 做出自己的修改或更新時重複使用這些數值。如果以後加載相同

.proto

的舊版本,這可能會導緻嚴重問題,包括資料損壞,隐私錯誤等。確定不會發生這種情況的一種方法是指定已删除字段的字段編号(有時也需要指定名稱為保留狀态,英文名稱可能會導緻 JSON 序列化問題)為 “保留” 狀态。如果将來的任何使用者嘗試使用這些字段辨別符,protocol buffer 編譯器将會抱怨。你可以使用

max

關鍵字指定保留的數值範圍一直到最大值。

enum Foo {
  reserved 2, 15, 9 to 11, 40 to max;
  reserved "FOO", "BAR";
}
           

請注意,你不能在同一 "reserved" 語句中将字段名稱和字段編号混合在一起指定。

使用其他 Message 類型

你可以使用其他 message 類型作為字段類型。例如,假設你希望在每個 SearchResponse 消息中包含 Result message - 為此,你可以在同一 .proto 中定義 Result message 類型,然後在SearchResponse 中指定 Result 類型的字段:

message SearchResponse {
  repeated Result result = 1;
}

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

導入定義 Importing Definitions

在上面的示例中,Result message 類型在與 SearchResponse 相同的檔案中定義 - 如果要用作字段類型的 message 類型已在另一個 .proto 檔案中定義,該怎麼辦?

你可以通過導入來使用其他 .proto 檔案中的定義。要導入另一個 .proto 的定義,可以在檔案頂部添加一個 import 語句:

預設情況下,你隻能使用直接導入的 .proto 檔案中的定義。但是,有時你可能需要将 .proto 檔案移動到新位置。現在,你可以在舊位置放置一個虛拟 .proto 檔案,以使用 import public 概念将所有導入轉發到新位置,而不是直接移動 .proto 檔案并在一次更改中更新所有調用點。導入包含 import public 語句的 proto 的任何人都可以傳遞依賴導入公共依賴項。例如:

// new.proto
// All definitions are moved here
           
// old.proto
// This is the proto that all clients are importing.
import public "new.proto";
import "other.proto";
           
// client.proto
import "old.proto";
// 你可以使用 old.proto 和 new.proto 中的定義,但無法使用 other.proto
           

使用指令 -I/--proto_path 讓 protocol 編譯器在指定的一組目錄中搜尋要導入的檔案。如果沒有給出這個指令選項,它将查找調用編譯器所在的目錄。通常,你應将 --proto_path 設定為項目的根目錄,并對所有導入使用完全限定名稱。

使用 proto3 Message 類型

可以導入 proto3 message 類型并在 proto2 message 中使用它們,反之亦然。但是,proto2 枚舉不能用于 proto3 文法。

嵌套類型 Nested Types

你可以在其他 message 類型中定義和使用 message 類型,如下例所示 - 此處結果消息在SearchResponse 消息中定義:

message SearchResponse {
  message Result {
    required string url = 1;
    optional string title = 2;
    repeated string snippets = 3;
  }
  repeated Result result = 1;
}
           

如果要在其父消息類型之外重用此消息類型,請将其稱為 Parent.Type:

message SomeOtherMessage {
  optional SearchResponse.Result result = 1;
}
           

你可以根據需要深入的嵌套消息:

message Outer {                  // Level 0
  message MiddleAA {  // Level 1
    message Inner {   // Level 2
      required int64 ival = 1;
      optional bool  booly = 2;
    }
  }
  message MiddleBB {  // Level 1
    message Inner {   // Level 2
      required int32 ival = 1;
      optional bool  booly = 2;
    }
  }
}
           

Groups

請注意,此功能已棄用,在建立新消息類型時不應使用 - 請改用嵌套消息類型。

Groups 是在 message 定義中嵌套資訊的另一種方法。例如,指定包含許多結果的SearchResponse 的另一種方法如下:

message SearchResponse {
  repeated group Result = 1 {
    required string url = 2;
    optional string title = 3;
    repeated string snippets = 4;
  }
}
           

group 隻是将嵌套 message 類型和字段組合到單個聲明中。在你的代碼中,你可以将此消息視為具有名為

result

Result

類型字段(前一名稱轉換為小寫,以便它不與前者沖突)。是以,此示例完全等同于上面的

SearchResponse

,但 message 具有不同的編碼結果。

譯者注:

再次強調,此功能已棄用,這裡隻為盡可能保留原文内容。

更新 message 類型

如果現有的 message 類型不再滿足你的所有需求 - 例如,你希望 message 格式具有額外的字段 - 但你仍然希望使用舊格式建立代碼,請不要擔心!在不破壞任何現有代碼的情況下更新 message 類型非常簡單。請記住以下規則:

  • 請勿更改任何現有字段的字段編号。
  • 你添加的任何新字段都應該是

    optional

    repeated

    。這意味着使用“舊”消息格式的代碼序列化的任何消息都可以由新生成的代碼進行解析,因為它們不會缺少任何

    required

    元素。你應該為這些元素設定合理的

    預設值

    ,以便新代碼可以正确地與舊代碼生成的 message 進行互動。同樣,你的新代碼建立的 message 可以由舊代碼解析:舊的二進制檔案在解析時隻是忽略新字段。但是未丢棄這個新字段(未知字段),如果稍後序列化消息,則将新字段(未知字段)與其一起序列化 - 是以,如果将消息傳遞給新代碼,則新字段仍然可用。
  • 隻要在更新的 message 類型中不再使用字段編号,就可以删除非必填字段。你可能希望重命名該字段,可能添加字首 "OBSOLETE_",或者将字段編号保留(Reserved),以便将來你的

    .proto

    的使用者不會不小心重用這個編号。
  • 隻要類型和編号保持不變,非必填字段就可以轉換為擴充 extensions,反之亦然。
  • int32

    uint32

    int64

    uint64

    bool

    都是相容的 - 這意味着你可以将字段從這些類型更改為另一種類型,而不會破壞向前或向後相容性。如果從中解析出一個不符合相應類型的數字,你将獲得與在 C++ 中将該數字轉換為該類型時相同的效果(例如,如果将 64 位數字作為 int32 讀取,它将被截斷為 32 位)。
  • sint32

    sint64

    彼此相容,但與其他整數類型不相容。
  • 隻要位元組是有效的 UTF-8,

    string

    bytes

    就是相容的。
  • 如果位元組包含 message 的編碼版本,則嵌入 message 與

    bytes

    相容。
  • fixed32

    sfixed32

    相容,

    fixed64

    sfixed64

    相容。
  • optional

    repeated

    相容。給定重複字段的序列化資料作為輸入,期望該字段為

    optional

    的用戶端将采用最後一個輸入值(如果它是基本類型字段)或合并所有輸入元素(如果它是 message 類型字段)。
  • 更改預設值通常是正常的,隻要你記住永遠不會通過網絡發送預設值。是以,如果程式接收到未設定特定字段的消息,則程式将看到該程式的協定版本中定義的預設值。它不會看到發件人代碼中定義的預設值。
  • enum

    int32

    uint32

    int64

    uint64

    相容(注意,如果它們不适合,值将被截斷),但要注意 message 反序列化時用戶端代碼對待它們将有所不同。值得注意的是,當 message 被反序列化時,将丢棄無法識别的

    enum

    值,這使得字段的

    has..

    通路器傳回 false 并且其 getter 傳回

    enum

    定義中列出的第一個值,或者如果指定了一個預設值則傳回預設值。在 repeated 枚舉字段的情況下,任何無法識别的值都将從清單中删除。但是,整數字段将始終保留其值。是以,在有可能接收超出範圍的枚舉值時,對整數更新為

    enum

    這一操作需要非常小心。
  • 在目前的 Java 和 C++ 實作中,當删除無法識别的

    enum

    值時,它們與其他未知字段一起存儲。請注意,如果此資料被序列化,然後由識别這些值的用戶端重新解析,則會導緻奇怪的行為。在 optional 可選字段的情況下,即使在反序列化原始 message 之後寫入新值,舊值仍然可以被用戶端識别。在 repeated 字段的情況下,舊值将出現在任何已識别和新添加的值之後,這意味着順序将不被保留。
  • 将單個

    optional

    值更改為 new

    oneof

    的成員是安全且二進制相容的。如果你确定沒有代碼一次設定多個,則将多個

    optional

    字段移動到新的

    oneof

    中可能是安全的。但是将任何字段移動到現有的

    oneof

    是不安全的。

擴充 Extensions

通過擴充,你可以聲明 message 中的一系列字段編号用于第三方擴充。擴充名是那些未由原始 .proto 檔案定義的字段的占位符。這允許通過使用這些字段編号來定義部分或全部字段進而将其它 .proto 檔案定義的字段添加到目前 message 定義中。我們來看一個例子:

message Foo {
  // ...
  extensions 100 to 199;
}
           

這表示 Foo 中的字段數 [100,199] 的範圍是為擴充保留的。其他使用者現在可以使用指定範圍内的字段編号在他們自己的 .proto 檔案中為 Foo 添加新字段,例如:

extend Foo {
  optional int32 bar = 126;
}
           

這會将名為 bar 且編号為 126 的字段添加到 Foo 的原始定義中。

譯者注:

第一段翻譯過來的語義實在是太别扭了(因為站在了被擴充字段所在的 .proto 檔案的角度來看待擴充),實際站在擴充字段所在的 .proto 檔案的角度-就是可以在自己的 .proto 檔案中擴充其他人定義的另一個 .proto 中的 message。

當使用者的 Foo 消息被編碼時,其格式與使用者在 Foo 中正常定義新字段的格式完全相同。但是,在應用程式代碼中通路擴充字段的方式與通路正常字段略有不同 - 生成的資料通路代碼具有用于處理擴充的特殊通路器。那麼,舉個例子,下面就是如何在 C++ 中設定 bar 的值:

Foo foo;
foo.SetExtension(bar, 15);
           

類似地,Foo 類定義模闆化通路器 HasExtension(),ClearExtension(),GetExtension(),MutableExtension() 和 AddExtension()。它們都具有與正常字段生成的通路器相比對的語義。有關使用擴充的更多資訊,請參閱所選語言的代碼生成參考。

請注意,擴充可以是任何字段類型,包括 message 類型,但不能是 oneofs 或 maps。

嵌套擴充

你可以在另一種 message 類型内部聲明擴充:

message Baz {
  extend Foo {
    optional int32 bar = 126;
  }
  ...
}
           

在這種情況下,通路此擴充的 C++ 代碼為:

Foo foo;
foo.SetExtension(Baz::bar, 15);
           

換句話說,唯一的影響是 bar 是在 Baz 的範圍内定義。

注意:

這是一個常見的混淆源:在一個 message 類型中聲明嵌套的擴充塊并不意味着外部類型和擴充類型之間存在任何關系。特别是,上面的例子并不意味着 Baz 是 Foo 的任何子類。這意味着符号欄是在 Baz 範圍内聲明的;它僅僅隻是一個靜态成員而已。

一種常見的模式是在擴充的字段類型範圍内定義擴充 - 例如,這裡是 Baz 類型的 Foo 擴充,其中擴充名被定義為 Baz 的一部分:

message Baz {
  extend Foo {
    optional Baz foo_ext = 127;
  }
  ...
}
           

譯者注:

這裡比較繞,實際上就是要對某個 message A 擴充一個字段 B(B 類型),那麼可以将這條擴充語句寫在 message B 的定義裡。

但是,并不是必須要在類型内才能定義該類型的擴充字段。你也可以這樣做:

message Baz {
  ...
}

// 該定義甚至可以移到另一個檔案中
extend Foo {
  optional Baz foo_baz_ext = 127;
}
           

實際上,這種文法可能是首選的,以避免混淆。如上所述,嵌套文法經常被不熟悉擴充的使用者誤認為是子類。

選擇擴充字段編号

確定兩個使用者不使用相同的字段編号向同一 message 類型添加擴充名非常重要 - 如果擴充名被意外解釋為錯誤類型,則可能導緻資料損壞。你可能需要考慮為項目定義擴充編号的約定以防止這種情況發生。

如果你的編号約定可能涉及那些具有非常大字段編号的擴充,則可以使用 max 關鍵字指定擴充範圍至編号最大值:

message Foo {
  extensions 1000 to max;
}
           

最大值為 229 - 1,或者 536,870,911。

與一般選擇字段編号時一樣,你的編号約定還需要避免 19000 到 19999 的字段編号(FieldDescriptor::kFirstReservedNumber 到 FieldDescriptor::kLastReservedNumber),因為它們是為 Protocol Buffers 實作保留的。你可以定義包含此範圍的擴充名範圍,但 protocol 編譯器不允許你使用這些編号定義實際擴充名。

Oneof

如果你的 message 包含許多可選字段,并且最多隻能同時設定其中一個字段,則可以使用 oneof 功能強制執行此行為并節省記憶體。

Oneof 字段類似于可選字段,除了 oneof 共享記憶體中的所有字段,并且最多隻能同時設定一個字段。設定 oneof 的任何成員會自動清除所有其他成員。你可以使用特殊的 case() 或 WhichOneof() 方法檢查 oneof 字段中目前是哪個值(如果有)被設定,具體方法取決于你選擇的語言。

使用 Oneof

要在 .proto 中定義 oneof,請使用 oneof 關鍵字,後跟你的 oneof 名稱,在本例中為 test_oneof:

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

然後,将 oneof 字段添加到 oneof 定義中。你可以添加任何類型的字段,但不能使用

required

optional

repeated

關鍵字。如果需要向 oneof 添加重複字段,可以使用包含重複字段的 message。

在生成的代碼中,oneof 字段與正常

optional

方法具有相同的 getter 和 setter。你還可以使用特殊方法檢查 oneof 中的值(如果有)。你可以在相關的 API 參考中找到有關所選語言的 oneof API的更多資訊。

Oneof 特性

  • 設定 oneof 字段将自動清除 oneof 的所有其他成員。是以,如果你設定了多個字段,則隻有你設定的最後一個字段仍然具有值。
SampleMessage message;
message.set_name("name");
CHECK(message.has_name());
message.mutable_sub_message();   // Will clear name field.
CHECK(!message.has_name());
           
  • 如果解析器遇到同一個 oneof 的多個成員,則在解析的消息中僅使用看到的最後一個成員。
  • oneof 不支援擴充
  • oneof 不能使用 repeated
  • 反射 API 适用于 oneof 字段
  • 如果你使用的是 C++,請確定你的代碼不會導緻記憶體崩潰。以下示例代碼将崩潰,因為已認證調用 set_name() 方法删除了 sub_message。
SampleMessage message;
SubMessage* sub_message = message.mutable_sub_message();
message.set_name("name");      // Will delete sub_message
sub_message->set_...            // Crashes here
           
  • 同樣在 C++中,如果你使用 Swap() 交換了兩條 oneofs 消息,則每條消息将以另一條消息的 oneof 執行個體結束:在下面的示例中,msg1 将具有 sub_message 而 msg2 将具有 name。
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 的值傳回 None/NOT_SET,則可能意味着 oneof 尚未設定或已設定為 oneof 的另一個字段。這種情況是無法區分的,因為無法知道未知字段是否是 oneof 成員。

标簽重用問題

  • 将 optional 可選字段移入或移出 oneof:在序列化和解析 message 後,你可能會丢失一些資訊(某些字段将被清除)。但是,你可以安全地将單個字段移動到新的 oneof 中,并且如果已知隻有一個字段被設定,則可以移動多個字段。
  • 删除 oneof 字段并将其重新添加回去:在序列化和解析 message 後,這可能會清除目前設定的 oneof 字段。
  • 拆分或合并 oneof:這與移動正常的 optional 字段有類似的問題。

Maps

如果要在資料定義中建立關聯映射,protocol buffers 提供了一種友善快捷的文法:

...其中

key_type

可以是任何整數或字元串類型(任何标量類型除浮點類型和

bytes

)。請注意,枚舉不是有效的

key_type

value_type

可以是除 map 之外的任何類型。

是以,舉個例子,如果要建立項目映射,其中每個 "Project" message 都與字元串鍵相關聯,則可以像下面這樣定義它:

生成的 map API 目前可用于所有 proto2 支援的語言。你可以在相關的 API 參考 中找到有關所選語言的 map API 的更多資訊。

Maps 特性

  • maps 不支援擴充
  • maps 不能是 repeated、optional、required
  • map 值的格式排序和 map 疊代排序未定義,是以你不能依賴于特定順序的 map 項
  • 生成 .proto 的文本格式時,maps 按鍵排序。數字鍵按數字排序
  • 當解析或合并時,如果有重複的 map 鍵,則使用最後看到的鍵。從文本格式解析 map 時,如果存在重複鍵,則解析可能會失敗

向後相容性

map 文法等效于以下内容,是以不支援 map 的 protocol buffers 實作仍可處理你的資料:

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

repeated MapFieldEntry map_field = N;
           

任何支援 maps 的 protocol buffers 實作都必須生成和接受上述定義所能接受的資料。

Packages

你可以将 optional 可選的包說明符添加到 .proto 檔案,以防止 protocol message 類型之間的名稱沖突。

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

然後,你可以在定義 message 類型的字段時使用包說明符:

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

package 影響生成的代碼的方式取決于你所選擇的語言:

  • 在 C++ 中,生成的類包含在 C++ 命名空間中。例如,Open 将位于命名空間 foo::bar 中。
  • 在 Java 中,除非在 .proto 檔案中明确提供選項 java_package,否則該包将用作 Java 包
  • 在 Python 中,package 指令被忽略,因為 Python 子產品是根據它們在檔案系統中的位置進行組織的

請注意,即使 package 指令不直接影響生成的代碼,但是例如在 Python 中,仍然強烈建議指定 .proto 檔案的包,否則可能導緻描述符中的命名沖突并使 proto 對于其他語言不友善。

Packages 和名稱解析

protocol buffer 語言中的類型名稱解析與 C++ 類似:首先搜尋最裡面的範圍,然後搜尋下一個範圍,依此類推,每個包被認為是其父包的 “内部”。一個領先的 '.'(例如 .foo.bar.Baz)意味着從最外層的範圍開始。

protocol buffer 編譯器通過解析導入的 .proto 檔案來解析所有類型名稱。每種語言的代碼生成器都知道如何使用相應的語言類型,即使它具有不同的範圍和規則。

定義服務

如果要将 message 類型與 RPC(遠端過程調用)系統一起使用,則可以在 .proto 檔案中定義 RPC 服務接口,protocol buffer 編譯器将使用你選擇的語言生成服務接口代碼和存根。是以,例如,如果要定義一個 RPC 服務,其中具有一個擷取 SearchRequest 并傳回 SearchResponse 的方法,可以在 .proto 檔案中定義它,如下所示:

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

預設情況下,protocol 編譯器将生成一個名為 SearchService 的抽象接口和相應的 “存根” 實作。存根轉發所有對 RpcChannel 的調用,而 RpcChannel 又是一個抽象接口,你必須根據自己的 RPC 系統自行定義。例如,你可以實作一個 RpcChannel,它将 message 序列化并通過 HTTP 将其發送到伺服器。換句話說,生成的存根提供了一個類型安全的接口,用于進行基于 protocol-buffer 的 RPC 調用,而不會将你鎖定到任何特定的 RPC 實作中。是以,在 C++ 中,你可能會得到這樣的代碼:

using google::protobuf;

protobuf::RpcChannel* channel;
protobuf::RpcController* controller;
SearchService* service;
SearchRequest request;
SearchResponse response;

void DoSearch() {
  // You provide classes MyRpcChannel and MyRpcController, which implement
  // the abstract interfaces protobuf::RpcChannel and protobuf::RpcController.
  channel = new MyRpcChannel("somehost.example.com:1234");
  controller = new MyRpcController;

  // The protocol compiler generates the SearchService class based on the
  // definition given above.
  service = new SearchService::Stub(channel);

  // Set up the request.
  request.set_query("protocol buffers");

  // Execute the RPC.
  service->Search(controller, request, response, protobuf::NewCallback(&Done));
}

void Done() {
  delete service;
  delete channel;
  delete controller;
}
           

所有服務類還實作了 Service 接口,它提供了一種在編譯時不知道方法名稱或其輸入和輸出類型的情況下來調用特定方法的方法。在伺服器端,這可用于實作一個可以注冊服務的 RPC 伺服器。

using google::protobuf;

class ExampleSearchService : public SearchService {
 public:
  void Search(protobuf::RpcController* controller,
              const SearchRequest* request,
              SearchResponse* response,
              protobuf::Closure* done) {
    if (request->query() == "google") {
      response->add_result()->set_url("http://www.google.com");
    } else if (request->query() == "protocol buffers") {
      response->add_result()->set_url("http://protobuf.googlecode.com");
    }
    done->Run();
  }
};

int main() {
  // You provide class MyRpcServer.  It does not have to implement any
  // particular interface; this is just an example.
  MyRpcServer server;

  protobuf::Service* service = new ExampleSearchService;
  server.ExportOnPort(1234, service);
  server.Run();

  delete service;
  return 0;
}
           

如果你不想插入自己現有的 RPC 系統,現在可以使用 gRPC: 一個由谷歌開發的與語言和平台無關的開源 RPC 系統。gRPC 特别适用于 protocol buffers,并允許你使用特殊的 protocol buffers 編譯器插件直接從

.proto

檔案生成相關的 RPC 代碼。但是,由于使用 proto2 和 proto3 生成的用戶端和伺服器之間存在潛在的相容性問題,我們建議你使用 proto3 來定義 gRPC 服務。你可以在 Proto3 語言指南 中找到有關 proto3 文法的更多資訊。如果你确實希望将 proto2 與 gRPC 一起使用,則需要使用 3.0.0 或更高版本的 protocol buffers 編譯器和庫。

除了 gRPC 之外,還有許多正在進行的第三方項目,用于開發 Protocol Buffers 的 RPC 實作。有關我們了解的項目的連結清單,請參閱 第三方附加元件維基頁面。

選項 Options

.proto 檔案中的各個聲明可以使用許多選項進行注釋。選項不會更改聲明的整體含義,但可能會影響在特定上下文中處理它的方式。可用選項的完整清單在 google/protobuf/descriptor.proto 中定義。

一些選項是檔案級選項,這意味着它們應該在頂級範圍内編寫,而不是在任何消息,枚舉或服務定義中。一些選項是 message 消息級選項,這意味着它們應該寫在 message 消息定義中。一些選項是字段級選項,這意味着它們應該寫在字段定義中。選項也可以寫在枚舉類型、枚舉值、服務類型和服務方法上,但是,目前在這幾個項目上并沒有任何有用的選項。

以下是一些最常用的選項:

  • java_package(檔案選項):要用于生成的 Java 類的包。如果 .proto 檔案中沒有給出顯式的 java_package 選項,那麼預設情況下将使用 proto 包(使用 .proto 檔案中的 “package” 關鍵字指定)。但是,proto 包通常不能生成好的 Java 包,因為 proto 包不會以反向域名開頭。如果不生成Java 代碼,則此選項無效。
  • java_outer_classname(檔案選項):要生成的最外層 Java 類(以及檔案名)的類名。如果 .proto 檔案中沒有指定顯式的 java_outer_classname,則通過将 .proto 檔案名轉換為 camel-case 來構造類名(是以 foo_bar.proto 變為 FooBar.java)。如果不生成 Java 代碼,則此選項無效。
  • optimize_for(檔案選項):可以設定為 SPEED,CODE_SIZE 或 LITE_RUNTIME。這會以下列方式影響 C++和 Java 的代碼生成器(可能還有第三方生成器):
    • SPEED(預設值):protocol buffer 編譯器将生成用于對 message 類型進行序列化,解析和執行其他常見操作的代碼。此代碼經過高度優化。
    • CODE_SIZE:protocol buffer 編譯器将生成最少的類,并依賴于基于反射的共享代碼來實作序列化,解析和各種其他操作。是以,生成的代碼将比使用 SPEED 小得多,但操作會更慢。類仍将實作與 SPEED 模式完全相同的公共 API。此模式在包含大量 .proto 檔案的應用程式中最有用,并且不需要所有這些檔案都非常快。
    • LITE_RUNTIME:protocol buffer 編譯器将生成僅依賴于 “lite” 運作時庫(libprotobuf-lite 而不是libprotobuf)的類。精簡版運作時比整個庫小得多(大約小一個數量級),但省略了描述符和反射等特定功能。這對于在行動電話等受限平台上運作的應用程式尤其有用。編譯器仍将生成所有方法的快速實作,就像在 SPEED 模式下一樣。生成的類将僅實作每種語言的 MessageLite 接口,該接口僅提供完整 Message 接口的方法的子集。
  • cc_generic_services

    java_generic_services

    py_generic_services

    (檔案選項):protocol buffer 編譯器應根據服務定義判斷是否生成 C++,Java 和 Python 抽象服務代碼。由于遺留原因,這些預設為 “true”。但是,從版本 2.3.0(2010年1月)開始,RPC 實作最好提供 代碼生成器插件 生成更具體到每個系統的代碼,而不是依賴于 “抽象” 服務。
// This file relies on plugins to generate service code.
option cc_generic_services = false;
option java_generic_services = false;
option py_generic_services = false;
           
  • cc_enable_arenas

    (檔案選項):為 C++ 生成的代碼啟用 arena allocation
  • message_set_wire_format(消息選項):如果設定為 true,則消息使用不同的二進制格式,旨在與 Google 内部使用的舊格式相容,即 MessageSet。Google 以外的使用者可能永遠不需要使用此選項。必須嚴格按如下方式聲明消息:
message Foo {
  option message_set_wire_format = true;
  extensions 4 to max;
}
           
  • packed

    (字段選項):如果在基本數字類型的重複字段上設定為 'true`,則一個更緊湊的編碼 被使用。使用此選項沒有任何缺點。但請注意,在版本 2.3.0 之前,在不期望的情況下接收打包資料的解析器将忽略它。是以,在不破壞相容性的情況下,無法将現有字段更改為打包格式。在 2.3.0 及更高版本中,此更改是安全的,因為可打包字段的解析器将始終接受這兩種格式,但如果你必須使用舊的 protobuf 版本處理舊程式,請務必小心。
  • deprecated

    (field option):如果設定為

    true

    ,表示該字段已棄用,新代碼不應使用該字段。在大多數語言中,這沒有實際效果。在 Java 中,這變成了

    @Deprecated

    注釋。将來,其他特定于語言的代碼生成器可能會在字段的通路器上生成棄用注釋,這将導緻在編譯嘗試使用該字段的代碼時發出警告。如果任何人都未使用該字段,并且你希望阻止新使用者使用該字段,請考慮使用 reserved 替換字段聲明。

自定義選項

Protocol Buffers 甚至允許你定義和使用自己的選項。請注意,這是 進階功能,大多數人不需要。由于選項是由

google/protobuf/descriptor.proto

(如

FileOptions

FieldOptions

)中定義的消息定義的,是以定義你自己的選項隻需要擴充這些消息。例如:

import "google/protobuf/descriptor.proto";

extend google.protobuf.MessageOptions {
  optional string my_option = 51234;
}

message MyMessage {
  option (my_option) = "Hello world!";
}
           

這裡我們通過擴充 MessageOptions 定義了一個新的 message 級選項。然後,當我們使用該選項時,必須将選項名稱括在括号中以訓示它是擴充名。我們現在可以在 C++ 中讀取 my_option 的值,如下所示:

這裡,

MyMessage::descriptor()->options()

傳回

MyMessage

MessageOptions

protocol message。從中讀取自定義選項就像閱讀任何其他擴充。

同樣,在 Java 中我們會寫:

在 Python 中它将是:

value = my_proto_file_pb2.MyMessage.DESCRIPTOR.GetOptions()
  .Extensions[my_proto_file_pb2.my_option]
           

可以在 Protocol Buffers 語言中為每種結構自定義選項。這是一個使用各種選項的示例:

import "google/protobuf/descriptor.proto";

extend google.protobuf.FileOptions {
  optional string my_file_option = 50000;
}
extend google.protobuf.MessageOptions {
  optional int32 my_message_option = 50001;
}
extend google.protobuf.FieldOptions {
  optional float my_field_option = 50002;
}
extend google.protobuf.EnumOptions {
  optional bool my_enum_option = 50003;
}
extend google.protobuf.EnumValueOptions {
  optional uint32 my_enum_value_option = 50004;
}
extend google.protobuf.ServiceOptions {
  optional MyEnum my_service_option = 50005;
}
extend google.protobuf.MethodOptions {
  optional MyMessage my_method_option = 50006;
}

option (my_file_option) = "Hello world!";

message MyMessage {
  option (my_message_option) = 1234;

  optional int32 foo = 1 [(my_field_option) = 4.5];
  optional string bar = 2;
}

enum MyEnum {
  option (my_enum_option) = true;

  FOO = 1 [(my_enum_value_option) = 321];
  BAR = 2;
}

message RequestType {}
message ResponseType {}

service MyService {
  option (my_service_option) = FOO;

  rpc MyMethod(RequestType) returns(ResponseType) {
    // Note:  my_method_option has type MyMessage.  We can set each field
    //   within it using a separate "option" line.
    option (my_method_option).foo = 567;
    option (my_method_option).bar = "Some string";
  }
}
           

請注意,如果要在除定義它之外的包中使用自定義選項,則必須在選項名稱前加上包名稱,就像對類型名稱一樣。例如:

// foo.proto
import "google/protobuf/descriptor.proto";
package foo;
extend google.protobuf.MessageOptions {
  optional string my_option = 51234;
}
           
// bar.proto
import "foo.proto";
package bar;
message MyMessage {
  option (foo.my_option) = "Hello world!";
}
           

最後一件事:由于自定義選項是擴充名,是以必須為其配置設定字段編号,就像任何其他字段或擴充名一樣。在上面的示例中,我們使用了 50000-99999 範圍内的字段編号。此範圍保留供個别組織内部使用,是以你可以自由使用此範圍内的數字用于内部應用程式。但是,如果你打算在公共應用程式中使用自定義選項,則務必確定你的字段編号是全局唯一的。要擷取全球唯一的字段編号,請發送請求以向 protobuf全球擴充系統資料庫 添加條目。通常你隻需要一個擴充号。你可以通過将多個選項放在子消息中來實作一個擴充号聲明多個選項:

message FooOptions {
  optional int32 opt1 = 1;
  optional string opt2 = 2;
}

extend google.protobuf.FieldOptions {
  optional FooOptions foo_options = 1234;
}

// usage:
message Bar {
  optional int32 a = 1 [(foo_options).opt1 = 123, (foo_options).opt2 = "baz"];
  // alternative aggregate syntax (uses TextFormat):
  optional int32 b = 2 [(foo_options) = { opt1: 123 opt2: "baz" }];
}
           

另請注意,每種選項類型(檔案級别,消息級别,字段級别等)都有自己的數字空間,例如,你可以使用相同的數字聲明 FieldOptions 和 MessageOptions 的擴充名。

生成你的類

要生成 Java,Python 或 C++代碼,你需要使用

.proto

檔案中定義的 message 類型,你需要在

.proto

上運作 protocol buffer 編譯器

protoc

。如果尚未安裝編譯器,請 下載下傳軟體包 并按照 README 檔案中的說明進行操作。

Protocol 編譯器的調用如下:

protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR path/to/file.proto
           
  • IMPORT_PATH 指定在解析導入指令時查找 .proto 檔案的目錄。如果省略,則使用目前目錄。可以通過多次傳遞 --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 生成的代碼 。

      為了友善起見,如果 DST_DIR 以 .zip 或 .jar 結尾,編譯器會将輸出寫入到具有給定名稱的單個 ZIP 格式的存檔檔案。.jar 輸出還将根據 Java JAR 規範的要求提供清單檔案。請注意,如果輸出存檔已存在,則會被覆寫;編譯器不夠智能,無法将檔案添加到現有存檔中。

  • 你必須提供一個或多個 .proto 檔案作為輸入。可以一次指定多個 .proto 檔案。雖然檔案是相對于目前目錄命名的,但每個檔案必須駐留在其中一個 IMPORT_PATH 中,以便編譯器可以确定其規範名稱。
版權聲明

原文連結:https://www.jianshu.com/p/6f68fb2c7d19

繼續閱讀