Language Guide(proto2)

該文檔描述瞭如何使用protocol buffer語言來構建你自己的proto數據,包括proto文件語法和如何創建可以使用的classes文件,它涵蓋了proto2版本的語言。如果需要proto3語法的,請移步Proto3 Language Guide:link

首選Proto3,但仍將繼續支持proto2,我門推薦新項目使用proto3代替,它更容易使用並且支持更多的語言

這是一個參考指南-有關使用本文檔中描述的許多功能的分步示例,請參見所選語言的教程。

定義消息類型

首先,我們先看一個非常簡單的例子。讓我們假設你想定義一個請求的消息格式,每一個請求裏包含一個搜索字符串,你想要的特定頁數的結果和一頁的結果數量,那麼結構就是如下:

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

這個類定義了一個消息,包含三個字段(name/value pairs),每個片段都是你想要包含在此類消息中的數據。每個域都有一個名稱和類型

指定域的類型

在上面的示例中,所有字段均爲標量類型:兩個整數(page_number和result_per_page)和一個字符串(query)。但是,您也可以爲字段指定複合類型,包括枚舉和其他消息類型。

分配字段編號

如您所見,消息定義中的每個字段都有一個唯一的數字。這些數字用於標識消息二進制格式的字段,一旦使用了消息類型,就不應更改這些數字。請注意,範圍爲1到15的字段號需要一個字節來編碼,包括字段號和字段的類型(您可以在Protocol Buffer Encoding中找到有關此內容的更多信息)。16到2047之間的字段編號佔用兩個字節。因此,您應該爲經常出現的消息元素保留字段編號1到15。切記爲將來可能添加的頻繁出現的元素留出一些空間。

您可以指定最小的數爲1,最大爲2^{29} - 1,或536870911。您也不能使用數字19000到19999(FieldDescriptor::kFirstReservedNumber至FieldDescriptor::kLastReservedNumber),因爲它們是爲Protocol Buffers實現保留的-如果您在proto中使用這些保留數之一,則編譯器會提示衝突。同樣,您不能使用任何以前保留的字段號。

指定字段規則

您可以指定以下任何一種消息字段:

required:格式正確的消息必須有一個此字段。
optional:格式正確的消息可以包含0個或1個此字段(但不能超過1個)。
repeated:在格式正確的消息中,此字段可以重複任意次(包括0次)。重複值的順序將保留。
由於歷史原因,repeated標量數字類型的字段編碼效率不高。新代碼應使用特殊選項[packed=true]來獲得更有效的編碼。例如:

repeated int32 samples = 4 [packed=true];

您可以找到有關packed 編碼的更多信息。link

Required Is Forever 您將字段標記爲required時應格外小心。如果您希望停止寫入或發送必填字段,則將字段更改爲可選字段會很麻煩–老的讀者會認爲沒有該字段的消息不完整,可能會無意拒絕或丟棄它們。您應該考慮爲buffers編寫特定於應用程序的自定義驗證例程。Google的一些工程師得出的結論是,使用required弊大於利。他們更喜歡只使用optional和repeated。但是,這種觀點並不普遍。

添加更多消息類型

可以在一個.proto文件中定義多種消息類型。如果要定義多個相關消息,這將非常有用–例如,如果要定義與您的SearchResponse消息類型相對應的回覆消息格式,可以將其添加到相同的消息中.proto:

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

message SearchResponse {
 ...
}

合併消息會導致膨脹。 雖然可以在單個.proto文件中定義多種消息類型(例如消息,枚舉和服務),但是當在單個文件中定義大量具有不同依賴性的消息時,也可能導致依賴性膨脹。建議每個.proto文件包含儘可能少的消息類型。

添加評論

要將註釋添加到.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.
}

保留字段

