本指南說明如何使用通訊協定緩衝區語言來建構通訊協定緩衝區資料,包括 .proto
檔案語法,以及如何從 .proto
檔案產生資料存取類別。其中涵蓋通訊協定緩衝區語言的 proto2 版本:如要進一步瞭解 proto3 語法,請參閱 Proto3 語言指南。
這是參考指南 – 如需使用本文件中描述多項功能的逐步範例,請參閱所選語言的教學課程。
定義訊息類型
首先來看看一個簡單的範例。假設您想定義搜尋要求訊息格式,其中每個搜尋要求都有查詢字串、您感興趣的搜尋結果頁面,以及每個網頁的結果數量。以下是用來定義訊息類型的 .proto
檔案。
message SearchRequest {
required string query = 1;
optional int32 page_number = 2;
optional int32 result_per_page = 3;
}
SearchRequest
訊息定義會指定三個欄位 (名稱/值組合),每個要納入這類訊息的資料。每個欄位都有名稱和類型。
指定欄位類型
在上述範例中,所有欄位都是「純量類型」:兩個整數 (page_number
和 result_per_page
) 和字串 (query
)。不過您也可以為欄位指定複合類型,包含列舉和其他訊息類型。
指派欄位號碼
如您所見,訊息定義中的每個欄位都有專屬編號。這些數值會用於識別訊息二進位格式的欄位,且在您使用訊息類型後,請勿進行變更。1 到 15 範圍內的欄位編號需要一個位元組進行編碼,包括欄位號碼和欄位的類型 (詳情請參閱通訊協定緩衝區編碼)。16 到 2047 範圍內的欄位編號需要兩個位元組。因此,建議您針對經常發生的訊息元素保留數字 1 到 15。請記得為日後會經常更新的元素保留一些空間。
您可以指定的最小欄位值為 1,最大為 229 - 1,或 536,870,911。您不能使用 19000 到 19999 (FieldDescriptor::kFirstReservedNumber
到 FieldDescriptor::kLastReservedNumber
) 數字,因為這些數字會保留給通訊協定緩衝區實作 - 如果在 .proto
中使用其中一個保留號碼,通訊協定緩衝區編譯器將會編譯。同樣地,您無法使用先前保留的欄位號碼。
指定欄位規則
您可以指定下列訊息欄位:
required
:格式正確的訊息必須含有這個欄位中的一個。optional
:格式正確的訊息可以有零或一個欄位 (但只能有一個)。repeated
:在格式良好的訊息中,這個欄位可以重複出現任意次數 (包含零)。系統會保留重複值的順序。
基於歷史因素,純量數值類型的 repeated
欄位 (例如 int32
、int64
、enum
) 編碼的效率不如預期。新的程式碼應使用特殊選項 [packed = true]
來取得更有效率的編碼。例如:
repeated int32 samples = 4 [packed = true];
repeated ProtoEnum results = 5 [packed = true];
如要進一步瞭解 packed
編碼,請參閱通訊協定緩衝區編碼一文。
必要是永久性:將欄位標示為 required
時,請務必謹慎小心。如果您在某個時間點想要停止寫入或傳送必填欄位,請將該欄位變更為選填欄位會發生問題;由舊讀者認為沒有這個欄位的訊息不完整,且可能會不小心拒絕或捨棄這些訊息。請考慮為緩衝區撰寫應用程式專屬的自訂驗證處理常式。
每當有使用者為列舉新增值時,就會出現第二個必填欄位。在這種情況下,系統會將無法辨識的列舉值視為遺漏,這也會造成必要的值檢查失敗。
新增更多訊息類型
您可以在單一 .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 序列化問題)。reserved
日後使用者若嘗試使用這些欄位 ID,通訊協定緩衝區編譯器就會提出申訴。
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
保留的欄位值範圍 (包含 9 to 11
與 9, 10,
11
相同)。請注意,您無法在同一個 reserved
陳述式中混用欄位名稱和欄位號碼。
你的 .proto
是哪些內容?
在 .proto
上執行通訊協定緩衝區編譯器時,編譯器會以您選擇的語言產生程式碼,您需要使用您在檔案中所描述的訊息類型,包括取得和設定欄位值、將訊息序列化到輸出串流,以及從輸入串流剖析訊息。
- 針對 C++,編譯器會從各個
.proto
產生.h
和.cc
檔案,其中包含檔案所描述的各種訊息類型的類別。 - 針對 Java,編譯器會產生每個訊息類型的類別的
.java
檔案,以及用於建立訊息類別執行個體的特殊Builder
類別。 - Python 略有不同,Python 編譯器會產生模組,其中包含
.proto
中每種訊息類型的靜態描述元,然後與 metaclass 搭配使用,以便在執行階段建立必要的 Python 資料存取類別。 - 針對 Go,編譯器會產生
.pb.go
檔案,其中包含檔案中每個訊息類型的類型。
如要進一步瞭解如何按照各種語言使用 API,請依據所選語言的教學課程操作。如需更多的 API 詳細資料,請參閱相關的 API 參考資料。
純量值類型
純量訊息欄位可以是下列其中一種類型:這個表格會顯示 .proto
檔案中指定的類型,以及自動產生的類別中的對應類型:
.proto 類型 | Notes | C++ 類型 | Java 類型 | Python 類型[2] | Go 類型 |
---|---|---|---|---|---|
雙精度值 | 雙精度值 | 雙精度值 | 浮動 | *float64 | |
浮動 | 浮動 | 浮動 | 浮動 | *float32 | |
int32 | 使用長度長度編碼。對負數的編碼作業效率不佳 – 如果您的欄位可能有負值,請改用 sint32。 | int32 | int | int | *整數 32 |
int64 | 使用長度長度編碼。對負數的編碼作業效率不佳 – 如果您的欄位可能有負值,請改用 sint64。 | int64 | long | int/long[3] | *int64 |
uint32 | 使用長度長度編碼。 | uint32 | 整數 [1] | int/long[3] | *uint32 |
烏特文 64 | 使用長度長度編碼。 | 烏特文 64 | 長[1] | int/long[3] | *u664 |
Sint32 | 使用長度長度編碼。帶正負號的值。相較於一般的 int32,這些編碼的編碼數值會更有效率。 | int32 | int | int | *整數 32 |
Sint64 | 使用長度長度編碼。帶正負號的值。比起一般的 int64,這些數字較有效率地編碼出負數。 | int64 | long | int/long[3] | *int64 |
固定 32 | 一律為 4 個位元組。如果值通常大於 228,則比 uint32 更有效率。 | uint32 | 整數 [1] | int/long[3] | *uint32 |
已修正 64 | 一律八個位元組。如果值通常大於 256,則比 uint64 更有效率。 | 烏特文 64 | 長[1] | int/long[3] | *u664 |
Sfixed32 | 一律為 4 個位元組。 | int32 | int | int | *整數 32 |
Sfixed64 | 一律八個位元組。 | int64 | long | int/long[3] | *int64 |
bool | bool | 布林值 | bool | *布林值 | |
string | 字串一律須包含 UTF-8 編碼文字。 | string | 字串 | unicode (Python 2) 或 str (Python 3) | *字串 |
位元組 | 可包含任何位元組序列。 | string | ByteString | 位元組 | []位元組 |
如要進一步瞭解這些類型編碼的方式,請參閱通訊協定緩衝區編碼訊息。
[1] 在 Java 中,未簽署的 32 位元和 64 位元整數會以已簽署的對應項目表示,頂端位元只會儲存在簽署位元中。
[2] 在任何情況下,設定欄位的值都會執行類型檢查,確保值有效。
[3] 解碼 64 位元或未簽署的 32 位元整數時,一律會解碼為長整數,但如果在設定欄位時指定了 int,則可以是整數。不論在何種情況下,該值都必須符合設定時所指定的類型。請參閱 [2]。
選填欄位和預設值
如前文所述,訊息說明中的元素可能會加上 optional
標籤。
格式正確的訊息不一定包含選用元素。剖析訊息時,如果訊息中沒有選用元素,存取剖析物件中對應的欄位,會傳回該欄位的預設值。你可以在訊息說明中指定預設值。舉例來說,假設您想為 SearchRequest
的 result_per_page
值提供 10 的預設值。
optional int32 result_per_page = 3 [default = 10];
如果未指定選用元素的預設值,則會改用類型專屬的預設值:針對字串,預設值為空白字串。以位元組來說,預設值為空位元組字串。如果是布林值,預設值為 false。對於數字類型,預設值為 0。對於列舉,預設值是列舉列舉定義中所列的第一個值。這表示在列舉值清單的開頭加入值時,必須謹慎處理。如要瞭解如何安全地變更定義,請參閱更新訊息類型一節。
列舉
定義訊息類型時,您可能會希望其中一個欄位僅有一個預先定義的值清單。例如,假設您想為每個 SearchRequest
新增 corpus
欄位,其中語料庫可以是 UNIVERSAL
、WEB
、IMAGES
、LOCAL
、NEWS
、PRODUCTS
或 VIDEO
。方法很簡單,只要在訊息定義中加入 enum
即可:enum
類型的欄位只能有一組指定的常數做為其值 (如果您嘗試提供其他值,剖析器會將此值視為不明欄位)。在以下範例中,我們新增了名為 Corpus
的 enum
以及所有可能的值,以及 Corpus
類型的欄位:
enum Corpus {
CORPUS_UNSPECIFIED = 0;
CORPUS_UNIVERSAL = 1;
CORPUS_WEB = 2;
CORPUS_IMAGES = 3;
CORPUS_LOCAL = 4;
CORPUS_NEWS = 5;
CORPUS_PRODUCTS = 6;
CORPUS_VIDEO = 7;
}
message SearchRequest {
required string query = 1;
optional int32 page_number = 2;
optional int32 result_per_page = 3 [default = 10];
optional Corpus corpus = 4 [default = CORPUS_UNIVERSAL];
}
將相同的值指派給不同的列舉常數,即可定義別名。
如要這麼做,您必須將 allow_alias
選項設為 true
。否則,通訊協定緩衝區編譯器會在找到別名時產生錯誤訊息。雖然在序列化時所有的別名值都是有效的,但序列化時會一律使用第一個值。
enum EnumAllowingAlias {
option allow_alias = true;
EAA_UNSPECIFIED = 0;
EAA_STARTED = 1;
EAA_RUNNING = 1;
EAA_FINISHED = 2;
}
enum EnumNotAllowingAlias {
ENAA_UNSPECIFIED = 0;
ENAA_STARTED = 1;
// ENAA_RUNNING = 1; // Uncommenting this line will cause a compile error inside Google and a warning message outside.
ENAA_FINISHED = 2;
}
列舉常數必須在 32 位元整數的範圍內。由於 enum
值在傳輸上使用變數編碼,因此負值的效率較低,因此不建議使用。您可以在訊息定義中或外部定義 enum
,您可以在 .proto
檔案中的任何訊息定義中重複使用這些 enum
。您也可以使用 enum
語法,在一則訊息中宣告的 enum
類型做為其他訊息中欄位的類型。
當您在採用 enum
的 .proto
上執行通訊協定緩衝區編譯器時,產生的程式碼將會有對應的 Java 或 C++ 專用 enum
,或用於 Python 的特殊 EnumDescriptor
類別,用於在執行階段產生的類別中建立具有整數值的符號值常數。
如要進一步瞭解如何在應用程式中使用訊息 enum
,請參閱所選語言的產生的程式碼指南。
保留值
如果您透過完全移除列舉項目或為註解加上註解更新列舉類型,日後使用者可在對類型進行更新時,可以重複使用數值。如果日後載入相同 .proto
的舊版本 (包括資料毀損、隱私權錯誤等),可能會造成嚴重問題。確保這種情況不會發生,方法是為已刪除項目指定數值 (和/或名稱,也可能會造成 JSON 序列化問題) 為 reserved
。日後使用者若使用這些 ID,通訊協定緩衝區編譯器就會提出申訴。您可以使用 max
關鍵字,指定保留的數值範圍所能達到的最大值。
enum Foo {
reserved 2, 15, 9 to 11, 40 to max;
reserved "FOO", "BAR";
}
請注意,您無法在同一個 reserved
陳述式中混用欄位名稱和數值。
使用其他訊息類型
您可以使用其他訊息類型做為欄位類型。舉例來說,假設您想在每則 SearchResponse
訊息中加入 Result
訊息,請在同一個 .proto
中定義 Result
訊息類型,然後在 SearchResponse
中指定 Result
類型的欄位:
message SearchResponse {
repeated Result result = 1;
}
message Result {
required string url = 1;
optional string title = 2;
repeated string snippets = 3;
}
匯入定義
在上述範例中,您要在與 SearchResponse
相同的檔案內定義 Result
訊息類型,如果另一個 .proto
檔案已定義要做為欄位類型的訊息類型,該怎麼辦?
如要使用其他 .proto
檔案的定義,請匯入這些檔案。如要匯入其他 .proto
的定義,請在檔案頂端新增匯入陳述式:
import "myproject/other_protos.proto";
根據預設,您只能使用直接匯入的 .proto
檔案來定義定義。不過,有時您可能需要將 .proto
檔案移至新位置。
您不必一次移動 .proto
檔案,並且一次更新所有呼叫網站,則可將預留位置 .proto
檔案放入舊位置,使用 import public
標記將所有匯入項目轉送至新的位置。
請注意,Java 不支援公開匯入功能。
任何匯入 import public
陳述式的 proto 的程式碼都可以遞移依賴 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 訊息中使用這些類型,反之亦然。不過,proto2 列舉不能用於 proto3 語法。
巢狀類型
您可以在其他訊息類型中定義及使用訊息類型,如以下範例所示: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;
}
您可以視需要建立訊息巢狀結構,在以下範例中,請注意名為 Inner
的兩個巢狀類型是完全獨立的,因為它們是在不同的訊息中定義:
message Outer { // Level 0
message MiddleAA { // Level 1
message Inner { // Level 2
optional int64 ival = 1;
optional bool booly = 2;
}
}
message MiddleBB { // Level 1
message Inner { // Level 2
optional string name = 1;
optional bool flag = 2;
}
}
}
群組
請注意,群組功能已淘汰,不應在建立新的訊息類型時使用。請改用巢狀訊息類型。
您也可以在訊息定義中以巢狀方式設定訊息的巢狀結構。例如,指定另一個包含 Result
的 SearchResponse
的方法如下:
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_」,或將欄位編號設為 保留,這樣
.proto
的未來使用者就無法意外重複使用號碼。 - 只要類型和編號保持不變,非必填欄位可以轉換為擴充功能,反之亦然。
int32
、uint32
、int64
、uint64
和bool
都相容,這表示您可以將欄位從其中一種類型變更為其他類型,而不會破壞前向或回溯相容性。如果從電線剖析不符合對應類型的行數,您將產生與使用 C++ 將類型投放到該類型的效果相同 (例如,如果將 64 位元的數字讀取為 int32,則會被截斷為 32 位元)。sint32
和sint64
彼此相容,但無法與其他整數類型相容。string
和bytes
相容,只要位元組有效,即符合 UTF-8 標準。- 如果位元組含有已編碼的訊息,則內嵌訊息與
bytes
相容。 fixed32
與sfixed32
相容,fixed64
則與sfixed64
相容。- 對於
string
、bytes
和訊息欄位,optional
與repeated
相容。以重複欄位的序列化資料做為輸入,如果這個欄位是optional
,用戶端將擷取最後一個輸入值 (如果是原始類型欄位),或合併所有輸入元素 (如果是訊息類型欄位)。請注意,對數值類型 (包括布林值和列舉) 通常並不安全。數值類型的重複欄位可透過 packed 格式序列化,如果預期為optional
欄位,系統就無法正確剖析這類欄位。 - 變更預設值通常是一般的,只要您記住,系統永遠不會透過預設值傳送預設值。因此,如果程式接收到未設定特定訊息的訊息,該程式將會看到其在程式版本的通訊協定中所定義的預設值。系統「不會」顯示寄件者程式碼中定義的預設值。
enum
與int32
、uint32
、int64
和uint64
的電匯格式相容 (請注意,如果值不符合,則系統會截斷值),但請注意,當用戶端解除訊息序列化時,用戶端代碼的處理方式可能有所不同。值得注意的是,在郵件解除序列化時,系統會捨棄無法辨識的enum
值,進而讓該欄位的has..
存取子傳回 false,其 getter 會傳回enum
定義中所列的第一個值,或指定預設值 (如果有指定預設值)。針對重複的列舉欄位,系統會將任何無法辨識的值從清單中移除。但是整數欄位一律會保留其值。因此,如果要在傳輸中收到邊界的列舉值,將整數升級至enum
時,請務必謹慎小心。- 在目前的 Java 和 C++ 實作中,一旦移除無法辨識的
enum
值,這些值就會與其他不明欄位一起儲存。請注意,如果這些資料序列化,然後由可識別這些值的用戶端重新剖析,這可能會導致異常行為。若是選填欄位,即使原始訊息反序列化後寫入了新值,用戶端可繼續讀取舊的值。若是重複的欄位,舊的值會顯示在任何已辨識和新增的值之後,這表示系統不會保留順序。 - 將單一
optional
欄位或擴充功能變更為新oneof
的成員與二進位檔相容,但在某些語言 (特別是 Go) 中,產生的程式碼的 API 會以不相容的方式變更。因此,Google 不會在其公開 API 中做出這類變更,如 AIP-180 中所述。如果對原始碼相容性有相同的注意事項,但如果您確定沒有一次設定多個程式碼,則將多個欄位移到新的oneof
可能安全無虞。無法將欄位移至現有的oneof
。同樣地,將單一欄位oneof
變更為optional
欄位或擴充功能是安全的。 - 變更
map<K, V>
與對應repeated
訊息欄位之間的欄位與二進位檔相容 (請參閱下方的地圖,瞭解訊息版面配置和其他限制)。但是,變更的安全性取決於應用程式:將訊息序列化和重新序列化時,使用repeated
欄位定義的用戶端會產生語意相同的結果;不過,使用map
欄位定義的用戶端可能會重新排序含有重複索引鍵的項目。
擴充功能
擴充功能可讓您宣告訊息中的特定欄位號碼可供第三方擴充功能使用。擴充欄位是預留位置,其類型未由原始 .proto
檔案定義。如此一來,其他 .proto
檔案就會以這些欄位編號來定義部分或全部欄位的類型,藉此為您的訊息定義加入訊息定義。以下面這段程式碼為例:
message Foo {
// ...
extensions 100 to 199;
}
這表示 Foo
中的欄位值編號 [100, 199] 的範圍會保留給擴充功能。其他使用者現在可以在匯入 .proto
的 .proto
檔案中,使用指定範圍內的欄位編號,在 Foo
中新增欄位,例如:
extend Foo {
optional int32 bar = 126;
}
這會在 Foo
的原始定義中新增名為 bar
且欄位號碼為 126 的欄位。
使用者的 Foo
訊息經過編碼後,傳輸格式會與使用者在 Foo
中定義新欄位完全相同。不過,在應用程式程式碼中存取擴充功能欄位的方式與存取一般欄位的方式略有不同:您產生的資料存取碼有專屬的存取子,可以處理擴充功能。舉例來說,以下是在 C++ 中設定 bar
值的方式:
Foo foo;
foo.SetExtension(bar, 15);
同樣地,Foo
類別可定義範本存取子 HasExtension()
、ClearExtension()
、GetExtension()
、MutableExtension()
和 AddExtension()
。所有函式的語意都符合一般欄位產生的對應存取子。如要進一步瞭解如何使用擴充功能,請參閱所選語言產生的程式碼參考資料。
請注意,擴充功能可以是任何欄位類型,包括訊息類型,但不能是其中一種或對應。
巢狀擴充功能
您可以在另一種類型的範圍內宣告擴充功能:
message Baz {
extend Foo {
optional int32 bar = 126;
}
...
}
在這個範例中,存取這個擴充功能的 C++ 程式碼如下:
Foo foo;
foo.SetExtension(Baz::bar, 15);
換句話說,bar
是定義在 Baz
範圍內的定義。
這是常見的混淆來源:在巢狀訊息中宣告巢狀結構的 extend
區塊並不暗示外部類型與擴充類型之間的關聯。請特別注意,以上範例並不表示 Baz
是 Foo
的任何子類別。只是表示 bar
符號是在 Baz
範圍內宣告;這只是靜態成員。
常見的模式是在擴充功能欄位類型的範圍內定義擴充功能。舉例來說,以下是 Baz
類型的 Foo
擴充功能,其中擴充功能定義為 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;
}
事實上,建議您採用這個語法以避免混淆。如前文所述,巢狀結構語法往往會錯誤,導致尚未熟悉擴充功能的使用者將子類別分類。
選擇擴充功能號碼
請務必確定兩位使用者不會使用相同的欄位號碼將擴充功能新增至相同訊息類型;如果擴充功能意外誤解為錯誤的類型,可能會導致資料損毀。建議您為專案定義擴充功能編號慣例,以避免發生這類情況。
如果您的編號慣例可能涉及到欄位數量極大的擴充欄位,您可以使用 max
關鍵字,指定擴充功能的範圍可達到最多的欄位值:
message Foo {
extensions 1000 to max;
}
max
為 229 - 1 或 536,870,911。
一般而言,選擇編號欄位時,編號編號也必須避免使用 19000 到 19999 (FieldDescriptor::kFirstReservedNumber
到 FieldDescriptor::kLastReservedNumber
) 的欄位號碼,因為這些格式已保留給通訊協定緩衝區。您可以定義包含這個範圍的擴充範圍,但通訊協定編譯器不允許您以這些數字定義實際擴充。
單人
如果訊息包含許多選用欄位,且同時可以設定最多一個欄位,您可以使用其中一項功能來強制執行此行為並節省記憶體。
其中一個欄位類似選用欄位,除了其中其中一個共用記憶體中的所有欄位,而且一次只能設定一個欄位。設定其中一個成員後,系統會自動清除所有其他成員。您可以使用特別的 case()
或 WhichOneof()
方法 (可視語言) 檢查其中一個設定的值 (如果有的話)。
使用 Oneof
如要在 .proto
中定義其中一種,請使用 oneof
關鍵字,後接您的名字,此例中為 test_oneof
:
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
然後將其中一個欄位新增至其中一項定義。您可以新增任何類型的欄位,但無法使用 required
、optional
或 repeated
關鍵字。如果您需要為其中一個欄位新增重複欄位,可以使用包含重複欄位的訊息。
在產生的程式碼中,其中一個欄位具有相同的 getter 和 setter 與一般 optional
方法相同。此外,您也提供了特殊的方法,用於檢查其中一個值 (如果有的話)。如要進一步瞭解所選語言的其中一個 API,請前往相關的 API 參考資料。
其中一項功能
設定其中一個欄位會自動清除其中一個其他所有成員。因此,如果您設定了多個欄位,則只有 last 欄位仍具有值。
SampleMessage message; message.set_name("name"); CHECK(message.has_name()); message.mutable_sub_message(); // Will clear name field. CHECK(!message.has_name());
如果剖析器遇到有線同個的多位成員,剖析訊息中只會使用最後偵測到的成員。
不支援其中一項擴充功能。
其中一個值不得為
repeated
。反思 API 適用於其中一個欄位。
如果您將其中一個欄位設為預設值 (例如將 int32 其中一個欄位設為 0),系統就會設定該其中一個欄位的「case」,並將該值在線上序列化。
如果您使用 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()
有兩個訊息與其中一個訊息,對每則訊息而言,最後一則訊息會與最後其中一個情況產生。在以下範例中,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());
回溯相容性問題
新增或移除其中一個欄位時,請務必小心謹慎。若是檢查其中一個項目的值為傳回 None
/NOT_SET
,可能表示該值尚未設定,或者已經設定在另一個版本的欄位中。無法分辨差異,因為無法知道傳輸上的未知欄位是否為其中任何欄位。
標記重複使用問題
- 將選填欄位移入或移出任一欄位:訊息序列化並剖析後,可能會遺失部分資訊 (部分欄位將會遭到清除)。不過,您可以放心將單一欄位安全移到「新的」欄位,而且如果知道多個欄位只設定為一個欄位,則可移動多個欄位。詳情請參閱更新訊息類型。
- 刪除其中一個欄位並加回:在訊息序列化和剖析後,這可能會清除您目前設定的其中一個欄位。
- 分割或合併其中:這與移動一般
optional
欄位有類似的問題。
地圖
如果您想要在資料定義中建立關聯地圖,通訊協定緩衝區會提供實用的捷徑語法:
map<key_type, value_type> map_field = N;
...其中 key_type
可以是任何整數或字串類型 (因此,除了浮點類型和 bytes
之外,任何「純量」類型)。請注意,列舉是無效的「key_type
」。value_type
可以是其他地圖以外的任何類型。
例如,如果您想建立專案地圖,其中每個 Project
訊息都與字串鍵相關聯,您可以定義如下:
map<string, Project> projects = 3;
產生的 Maps API 目前適用於所有支援 proto2 的語言。如要進一步瞭解所選語言的 Maps API,請參閱相關的 API 參考資料。
地圖功能
- 地圖不支援擴充功能。
- Google 地圖不得為
repeated
、optional
或required
。 - 對地圖值的有線順序排序和地圖疊代順序是未定義,因此您無法依賴地圖項目的順序。
- 為
.proto
產生文字格式時,地圖會依照金鑰排序。數字鍵會依數字排序。 - 從電線剖析或合併時,如果有重複的地圖金鑰,則會使用上次看到的金鑰。以文字格式剖析地圖時,如果重複的索引鍵,剖析可能會失敗。
回溯相容性
地圖語法相當於電匯上的下列內容,因此不支援通訊協定的通訊協定緩衝區實作仍可處理您的資料:
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
檔案指定套件,否則可能會導致描述元中的命名衝突,導致 proto 無法移植其他語言。
套件和名稱解析
通訊協定緩衝區語言中的類型名稱解析與 C++ 類似:先搜尋最內部範圍,然後搜尋到最內層,依此類推,每個套件都視為其父項套件的「內部」。開頭的「.」(例如 .foo.bar.Baz
) 表示從最外層開始。
通訊協定緩衝區編譯器會剖析匯入的 .proto
檔案,以解析所有類型名稱。每種語言的程式碼產生器都知道如何參照該語言的每種類型,即使其具有不同的範圍規則。
定義服務
如果您想使用 RPC (遠端程序呼叫) 系統的訊息類型,可以在 .proto
檔案中定義 RPC 服務介面,而通訊協定緩衝區編譯器將會以您選擇的語言產生服務介面程式碼和虛設常式。例如,如果您想使用可接受 SearchRequest
並傳回 SearchResponse
的方法來定義 RPC 服務,您可以在 .proto
檔案中定義該服務,如下所示:
service SearchService {
rpc Search(SearchRequest) returns (SearchResponse);
}
根據預設,通訊協定編譯器會產生名為 SearchService
的抽象介面,以及對應的「stub」實作。該虛設常式會將所有呼叫轉送至 RpcChannel
,而這必須是抽象介面,您必須根據自己的遠端程序呼叫 (RPC) 系統進行自我定義。舉例來說,您可以實作 RpcChannel
,將訊息序列化,並透過 HTTP 傳送到伺服器。換句話說,產生的虛設常式會提供類型安全介面,用於執行以通訊協定緩衝區的 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 語言指南。如果您想將 proto2 與 gRPC 搭配使用,必須使用通訊協定緩衝區編譯器和程式庫 3.0.0 版或更高版本。
除了 gRPC 以外,還有一些進行中的第三方專案,可為通訊協定緩衝區開發 RPC 實作。如需已知專案的連結清單,請參閱第三方外掛程式維基網頁。
選項
.proto
檔案中的個別宣告可以利用多項選項加上註解。選項不會變更宣告的整體意義,但可能會影響在特定情境中處理的方式。/google/protobuf/descriptor.proto
定義了可用選項的完整清單。
有些選項是檔案層級選項,表示應該在頂層範圍寫入,而非在訊息、列舉或服務定義中撰寫。有些選項是訊息層級選項,表示應寫入訊息定義。有些選項是欄位層級選項,表示應寫入欄位定義。您也可以透過列舉類型、列舉值、其中一個欄位、服務類型及服務方法,撰寫選項。不過,這些方法目前並沒有適用的選項。
以下是幾個最常用的選項:
java_package
(檔案選項):要用於產生的 Java 類別的套件。如果您未在.proto
檔案中提供明確的java_package
選項,則根據預設會使用 proto 套件 (在.proto
檔案中使用「package」關鍵字)。不過,proto 套件通常不會提供良好的 Java 套件,因為 proto 套件不應以反向網域名稱開頭。如果未產生 Java 程式碼,這個選項就不會生效。option java_package = "com.example.foo";
java_outer_classname
(檔案選項):您要產生的包裝函式 Java 類別的類別名稱 (以及檔案名稱)。如果.proto
檔案中並未明確指定java_outer_classname
,系統會將.proto
檔案名稱轉換為駝峰式大小寫 (將foo_bar.proto
變成FooBar.java
),藉此建構類別名稱。如果停用java_multiple_files
選項,則在外部外包裝函式 Java 類別「之間」產生的所有其他類別/列舉等項目將以巢狀類別/列舉等形式產生。如果未產生 Java 程式碼,這個選項就不會產生任何作用。option java_outer_classname = "Ponycopter";
java_multiple_files
(檔案選項):如果設為 false,這個.proto
檔案將只會產生一個.java
檔案,且為頂層訊息、服務和列舉建立的所有 Java 類別/enum 等均將在外部類別內建立 (請參閱java_outer_classname
)。如果為 true,系統會針對這個類別和類別的.java
類別,如果未產生 Java 程式碼,這個選項就不會產生任何作用。option java_multiple_files = true;
optimize_for
(檔案選項):可設為SPEED
、CODE_SIZE
或LITE_RUNTIME
。這會影響 C++ 和 Java 程式碼產生器 (可能包括第三方產生器):SPEED
(預設):通訊協定緩衝區編譯器會產生程式碼,用於對訊息類型進行序列化、剖析及執行其他一般作業。這組程式碼經過高度最佳化。CODE_SIZE
:通訊協定緩衝區編譯器會產生最少的類別,並採用共用、反映性的程式碼來實作序列化、剖析和各種其他作業。因此,產生的程式碼會遠小於SPEED
,但作業的執行速度就會變慢。類別仍會與在SPEED
模式下完全相同的公開 API。這種模式最適合包含非常大量.proto
檔案的應用程式,且不需要全部以盲目顯示。LITE_RUNTIME
:通訊協定緩衝區編譯器會產生僅依「lite」執行階段程式庫為基礎的類別 (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 實作將更適合使用程式碼產生器外掛程式,以產生更適合每個系統的程式碼,而不是依賴「抽象」的服務。// 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++ 產生的程式碼啟用區域配置。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
註解。對於 C++,每次使用已淘汰的欄位時,clang-tidy 都會產生警告。日後,其他語言專用的程式碼產生器可能會為該欄位的存取子產生淘汰註解,如此一來,在編譯嘗試使用該欄位的程式碼時,即會發出警告。如果有任何人未使用該欄位,且想要避免新使用者使用該欄位,請考慮將欄位宣告替換為 serveserve 陳述式。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
來定義新的訊息層級選項。當我們使用該選項時,必須使用括號括住選項名稱,以表示這是擴充功能。我們現在可以在 C++ 中讀取 my_option
的值,如下所示:
string value = MyMessage::descriptor()->options().GetExtension(my_option);
此處,MyMessage::descriptor()->options()
會傳回 MyMessage
的 MessageOptions
通訊協定訊息。讀取自訂選項就像讀取任何其他擴充功能一樣。
同樣地,在 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
。如果您尚未安裝編譯器,請下載套件並按照 README 的指示操作。
通訊協定編譯器的叫用方式如下:
protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR path/to/file.proto
IMPORT_PATH
會指定解析import
指令時要尋找.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 格式封存檔案。也會按照 Java JAR 規格的要求,提供.jar
輸出的資訊清單檔案。請注意,如果輸出封存檔已存在,系統會覆寫這個檔案,導致編譯器不夠聰明,無法將檔案新增至現有的封存檔。您必須提供一或多個
.proto
檔案做為輸入內容。可以一次指定多個.proto
檔案。雖然檔案是以目前的目錄命名,但每個檔案都必須位於其中一個IMPORT_PATH
中,編譯器才能判斷其標準名稱。
檔案位置
最好不要將 .proto
檔案放在其他語言來源所在的目錄中。請考慮在專案的根套件下為 .proto
檔案建立子套件 proto
。
地點應通用
使用 Java 程式碼時,建議您將相關的 .proto
檔案放在與 Java 來源相同的目錄中。不過,如有任何非 Java 程式碼都使用相同的 proto,路徑前置字串就不再合理。因此,通常將原型放在相關的通用語言目錄中,例如 //myteam/mypackage
。
這項規則的例外狀況是表明 proto 只會用於 Java 內容,例如用於測試。
支援平台
如需以下資訊:
- 支援的作業系統、編譯器、建構系統和 C++ 版本,請參閱基礎 C++ 支援政策。
- 支援的 PHP 版本,請參閱支援的 PHP 版本。