如果您通過完全刪除字段或將其註釋掉來更新消息類型,將來的用戶可以在自己對類型進行更新時重用該字段號。如果他們以後加載同一.proto的舊版本,可能會導致嚴重的問題,包括數據損壞,隱私錯誤等。確保不會發生這種情況的一種方法是指定保留已刪除字段的字段編號(和/或名稱,這也可能導致JSON序列化問題)。如果將來有用戶嘗試使用這些字段標識符,則編譯器會報錯。

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

請注意,您不能在同reserved一條語句中混用字段名稱和字段編號。

proto 文件到底生成了什麼?

當你編譯.proto文件時,編譯器會以您選擇的語言生成代碼,您將需要使用文件中描述的消息類型,包括獲取和設置字段值,將消息序列化爲輸出流,並從輸入流中解析消息。

對於C ++,編譯器從每個.proto文件中,生成一個.h和.cc文件,併爲文件中描述的每種消息類型提供一個類。
對於Java,編譯器將生成一個.java文件,其中包含每種消息類型的類以及Builder用於創建消息類實例的特殊類。
Python有點不同– Python編譯器會在您的中生成一個模塊,其中包含每種消息類型的靜態描述符,然後該模塊與元類一起使用,以在運行時創建必要的Python數據訪問類。
對於Go,編譯器會生成一個.pb.go文件,其中包含文件中每種消息類型的類型。
您可以按照所選語言的教程,找到有關每種語言使用API​​的更多信息。有關API的更多詳細信息,請參見相關的API參考。

標量值類型

標量消息字段可以具有以下類型之一-該表顯示.proto文件中指定的類型,以及自動生成的類中的相應類型:
proto支持的標量類型

您可以在Protocol Buffer Encoding文章中link中找到更多有關如何編碼這些類型的信息。

[1]在Java中,無符號的32位和64位整數使用帶符號的對等體表示,最高位僅存儲在符號位中。

[2]在所有情況下,將值設置爲字段都會執行類型檢查以確保其有效。

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

可選字段和默認值

如上所述,消息描述中的元素可以被標記optional。格式正確的消息可能包含也可能不包含可選元素。解析消息時,如果其中不包含可選元素,則解析對象中的相應字段將設置爲該字段的默認值。可以將默認值指定爲消息描述的一部分。例如,假設你想爲SearchRequest的result_per_page提供的10爲默認值,如下表示:

optional int32 result_per_page = 3 [default = 10];

如果未爲可選元素指定默認值,則使用特定於類型的默認值:對於字符串,默認值爲空字符串。對於字節,默認值爲空字節字符串。對於布爾值,默認值爲false。對於數字類型,默認值爲0。對於枚舉,默認值爲枚舉類型定義中列出的第一個值。這意味着在將值添加到枚舉值列表的開頭時必須格外小心。有關如何安全更改定義的準則,請參閱“ 更新消息類型”部分。

枚舉

在定義消息類型時,您可能希望其字段之一僅具有一個預定義的值列表之一。例如,假設你想添加一個 corpus字段每個SearchRequest,其中語料庫可以 UNIVERSAL,WEB,IMAGES,LOCAL,NEWS,PRODUCTS或VIDEO。您可以非常簡單通過enum在消息定義-具有enum類型的字段只能將一組指定的常量作爲其值之一(如果嘗試提供其他值,則解​​析器會將其視爲未知值領域)。在下面的示例中,我們添加了一個帶有所有可能值的枚舉類型項Corpus,以及一個type字段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,否則協議別名會在找到別名時生成一條錯誤消息。

枚舉器常量必須在32位整數範圍內。由於enum值在“導線”【該詞不知道怎麼翻譯】上使用varint編碼,因此負值效率不高,因此不建議使用。您可以在消息定義內定義枚舉,如上例所示,也可以在外部定義-這些枚舉可以在.proto文件中的任何消息定義中重複使用。 您還可以使用語法MessageType.EnumType將一條消息中聲明的枚舉類型用作另一條消息中字段的類型。

當您在使用枚舉的.proto上運行協議緩衝區編譯器時,生成的代碼將具有一個對應於Java或C ++的枚舉,或者一個特定的Python EnumDescriptor類,該類用於創建一組符號常量,並在其中使用整數值。 運行時生成的類

有關如何在應用程序中使用消息枚舉的更多信息,請參見針對所選語言生成的代碼指南。

保留值

如果通過完全刪除枚舉條目或將其註釋掉來更新枚舉類型,則將來的用戶在自己對類型進行更新時可以重用數值。如果他們以後加載舊版本的舊版本,可能會導致嚴重的問題.proto,包括數據損壞,隱私錯誤等。確保不會發生這種情況的一種方法是,將已刪除的條目的數字值(和/或名稱,也可能導致JSON序列化的問題)指定爲reserved。如果將來有任何用戶嘗試使用這些標識符,則協議緩衝區編譯器將抱怨。您可以使用max關鍵字指定保留的數值範圍達到最大可能值。

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

請注意,您不能在同reserved一條語句中混合使用字段名和數字值。

使用其他消息類型

您可以使用其他消息類型作爲字段類型。例如,假設你想包括Result每個消息的SearchResponse消息要做到這一點,你可以定義一個Result在同一個消息類型的proto文件,然後在SearchResponse指定類型的字段Result:·

message SearchResponse {
  repeated Result result = 1;
}

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

導入定義

在上面的示例中,Result消息類型都定義在同一文件SearchResponse中。如果要用的字段類型已經在另一個.proto文件中定義,該怎麼辦?

您可以通過導入其他文件來使用它們的定義。要導入其它.proto的定義,請在文件頂部添加一個import語句:

import "myproject/other_protos.proto";

默認情況下,您只能使用直接導入.proto文件中的定義。但是,有時您可能需要將.proto文件移動到新位置。現在,您可以直接在原位置放置一個虛擬文件,以使用該import public概念將所有導入轉發到新位置,而不必一次移動文件並一次更改所有引用點。import public任何導入包含該import public語句的原型的人都可以過渡地依賴項。例如:

// 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";
// You use definitions from old.proto and new.proto, but not other.proto

協議編譯器使用-I/ --proto_path標誌在協議編譯器命令行上指定的一組目錄中搜索導入的文件 。如果未給出任何標誌,它將在調用編譯器的目錄中查找。通常,應將–proto_path標誌設置爲項目的根,並對所有導入使用完全限定的名稱。

使用proto3消息類型

可以導入proto3消息類型並在proto2消息中使用它們,反之亦然。但是,不能在proto3語法中使用proto2枚舉。

嵌套類型

您可以在其他消息類型內定義和使用消息類型,如以下示例所示–在此處,消息Result是在消息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;
    }
  }
}

請注意,此功能已棄用,在創建新的消息類型時不應使用,而是使用嵌套消息類型。

組是在信息定義中嵌套信息的另一種方法。例如,另一種指定SearchResponse包含多個Results的方法如下:

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

組將嵌套的消息類型和字段簡單地組合爲一個聲明。在您的代碼中,您可以將此消息視爲具有一個Result稱爲“類型”字段的類型result (後者的名稱將轉換爲小寫,以使其與前者不衝突)。因此,此示例與SearchResponse上述示例完全等效,除了該消息具有不同的線路格式。

更新消息類型

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

  • 不要更改任何現有字段的字段編號。
  • 您添加的任何新字段應爲optional或repeated。這意味着使用新的代碼可以解析使用“舊”消息格式通過代碼序列化的所有消息,因爲它們不會丟失任何required元素。您應該爲這些元素設置合理的默認值,以便新代碼可以與舊代碼生成的消息正確交互。同樣,由新代碼創建的消息可以由舊代碼解析:舊的二進制文件在解析時只會忽略新字段。但是,未知字段不會被丟棄,並且如果以後對消息進行序列化,則未知字段也會與之一起進行序列化–因此,如果消息傳遞給新代碼,則新字段仍然可用。
  • 只要在更新後的消息類型中不再使用該字段號,就可以刪除不需要的字段。您可能想要重命名該字段,或者添加前綴“ OBSOLETE_”,或者將字段編號保留爲,以便將來的用戶不會意外地重用該編號。
  • 只要類型和數字保持不變,就可以將不需要的字段轉換爲擴展名,反之亦然。
  • int32,uint32,int64,uint64,和bool都是兼容的-這意味着你可以從這些類型到另一種改變-或向後兼容。如果從對應的類型不適合的導線中解析出一個數字,則將獲得與在C ++中將該數字強制轉換爲該類型一樣的效果(例如,如果將64位數字讀爲int32,它將被截斷爲32位)。
  • sint32和sint64彼此兼容,但與其他整數類型不兼容。
  • string和bytes只要字節是有效的UTF-8即可兼容。
  • 嵌入式消息與bytes字節是否包含消息的編碼版本兼容。
  • fixed32與兼容sfixed32,fixed64與sfixed64兼容。
  • optional與兼容repeated。給定重複字段的序列化數據作爲輸入,如果期望此字段optional是原始類型字段,則期望該字段的客戶端將採用最後一個輸入值;如果是消息類型字段,則將合併所有輸入元素。
  • 只要您記得從未通過網絡發送默認值,通常就可以更改默認值。因此,如果程序收到未設置特定字段的消息,則該程序將看到該程序協議版本中定義的默認值。它不會看到發件人代碼中定義的默認值。
  • enum與兼容int32,uint32,int64,和uint64電線格式條款(請注意,如果他們不適合的值將被截斷),但是要知道,客戶端代碼可以區別對待反序列化的消息時。值得注意的是,enum當對消息進行反序列化時,無法識別的值將被丟棄,這會使字段的has…訪問器返回false,並且其getter返回enum定義中列出的第一個值;如果指定了默認值,則返回默認值。對於重複的枚舉字段,所有無法識別的值將從列表中刪除。但是,整數字段將始終保留其值。因此,在將整數升級爲a時,您需要非常小心,以在線路上enum接收超出範圍的枚舉值。
  • 在當前的Java和C ++實現中,當enum去除了無法識別的值時,它們會與其他未知字段一起存儲。請注意,如果將此數據序列化,然後由識別這些值的客戶端重新解析,則可能導致奇怪的行爲。對於可選字段,即使在反序列化原始消息之後寫入了新值,識別該值的客戶端仍會讀取舊值。在重複字段的情況下,舊值將出現在任何已識別和新添加的值之後,這意味着將不保留順序。
  • 將單個optional值更改爲新 值的成員oneof是安全且二進制兼容的。如果您確定沒有代碼一次設置多個optional字段,那麼將多個字段移動到新字段中oneof可能是安全的。將任何字段移到現有字段中oneof都是不安全的。
  • 在map<K, V>和對應的repeated消息字段之間更改字段是二進制兼容的(有關消息佈局和其他限制,請參見下面的Maps)。但是,更改的安全性取決於應用程序:在反序列化和重新序列化消息時,使用repeated字段定義的客戶端將產生語義上相同的結果;但是,使用map字段定義的客戶端可以對條目進行重新排序,並刪除具有重複鍵的條目。

擴展

擴展使您可以聲明消息中的字段號範圍可用於第三方擴展。擴展名是其類型未由原始.proto文件定義的字段的佔位符。這允許其他.proto文件通過使用這些字段編號定義某些或所有字段的類型來添加到您的消息定義中。讓我們看一個例子:

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

這表示字段編號[100,199] 範圍內的數值在Foo中被保留用於擴展。現在,其他用戶可以使用指定範圍內的字段編號將新字段添加到Foo的自己的.proto文件中,以導入您的.proto,例如:

extend Foo {
  optional int32 bar = 126;
}

這會將名稱爲bar類型數值爲126的字段添加到的原始定義Foo。

對用戶的Foo消息進行編碼後,有線格式與用戶在中定義新字段的方式完全相同Foo。但是,在應用程序代碼中訪問擴展字段的方式與訪問常規字段略有不同–生成的數據訪問代碼具有用於處理擴展的特殊訪問器。因此,例如,這是bar在C ++中設置value的方法:·

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

類似地,Foo類定義模板訪問器HasExtension(),ClearExtension(),GetExtension(),MutableExtension(),和AddExtension()。所有的語義都與普通字段的相應生成的訪問器匹配。有關使用擴展的更多信息,請參見針對您選擇的語言生成的代碼參考。

請注意,擴展名可以是任何字段類型,包括消息類型,但不能是oneofs或maps。

嵌套擴展

您可以在另一種類型的範圍內聲明擴展:·

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

在這種情況下,用於訪問此擴展的C ++代碼爲:

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

換句話說,唯一的效果是bar在的範圍內定義的Baz。
|這是造成混淆的常見原因:聲明extend嵌套在消息類型內部的塊並不意味着外部類型與擴展類型之間沒有任何關係。特別地,以上示例並不意味着Baz是的任何子類Foo。它的意思是符號bar在範圍內聲明Baz; 它只是一個靜態成員。|

一種常見的模式是在擴展的字段類型範圍內定義擴展-例如,這Foo是type Baz的擴展,其中擴展定義爲的一部分Baz:

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

但是,不需要在消息類型的內部定義具有消息類型的擴展名。您也可以這樣做:

message Baz {
  ...
}

// This can even be in a different file.
extend Foo {
  optional Baz foo_baz_ext = 127;
}

實際上,爲了避免混淆,可能首選此語法。如上所述,嵌套語法經常被尚未熟悉擴展的用戶誤認爲是子類。

Choosing Extension Numbers(不好翻譯,直接用原文)

確保兩個用戶不要使用相同的字段號將擴展名添加到同一消息類型是非常重要的–如果意外將擴展名解釋爲錯誤的類型,則可能導致數據損壞。您可能要考慮爲項目定義擴展名編號約定,以防止發生這種情況。

如果您的編號約定可能涉及具有非常大的字段號的擴展名,則可以使用max關鍵字指定擴展範圍達到最大可能的字段號:

message Foo {
  extensions 1000 to max;
}

max爲2^29 - 1,或536870911。

與通常選擇字段編號時一樣,您的編號約定也需要避免使用字段編號19000到19999(FieldDescriptor::kFirstReservedNumber至FieldDescriptor::kLastReservedNumber),因爲它們是爲協議緩衝區實現保留的。您可以定義包括該範圍的擴展範圍,但是協議編譯器不允許您使用這些數字定義實際的擴展。

Oneof

如果您的消息包含許多可選字段,並且最多同時設置一個字段,則可以使用oneof功能強制執行此行爲並節省內存。

除了共享內存中的所有字段外,oneof字段類似於可選字段,並且最多可以同時設置一個字段。設置oneof中的任何成員會自動清除所有其他成員。您可以根據所選擇的語言,使用特殊case()或WhichOneof()方法來檢查其中一個設置的值(如果有)。

使用Oneof

要在您中定義一個oneof,請在關鍵字前使用oneof修師,例如test_oneof:

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

然後,將oneof字段添加到oneof定義。您可以添加任何類型的字段,但不能使用required,optional或repeated關鍵字。如果需要將重複字段添加到一個字段中,則可以使用包含重複字段的消息。

在生成的代碼中,ofof字段具有與常規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不能是repeated。
  • 反射API適用於其中一個字段。
  • 如果將oneof字段設置爲默認值(例如將int32 oneof字段設置爲0),則將設置該oneof字段的“大小寫”,並且該值將在線路上序列化。
  • 如果您使用的是C ++,請確保您的代碼不會導致內存崩潰。以下示例代碼將崩潰,因爲sub_message已通過調用該set_name()方法將其刪除。
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兩個消息,每個消息將結束與對方的oneof情況下:在下面的例子中,msg1將具有sub_message與

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的不同版本中的字段。由於無法知道導線上的未知字段是否是oneof的成員,因此無法分辨出差異。

標籤重用問題

  • 將可選字段移入或移出oneof:在對消息進行序列化和解析後,您可能會丟失一些信息(某些字段將被清除)。但是,您可以安全地將單個字段移動到新字段中,並且如果已知只設置了一個字段,則可以移動多個字段。
  • 刪除一個oneof字段並將其添加回去:序列化和解析郵件後,這可能會清除您當前設置的oneof字段。
  • 拆分或合併其中一個:與移動常規optional字段有類似的問題。

maps

如果要在數據定義中創建關聯映射,則協議緩衝區提供了方便的快捷方式語法:

map < key_type ,value_type > map_field = N ;

…其中key_type可以是任何整數或字符串類型(因此,除浮點類型和以外的任何標量類型bytes)。請注意,枚舉不是有效的key_type。value_type可以是任何類型的除另一個map。

因此,例如,如果您想創建一個項目地圖,其中每個Project消息都與一個字符串鍵相關聯,則可以這樣定義它:

map<string, Project> projects = 3;

生成的map的 API當前可用於所有proto2支持的語言。您可以在相關API參考中找到有關所選語言的map API的更多信息。

map的特徵

  • map不支持擴展。
  • map不能是repeated,optional或required。
  • map的值是非線性排序和map迭代排序是不確定的,因此您不能依賴於map項的特定順序。
  • 在生成文本格式時.proto,地圖會按鍵排序。數字鍵按數字排序。
  • 從導線解析或合併時,如果存在重複的映射鍵,則使用最後可見的鍵。從文本格式解析map時,如果鍵重複,則解析可能會失敗。

向後兼容

映射語法與網上的以下語法等效,因此不支持映射的協議緩衝區實現仍可以處理您的數據:

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

repeated MapFieldEntry map_field = N;

任何支持映射的協議緩衝區實現都必須產生並接受可以被上述定義接受的數據。

您可以向.proto文件添加可選的package說明符,以防止協議消息類型之間的名稱衝突。

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

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

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

包說明符影響生成的代碼的方式取決於您選擇的語言:

  • 在C ++中,生成的類包裝在C ++名稱空間中。例如,Open將在命名空間中foo::bar。
  • 在Java中,除非您在.proto文件中顯式提供option java_package,否則該包將用作Java包。
  • 在Python中,該package指令將被忽略,因爲Python模塊是根據其在文件系統中的位置進行組織的。
  • 在Go中,該package指令將被忽略,並且生成的.pb.go文件位於以相應go_proto_library規則命名的包中。
    請注意,即使該package指令不直接影響生成的代碼(例如在Python中),仍強烈建議爲該.proto文件指定包,否則可能會導致描述符中的命名衝突,並使原型無法移植到其他文件中。語言。

軟件包和名稱解析

協議緩衝語言中的類型名稱解析類似於C ++:首先搜索最內層的作用域,然後搜索下一個最內層的作用,依此類推,每個包都被視爲其父包“內在”。引用“.” (例如,.foo.bar.Baz)表示從最外面的範圍開始。

協議緩衝區編譯器通過解析導入的.proto文件來解析所有類型名稱。每種語言的代碼生成器都知道如何引用該語言中的每種類型,即使它具有不同的範圍規則。

定義服務

如果要將消息類型與RPC(遠程過程調用)系統一起使用,則可以在.proto文件中定義RPC服務接口,協議緩衝區編譯器將以您選擇的語言生成服務接口代碼和存根。因此,例如,如果您想使用一種方法來定義RPC服務,該方法接受您的SearchRequest並返回SearchResponse,則可以在.proto文件中按以下方式進行定義:·

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

默認情況下,協議編譯器將生成一個稱爲的抽象接口SearchService和相應的“stub”實現。stub將所有調用轉發到RpcChannel,後者又是一個抽象接口,您必須根據自己的RPC系統定義自己。例如,您可以實現RpcChannel序列化消息並通過HTTP將其發送到服務器的。換句話說,生成的stub提供了一個類型安全的接口,用於進行基於協議緩衝區的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:這是Google開發的與語言和平臺無關的開源RPC系統。gRPC與協議緩衝區配合使用特別好,並允許您.proto使用特殊的協議緩衝區編譯器插件直接從文件中生成相關的RPC代碼。但是,由於proto2和proto3生成的客戶端和服務器之間存在潛在的兼容性問題,因此建議您使用proto3定義gRPC服務。您可以在《Proto3語言指南》中找到有關proto3語法的更多信息。如果確實要在gRPC中使用proto2,則需要使用3.0.0或更高版本的協議緩衝區編譯器和庫。

除gRPC之外,還有許多正在進行的第三方項目正在開發協議緩衝區的RPC實現。有關我們知道的項目的鏈接列表,請參見第三方附加組件Wiki頁面。

選項

.proto文件中的各個聲明可以使用許多選項進行註釋。選項不會改變聲明的整體含義,但可能會影響在特定上下文中處理聲明的方式。可用選項的完整列表在中定義google/protobuf/descriptor.proto。

一些選項是文件級選項,這意味着它們應在頂級範圍內編寫,而不是在任何消息,枚舉或服務定義內。一些選項是消息級別的選項,這意味着它們應該寫在消息定義中。一些選項是字段級選項,這意味着它們應在字段定義中編寫。選項也可以寫在枚舉類型,枚舉值,字段,服務類型和服務方法中;但是,目前沒有針對這些選項的有用選項。

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

  • java_package(文件選項):要用於生成的Java類的包。如果文件中未提供任何顯式java_package選項.proto,則默認情況下將使用proto軟件包(在.proto文件中使用“ package”關鍵字指定)。但是,proto軟件包通常不能成爲良好的Java包,因爲proto軟件包不應以反向域名開頭。如果未生成Java代碼,則此選項無效。
option java_package = "com.example.foo";
  • java_outer_classname(文件選項):要生成的最外層Java類的類名(以及文件名)。如果java_outer_classname在.proto文件中未指定任何顯式名稱,則將.proto文件名轉換爲駝峯大小寫(從而foo_bar.proto成爲FooBar.java)來構造類名。如果未生成Java代碼,則此選項無效。
    選項java_outer_classname =“ Ponycopter”;

  • optimize_for(文件選項):可以設置爲SPEED,CODE_SIZE或LITE_RUNTIME。這會通過以下方式影響C ++和Java代碼生成器(可能還有第三方生成器):

    • SPEED(默認):協議緩衝區編譯器將生成代碼,用於對消息類型進行序列化,解析和執行其他常見操作。此代碼已高度優化。
    • CODE_SIZE:協議緩衝區編譯器將生成最少的類,並將依賴於基於反射的共享代碼來實現序列化,解析和其他各種操作。因此,生成的代碼將比使用的代碼小得多SPEED,但是操作會更慢。類仍將實現與SPEED模式下完全相同的公共API 。此模式在包含大量.proto文件且不需要所有文件快速快速運行的應用程序中最有用。
    • LITE_RUNTIME:協議緩衝區編譯器將生成僅依賴於“精簡版”運行時庫(libprotobuf-lite而不是libprotobuf)的類。精簡版運行時比完整庫要小得多(大約小一個數量級),但省略了某些功能,例如描述符和反射。這對於在受限平臺(例如手機)上運行的應用程序特別有用。編譯器仍將像在SPEED模式下一樣快速生成所有方法的實現。生成的類將僅以MessageLite每種語言實現接口,這僅提供完整Message接口方法的一部分。
option optimize_for = CODE_SIZE;
  • cc_generic_services,java_generic_services, py_generic_services(文件選項):無論協議緩衝編譯器應該基於抽象服務代碼 的服務定義在C ++,Java和Python中,分別。出於傳統原因,這些默認設置爲true。但是,從2.3.0版(2010年1月)開始,RPC實現最好提供 代碼生成器插件來生成更特定於每個系統的代碼,而不是依賴於“抽象”服務。
    //此文件依賴於插件來生成服務代碼。
    選項cc_generic_services = false;
    選項java_generic_services = false;
    選項py_generic_services = false;

  • cc_enable_arenas(文件選項):啓用C ++生成代碼的平臺分配。

  • 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版本處理舊程序,請務必小心。
repeated int32 samples = 4 [packed=true];
  • deprecated(字段選項):如果設置爲true,則表明該字段已棄用,並且不應被新代碼使用。在大多數語言中,這沒有實際效果。在Java中,這成爲@Deprecated註釋。將來,其他特定於語言的代碼生成器可能會在字段的訪問器上生成棄用註釋,這反過來將導致在編譯嘗試使用該字段的代碼時發出警告。如果該字段未被任何人使用,並且您想阻止新用戶使用該字段,請考慮使用保留語句替換該字段聲明。
optional int32 old_field = 6 [deprecated=true];

自定義選項

協議緩衝區甚至允許您定義和使用自己的選項。請注意,這是大多數人不需要的高級功能。由於選項由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。當我們使用選項時,選項名稱必須用括號括起來以表明它是擴展名。現在,我們可以my_option像這樣讀取C ++中的值:

string value = MyMessage::descriptor()->options().GetExtension(my_option);

在此,MyMessage::descriptor()->options()返回的MessageOptions協議消息MyMessage。從中讀取自定義選項就像讀取其他任何擴展名一樣。

同樣,在Java中,我們將編寫:

String value = MyProtoFile.MyMessage.getDescriptor().getOptions()
  .getExtension(MyProtoFile.myOption);

在Python中將是:

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

可以爲協議緩衝區語言中的每種構造定義自定義選項。這是一個使用各種選項的示例:

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.OneofOptions {
  optional int64 my_oneof_option = 50003;
}
extend google.protobuf.EnumOptions {
  optional bool my_enum_option = 50004;
}
extend google.protobuf.EnumValueOptions {
  optional uint32 my_enum_value_option = 50005;
}
extend google.protobuf.ServiceOptions {
  optional MyEnum my_service_option = 50006;
}
extend google.protobuf.MethodOptions {
  optional MyMessage my_method_option = 50007;
}

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;
  oneof qux {
    option (my_oneof_option) = 42;

    string quux = 3;
  }
}

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的擴展名。

生成proto 類

爲了生成如下類型的Java,Python或C ++代碼.proto文件,則需要運行協議緩衝編譯器使用protoc運行.proto。如果尚未安裝編譯器,請下載軟件包並按照README中的說明進行操作。

協議編譯器的調用如下:

protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR path/to/file.proto
  • IMPORT_PATH指定.proto解析import指令時要在其中查找文件的目錄。如果省略,則使用當前目錄。可以通過–proto_path多次傳遞選項來指定多個導入目錄。將按順序搜索它們。 可以用作的簡寫形式。 -I=IMPORT_PATH–proto_path
  • 您可以提供一個或多個輸出指令:
    • –cpp_out在中生成C ++代碼DST_DIR。有關更多信息,請參見C ++生成的代碼參考。
    • –java_out在中生成Java代碼DST_DIR。有關更多信息,請參見Java生成的代碼參考。
    • –python_out在中生成Python代碼DST_DIR。有關更多信息,請參見Python生成的代碼參考。
      爲了更加方便,如果DST_DIR結尾爲.zip 或.jar,編譯器會將輸出寫入具有給定名稱的單個ZIP格式的存檔文件。 .jar根據Java JAR規範的要求,還將爲輸出提供清單文件。注意,如果輸出歸檔文件已經存在,它將被覆蓋;編譯器不夠智能,無法將文件添加到現有存檔中。
  • 您必須提供一個或多個.proto文件作爲輸入。.proto可以一次指定多個文件。儘管這些文件是相對於當前目錄命名的,但是每個文件都必須位於IMPORT_PATHs 之一中,以便編譯器可以確定其規範名稱。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章