protobuf 語法,proto3 語法參考

紅牛慕課_韓忠康發表於2019-10-31

語言指導(proto3)

翻譯自:developers.google.com/protocol-bu…

關注 紅牛慕課,傳送 proto3 獲取該文件的 PDF 版本。

本指導描述瞭如何使用 protocol buffer 語言來構建 protocol buffer 資料,包括 .proto 檔案語法和如何基於該 .proto 檔案生成資料訪問類。本文是涵蓋 protocol buffer 語言 proto3 版本的內容,若需要 proto2 版本的資訊,請參考 Proto2 Language Guide

本文是語言指導——關於文中描述內容的分步示例,請參考所選程式語言的對應 tutorial (當前僅提供了 proto2,更多 proto3 的內容會持續更新)。

定義一個訊息型別

我們先看一個簡單示例。比如說我們想定義個關於搜尋請求的訊息,每個搜尋請求包含一個查詢字串,一個特定的頁碼,和每頁的結果數量。下面是用於定義訊息型別的 .proto 檔案:

syntax = "proto3";

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}
複製程式碼
  • 檔案的第一行指明瞭我們使用的是 proto3 語法:若不指定該行 protocol buffer 編譯器會認為是 proto2 。該行必須是檔案的第一個非空或非註釋行。
  • SearchRequest 訊息定義了三個欄位(名稱/值對),欄位就是每個要包含在該型別訊息中的部分資料。每個欄位都具有名稱和型別 。

指定欄位型別

上面的例子中,全部欄位都是標量型別:兩個整型(page_numberresult_per_page)和一個字串型(query)。同樣,也可以指定複合型別的欄位,包括列舉型和其他訊息型別。

分配欄位編號

正如你所見,訊息中定義的每個欄位都有一個唯一編號。欄位編號用於在訊息二進位制格式中標識欄位,同時要求訊息一旦使用欄位編號就不應該改變。注意一點 1 到 15 的欄位編號需要用 1 個位元組來編碼,編碼同時包括欄位編號和欄位型別( 獲取更多資訊請參考 Protocol Buffer Encoding )。16 到 2047 的欄位變化使用 2 個位元組。因此應將 1 到 15 的編號用在訊息的常用欄位上。注意應該為將來可能新增的常用欄位預留欄位編號。

最小的欄位編號為 1,最大的為 2^29 - 1,或 536,870,911。注意不能使用 19000 到 19999 (FieldDescriptor::kFirstReservedNumberFieldDescriptor::kLastReservedNumber)的欄位編號,因為是 protocol buffer 內部保留的——若在 .proto 檔案中使用了這些預留的編號 protocol buffer 編譯器會發出警告。同樣也不能使用之前預留的欄位編號。

指定欄位規則

訊息的欄位可以是一下規則之一:

  • singular , 格式良好的訊息可以有 0 個或 1 個該欄位(但不能多於 1 個)。這是 proto3 語法的預設欄位規則。
  • repeated ,格式良好的訊息中該欄位可以重複任意次數(包括 0 次)。重複值的順序將被保留。

在 proto3 中,標量數值型別的重複欄位預設會使用 packed 壓縮編碼。

更多關於 packed 壓縮編碼的資訊請參考 Protocol Buffer Encoding

增加更多訊息型別

單個 .proto 檔案中可以定義多個訊息型別。這在定義相關聯的多個訊息中很有用——例如要定義與搜尋訊息SearchRequest 相對應的回覆訊息 SearchResponse,則可以在同一個 .proto 檔案中增加它的定義:

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

message SearchResponse {
 ...
}
複製程式碼

增加註釋

使用 C/C++ 風格的 ///* ... */ 語法在 .proto 檔案新增註釋。

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

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.
}
複製程式碼

保留欄位

在採取徹底刪除或註釋掉某個欄位的方式來更新訊息型別時,將來其他使用者再更新該訊息型別時可能會重用這個欄位編號。後面再載入該 .ptoto 的舊版本時會引發好多問題,例如資料損壞,隱私漏洞等。一個防止該問題發生的辦法是將刪除欄位的編號(或欄位名稱,欄位名稱會導致在 JSON 序列化時產生問題)設定為保留項 reserved。protocol buffer 編譯器在使用者使用這些保留欄位時會發出警告。

message Foo {
  reserved 2, 15, 9 to 11;
  reserved "foo", "bar";
}
複製程式碼

注意,不能在同一條 reserved 語句中同時使用欄位編號和名稱。

.proto 檔案會生成什麼?

當 protocol buffer 編譯器作用於一個 .proto 檔案時,編輯器會生成基於所選程式語言的關於 .proto 檔案中描述訊息型別的相關程式碼 ,包括對欄位值的獲取和設定,序列化訊息用於輸出流,和從輸入流解析訊息。

  • 對於 C++, 編輯器會針對於每個 .proto 檔案生成.h.cc 檔案,對於每個訊息型別會生成一個類。
  • 對於 Java, 編譯器會生成一個 .java 檔案和每個訊息型別對應的類,同時包含一個特定的 Builder類用於構建訊息例項。
  • Python 有些不同 – Python 編譯器會對於 .proto 檔案中每個訊息型別生成一個帶有靜態描述符的模組,以便於在執行時使用 metaclass 來建立必要的 Python 資料訪問類。
  • 對於 Go, 編譯器會生成帶有每種訊息型別的特定資料型別的定義在.pb.go 檔案中。
  • 對於 Ruby,編譯器會生成帶有訊息型別的 Ruby 模組的 .rb 檔案。
  • 對於Objective-C,編輯器會針對於每個 .proto 檔案生成pbobjc.hpbobjc.m. 檔案,對於每個訊息型別會生成一個類。
  • 對於 C#,編輯器會針對於每個 .proto 檔案生成.cs 檔案,對於每個訊息型別會生成一個類。
  • 對於 Dart,編輯器會針對於每個 .proto 檔案生成.pb.dart 檔案,對於每個訊息型別會生成一個類。

可以參考所選程式語言的教程瞭解更多 API 的資訊。更多 API 詳細資訊,請參閱相關的 API reference

標量資料型別

訊息標量欄位可以是以下型別之一——下表列出了可以用在 .proto 檔案中使用的型別,以及在生成程式碼中的相關型別:

.proto Type Notes C++ Type Java Type Python Type[2] Go Type Ruby Type C# Type PHP Type Dart Type
double double double float float64 Float double float double
float float float float float32 Float float float double
int32 使用變長編碼。負數的編碼效率較低——若欄位可能為負值,應使用 sint32 代替。 int32 int int int32 Fixnum or Bignum (as required) int integer int
int64 使用變長編碼。負數的編碼效率較低——若欄位可能為負值,應使用 sint64 代替。 int64 long int/long[3] int64 Bignum long integer/string[5] Int64
uint32 使用變長編碼。 uint32 int[1] int/long[3] uint32 Fixnum or Bignum (as required) uint integer int
uint64 使用變長編碼。 uint64 long[1] int/long[3] uint64 Bignum ulong integer/string[5] Int64
sint32 使用變長編碼。符號整型。負值的編碼效率高於常規的 int32 型別。 int32 int int int32 Fixnum or Bignum (as required) int integer int
sint64 使用變長編碼。符號整型。負值的編碼效率高於常規的 int64 型別。 int64 long int/long[3] int64 Bignum long integer/string[5] Int64
fixed32 定長 4 位元組。若值常大於2^28 則會比 uint32 更高效。 uint32 int[1] int/long[3] uint32 Fixnum or Bignum (as required) uint integer int
fixed64 定長 8 位元組。若值常大於2^56 則會比 uint64 更高效。 uint64 long[1] int/long[3] uint64 Bignum ulong integer/string[5] Int64
sfixed32 定長 4 位元組。 int32 int int int32 Fixnum or Bignum (as required) int integer int
sfixed64 定長 8 位元組。 int64 long int/long[3] int64 Bignum long integer/string[5] Int64
bool bool boolean bool bool TrueClass/FalseClass bool boolean bool
string 包含 UTF-8 和 ASCII 編碼的字串,長度不能超過 2^32 。 string String str/unicode[4] string String (UTF-8) string string String
bytes 可包含任意的位元組序列但長度不能超過 2^32 。 string ByteString str []byte String (ASCII-8BIT) ByteString string List<int>

可以在 Protocol Buffer Encoding 中獲取更多關於訊息序列化時型別編碼的相關資訊。

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

[2] 所有情況下,設定欄位的值將執行型別檢查以確保其有效。

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

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

[5] Integer 用於 64 位機器,string 用於 32 位機器。

預設值

當解析訊息時,若訊息編碼中沒有包含某個元素,則相應的會使用該欄位的預設值。預設值依據型別而不同:

  • 字串型別,空字串
  • 位元組型別,空位元組
  • 布林型別,false
  • 數值型別,0
  • 列舉型別,第一個列舉元素
  • 內嵌訊息型別,依賴於所使用的程式語言。參考 generated code guide 獲取詳細資訊。

對於可重複型別欄位的預設值是空的( 通常是相應語言的一個空列表 )。

注意一下標量欄位,在訊息被解析後是不能區分欄位是使用預設值(例如一個布林型欄位是否被設定為 false )賦值還是被設定為某個值的。例如你不能通過對布林值等於 false 的判斷來執行一個不希望在預設情況下執行的行為。同時還要注意若一個標量欄位設定為預設的值,那麼是不會被序列化以用於傳輸的。

檢視 generated code guide 來獲得更多關於程式語言生成程式碼的內容。

列舉

定義訊息型別時,可能需要某欄位值是一些預設值之一。例如當需要在 SearchRequest 訊息型別中增加一個 corpus 欄位, corpus 欄位的值可以是 UNIVERSALWEBIMAGESLOCALNEWSPRODUCTSVIDEO。僅僅需要在訊息型別中定義帶有預設值常量的 enum 型別即可完成上面的定義。

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_alisa 選項設定為 true,否則 protocol 編譯器當發現別名定義時會報錯。

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 位整數的範圍內。因為列舉值在傳輸時採用的是 varint 編碼,同時負值無效因而不建議使用。可以如上面例子所示,將列舉定義在訊息型別內,也可以將其定義外邊——這樣該列舉可以用在 .proto 檔案中定義的任意的訊息型別中以便重用。還可以使用 MessageType.EnumType 語法將列舉定義為訊息欄位的某一資料型別。

使用 protocol buffer 編譯器編譯 .proto 中的列舉時,對於 Java 或 C 會生成相應的列舉型別,對於 Python 會生成特定的 EnumDescriptor 類用於在執行時建立一組整型值符號常量即可。

反序列化時,未識別的列舉值會被保留在訊息內,但如何表示取決於程式語言。若語言支援開放列舉型別允許範圍外的值時,這些未識別的列舉值簡單的以底層整型進行儲存,就像 C++ 和 Go。若語言支援封閉列舉型別例如 Java,一種情況是使用特殊的訪問器(譯註:accessors)來訪問底層的整型。無論哪種語言,序列化時的未識別列舉值都會被保留在序列化結果中。

更多所選語言中關於列舉的處理,請參考 generated code guide

保留值

在採取徹底刪除或註釋掉某個列舉值的方式來更新列舉型別時,將來其他使用者再更新該列舉型別時可能會重用這個列舉數值。後面再載入該 .ptoto 的舊版本時會引發好多問題,例如資料損壞,隱私漏洞等。一個防止該問題發生的辦法是將刪除的列舉數值(或名稱,名稱會導致在 JSON 序列化時產生問題)設定為保留項 reserved。protocol buffer 編譯器在使用者使用這些特定數值時會發出警告。可以使用 max 關鍵字來指定保留值的範圍到最大可能值。

enum Foo {
  reserved 2, 15, 9 to 11, 40 to max;
  reserved "FOO", "BAR";
}
複製程式碼

注意不能在 reserved 語句中混用欄位名稱和數值。

使用其他訊息型別

訊息型別也可作為欄位型別。例如,我們需要在 SearchResponse 訊息中包含 Result 訊息——想要做到這一點,可以將 Result 訊息型別的定義放在同一個 .proto 檔案中同時在 SearchResponse 訊息中指定一個 Result 型別的欄位:

message SearchResponse {
  repeated Result results = 1;
}

message Result {
  string url = 1;
  string title = 2;
  repeated string snippets = 3;
}	
複製程式碼

匯入定義

前面的例子中,我們將 Result 訊息定義在了與 SearchResponse 相同的檔案中——但若我們需要作為欄位型別使用的訊息型別已經定義在其他的 .proto 檔案中了呢?

可以通過匯入操作來使用定義在其他 .proto 檔案中的訊息定義。在檔案的頂部使用 import 語句完成匯入其他 .proto 檔案中的定義:

import "myproject/other_protos.proto";
複製程式碼

預設情況下僅可以通過直接匯入 .proto 檔案來使用這些定義。然而有時會需要將 .proto 檔案移動位置。可以通過在原始位置放置一個偽 .proto 檔案使用 import public 概念來轉發對新位置的匯入,而不是在發生一點更改時就去更新全部對舊檔案的匯入位置。任何匯入包含 import public 語句的 proto 檔案就會對其中的 import public 依賴產生傳遞依賴。例如:

// new.proto
// 全部定義移動到該檔案
複製程式碼
// old.proto
// 這是在客戶端中匯入的偽檔案
import public "new.proto";
import "other.proto";
複製程式碼
// client.proto
import "old.proto";
// 可使用 old.proto 和 new.proto 中的定義,但不能使用 other.proto 中的定義
複製程式碼

protocol 編譯器會使用命令列引數 -I/--proto_path 所指定的目錄集合中檢索需要匯入的檔案。若沒有指定,會在呼叫編譯器的目錄中檢索。通常應該將 --proto_path 設定為專案的根目錄同時在 import 語句中使用全限定名。

使用 proto2 型別

可以在 proto3 中匯入 proto2 定義的訊息型別,反之亦然。然而,proto2 中的列舉不能直接用在 proto3 語法中(但匯入到 proto2 中 proto3 定義的列舉是可用的)。

巢狀型別

可以在一個訊息型別中定義和使用另一個訊息型別,如下例所示—— Result 訊息型別定義在了 SearchResponse 訊息型別中:

message SearchResponse {
  message Result {
    string url = 1;
    string title = 2;
    repeated string snippets = 3;
  }
  repeated Result results = 1;
}
複製程式碼

使用 Parent.Type 語法可以在父級訊息型別外重用內部定義訊息型別:

message SomeOtherMessage {
  SearchResponse.Result result = 1;
}
複製程式碼

支援任意深度的巢狀:

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;
    }
  }
}
複製程式碼

訊息型別的更新

如果現有的訊息型別不再滿足您的所有需求——例如,需要擴充套件一個欄位——同時還要繼續使用已有程式碼,別慌! 在不破壞任何現有程式碼的情況下更新訊息型別非常簡單。僅僅遵循如下規則即可:

  • 不要修改任何已有欄位的欄位編號
  • 若是新增新欄位,舊程式碼序列化的訊息仍然可以被新程式碼所解析。應該牢記新元素的預設值以便於新程式碼與舊程式碼序列化的訊息進行互動。類似的,新程式碼序列化的訊息同樣可以被舊程式碼解析:舊程式碼解析時會簡單的略過新欄位。參考未知欄位獲取詳細資訊。
  • 欄位可被移除,只要不再使用移除欄位的欄位編號即可。可能還會對欄位進行重新命名,或許是增加字首 OBSOLETE_ ,或保留欄位編號以保證後續不能重用該編號。
  • int32uint32int64uint64, 和 bool 是完全相容的——意味著可以從這些欄位其中的一個更改為另一個而不破壞前後相容性。若解析出來的數值與相應的型別不匹配,會採用與 C++ 一致的處理方案(例如,若將 64 位整數當做 32 位進行讀取,則會被轉換為 32 位)。
  • sint32sint64 相互相容但不與其他的整型相容。
  • string and bytes 在合法 UTF-8 位元組前提下也是相容的。
  • 巢狀訊息與 bytes 在 bytes 包含訊息編碼版本的情況下也是相容的。
  • fixed32sfixed32 相容, fixed64sfixed64相容。
  • enumint32uint32int64,和 uint64 相容(注意若值不匹配會被截斷)。但要注意當客戶端反序列化訊息時會採用不同的處理方案:例如,未識別的 proto3 列舉型別會被儲存在訊息中,但是當訊息反序列化時如何表示是依賴於程式語言的。整型欄位總是會保持其的值。
  • 將一個單獨值更改為新 oneof 型別成員之一是安全和二進位制相容的。 若確定沒有程式碼一次性設定多個值那麼將多個欄位移入一個新 oneof 型別也是可行的。將任何欄位移入已存在的 oneof 型別是不安全的。

未知欄位

未知欄位是解析結構良好的 protocol buffer 已序列化資料中的未識別欄位的表示方式。例如,當舊程式解析帶有新欄位的資料時,這些新欄位就會成為舊程式的未知欄位。

本來,proto3 在解析訊息時總是會丟棄未知欄位,但在 3.5 版本中重新引入了對未知欄位的保留機制以用來相容 proto2 的行為。在 3.5 或更高版本中,未知欄位在解析時會被保留同時也會包含在序列化結果中。

Any 型別

Any 型別允許我們將沒有 .proto 定義的訊息作為內嵌型別來使用。一個 Any 包含一個類似 bytes 的任意序列化訊息,以及一個 URL 來作為訊息型別的全域性唯一識別符號。要使用 Any 型別,需要匯入 google/protobuf/any.proto

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() 方法:

// 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 型別的位於 extensions 部分。

Oneof

若一個含有多個欄位的訊息同時大多數情況下一次僅會設定一個欄位,就可以使用 oneof 特性來強制該行為同時節約記憶體。

Oneof 欄位除了全部欄位位於 oneof 共享記憶體以及大多數情況下一次僅會設定一個欄位外與常規欄位類似。對任何oneof 成員的設定會自動清除其他成員。可以通過 case()WhichOneof() 方法來檢測 oneof 中的哪個值被設定了,這個需要基於所選的程式語言。

使用 oneof

使用 oneof 關鍵字在 .proto 檔案中定義 oneof,同時需要跟隨一個 oneof 的名字,就像本例中的 test_oneof

message SampleMessage {
  oneof test_oneof {
    string name = 4;
    SubMessage sub_message = 9;
  }
}
複製程式碼

然後將欄位新增到 oneof 的定義中。可以增加任意型別的欄位,但不能使用 repeated 欄位。

在生成的程式碼中,oneof 欄位和常規欄位一致具有 getters 和 setters 。同時也會獲得一個方法以用於檢測哪個值被設定了。更多所選程式語言中關於 oneof 的 API 可以參考 API reference

Oneof 特性

  • 設定 oneof 的一個欄位會清除其他欄位。因此入設定了多次 oneof 欄位,僅最後設定的欄位生效。
SampleMessage message;
message.set_name("name");
CHECK(message.has_name());
message.mutable_sub_message();   // 會清理 name 欄位
CHECK(!message.has_name());
複製程式碼
  • 若解析器在解析得到的資料時碰到了多個 oneof 的成員,最後一個碰到的是最終結果。
  • oneof 不能是 repeated
  • 反射 API 可作用於 oneof 欄位。
  • 若將一個 oneof 欄位設為了預設值(就像為 int32 型別設定了 0 ),那麼 oneof 欄位會被設定為 "case",同時在序列化編碼時使用。
  • 若使用 C++ ,確認程式碼不會造成記憶體崩潰。以下的示例程式碼就會導致崩潰,因為 sub_message 在呼叫 set_name() 時已經被刪除了。
SampleMessage message;
SubMessage* sub_message = message.mutable_sub_message();
message.set_name("name");      // 會刪除 sub_message
sub_message->set_...     		// 此處會崩潰
複製程式碼
  • 同樣在 C++ 中,若 Swap() 兩個 oneof 訊息,那麼訊息會以另一個訊息的 oneof 的情況:下例中,msg1會是 sub_message1msg2 中會是 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 欄位時要當心。若檢測到 oneof 的值是 None/NOT_SET,這意味著 oneof 未被設定或被設定為一個不同版本的 oneof 欄位。沒有方法可以區分,因為無法確定一個未知欄位是否是 oneof 的成員。

標記重用問題

  • 移入或移出 oneof 欄位: 訊息序列化或解析後,可能會丟失一些資訊(某些欄位將被清除)。然而,可以安全地將單個欄位移入新的 oneof 中,同樣若確定每次操作只有一個欄位被設定則可以移動多個欄位。
  • 刪除一個 oneof 欄位並又將其加回: 訊息序列化和解析後,可能會清除當前設定的 oneof 欄位。
  • 拆分或合併 oneof:這與移動常規欄位有類似的問題。

Map 對映表

若需要建立關聯對映表作為定義的資料的一部分,protocol buffers 提供了方便的快捷語法:

map<key_type, value_type> map_field = N;
複製程式碼

key_type 處可以是整型或字串型別(其實是除了 float 和 bytes 型別外任意的標量型別)。注意列舉不是合法的 key_typevalue_type 是除了 map 外的任意型別。

例如,若需要建立每個專案與一個字串 key 相關聯的對映表,可以採用下面的定義:

map<string, Project> projects = 3;
複製程式碼
  • 對映表欄位不能為 repeated
  • 對映表的編碼和迭代順序是未定義的,因此不能依賴對映表元素的順序來操作。
  • 當基於 .proto 生成文字格式時,對映表的元素基於 key 來排序。數值型的 key 基於數值排序。
  • 當解析或合併時,若出現衝突的 key 以最後一個 key 為準。當從文字格式解析時,若 key 衝突則會解析失敗。
  • 若僅僅指定了對映表中某個元素的 key 而沒有指定 value,當序列化時的行為是依賴於程式語言。在 C++,Java,和 Python 中使用型別的預設值來序列化,但在有些其他語言中可能不會序列化任何東西。

生成的對映表 API 當前可用於全部支援 proto3 的程式語言。在 API reference 中可以獲取更多關於對映表 API 的內容。

向後相容問題

對映表語法與以下程式碼是對等的,因此 protocol buffers 的實現即使不支援對映表也可以正常處理資料:

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

repeated MapFieldEntry map_field = N;
複製程式碼

任何支援對映表的 protocol buffers 實現都必須同時處理和接收上面程式碼的資料定義。

可以在 .proto 檔案中使用 package 指示符來避免 protocol 訊息型別間的命名衝突。

package foo.bar;
message Open { ... }
複製程式碼

這樣在定義訊息的欄位型別時就可以使用包指示符來完成:

message Foo {
  ...
  foo.bar.Open open = 1;
  ...
}
複製程式碼

包指示符的處理方式是基於程式語言的:

  • C++ 中生成的類位於名稱空間中。例如,Open 會位於名稱空間 foo::bar 中。
  • Java 中,使用 Java 的包,除非在 .proto 檔案中使用 option java_pacakge 做成明確的指定。
  • Python 中,package 指示符被忽略,這是因為 Python 的模組是基於檔案系統的位置來組織的。
  • Go 中,作為 Go 的包名來使用,除非在 .proto 檔案中使用 option java_pacakge 做成明確的指定。
  • Ruby 中,生成的類包裹於 Ruby 的名稱空間中,還要轉換為 Ruby 所需的大小寫風格(首字母大寫;若首字元不是字母,則使用 PB_ 字首)。例如,Open 會位於名稱空間 Foo::Bar 中。
  • C# 中作為名稱空間來使用,同時需要轉換為 PascalCase 風格,除非在 .proto 使用 option csharp_namespace 中明確的指定。例如,Open 會位於名稱空間 Foo.Bar 中。

包和名稱解析

protocol buffer 中型別名稱解析的工作機制類似於 C++ :先搜尋最內層作用域,然後是次內層,以此類推,每個包被認為是其外部包的內層。前導點(例如,.foo.bar.Baz)表示從最外層作用域開始。

protocol buffer 編譯器會解析匯入的 .proto 檔案中的全部型別名稱。基於程式語言生成的程式碼也知道如何去引用每種型別,即使程式語言有不同的作用域規則。

定義服務

若要在 RPC (Remote Procedure Call,遠端過程呼叫)系統中使用我們定義的訊息型別,則可在 .proto 檔案中定義這個 RPC 服務介面,同時 protocol buffer 編譯器會基於所選程式語言生成該服務介面程式碼。例如,若需要定義一個含有可以接收 SearchRequest 訊息並返回 SearchResponse 訊息方法的 RPC 服務,可以在 .proto 檔案中使用如下程式碼定義:

service SearchService {
  rpc Search (SearchRequest) returns (SearchResponse);
}
複製程式碼

最直接使用 protocal buffer 的 RPC 系統是 gRPC :一款 Google 開源,語言和平臺無關的 RPC 系統。gRPC 對 protocol buffer 的支援非常好同時允許使用特定的 protocol buffer 編譯器外掛來基於 .proto 檔案生成相關的程式碼。

若不想使用 gRPC,同樣可以在自己的 RPC 實現上使用 protocol buffer。可以在 Proto2 Language Guide 處獲得更多關於這方面的資訊。

同樣也有大量可用的第三方使用 protocol buffer 的專案。對於我們瞭解的相關專案列表,請參考 third-party add-ons wiki page

JSON 對映

Proto3 支援 JSON 的規範編碼,這使得系統間共享資料變得更加容易。下表中,將逐型別地描述這些編碼。

若 JSON 編碼中不存在某個值或者值為 null,當將其解析為 protocol buffer 時會解析為合適的預設值。若 procol buffer 中使用的是欄位的預設值,則預設情況下 JSON 編碼會忽略該欄位以便於節省空間。實現上應該提供一個選項以用來將具有預設值的欄位生成在 JSON 編碼中。

proto3 JSON JSON 示例 說明
message object {"fooBar": v, "g": null,…} 生成 JSON 物件。訊息欄位名對映為物件的 lowerCamelCase(譯著:小駝峰) 的 key。若指定了 json_name 選項,則使用該選項值作為 key。解析器同時支援 lowerCamelCase 名稱(或 json_name 指定名稱)和原始 proto 欄位名稱。全部型別都支援 null 值,是當做對應型別的預設值來對待的。
enum string "FOO_BAR" 使用 proto 中指定的列舉值的名稱。解析器同時接受列舉名稱和整數值。
map<K,V> object `{"k": v, …} 所有的 key 被轉換為字串型別。
repeated V array [v, …] null 被解釋為空列表 []。
bool true, false true, false
string string "Hello World!"
bytes base64 string "YWJjMTIzIT8kKiYoKSctPUB+" JSON 值是使用標準邊界 base64 編碼的字串。不論標準或 URL 安全還是攜帶邊界與否的 base64 編碼都支援。
int32, fixed32, uint32 number 1, -10, 0 JSON 值是 10 進位制數值。數值或字串都可以支援。
int64, fixed64, uint64 string "1", "-10" JSON 值是 10 進位制字串。數值或字串都支援。
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}。否則,會被轉換到一個物件中,同時會插入一個 "@type" 元素用以指明實際的型別。
Timestamp string "1972-01-01T10:00:20.021Z" 採用 RFC 3339 格式,其中生成的輸出總是 Z規範的,並使用 0、3、6 或 9 位小數。除 “Z” 以外的偏移量也可以。
Duration string "1.000340012s", "1s" 根據所需的精度,生成的輸出可能會包含 0、3、6 或 9 位小數,以 “s” 為字尾。只要滿足納秒精度和字尾 “s” 的要求,任何小數(包括沒有)都可以接受。
Struct object { … } 任意 JSON 物件。參見 struct.proto.
Wrapper types various types 2, "2", "foo", true,"true", null, 0, … 包裝器使用與包裝的原始型別相同的 JSON 表示,但在資料轉換和傳輸期間允許並保留 null。
FieldMask string "f.fooBar,h" 參見field_mask.proto
ListValue array [foo, bar, …]
Value value Any JSON value
NullValue null JSON null
Empty object {} 空 JSON 物件

JSON 選項

proto3 的 JSON 實現可以包含如下的選項:

  • 省略使用預設值的欄位:預設情況下,在 proto3 的 JSON 輸出中省略具有預設值的欄位。該實現可以使用選項來覆蓋此行為,來在輸出中保留預設值欄位。
  • 忽略未知欄位:預設情況下,proto3 的 JSON 解析器會拒絕未知欄位,同時提供選項來指示在解析時忽略未知欄位。
  • 使用 proto 欄位名稱代替 lowerCamelCase 名稱: 預設情況下,proto3 的 JSON 編碼會將欄位名稱轉換為 lowerCamelCase(譯著:小駝峰)形式。該實現提供選項可以使用 proto 欄位名代替。Proto3 的 JSON 解析器可同時接受 lowerCamelCase 形式 和 proto 欄位名稱。
  • 列舉值使用整數而不是字串表示: 在 JSON 編碼中列舉值是使用列舉值名稱的。提供了可以使用列舉值數值形式來代替的選項。

選項

.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_multiple_files (檔案選項):導致將頂級訊息、列舉、和服務定義在包級,而不是在以 .proto 檔案命名的外部類中。
option java_multiple_files = true;
複製程式碼
  • java_outer_classname(檔案選項):想生成的最外層 Java 類(也就是檔名)。若沒有在 .proto 檔案中明確指定 java_outer_classname 選項,類名將由 .proto 檔名轉為 camel-case 來構造(因此 foo_bar.proto 會變為 FooBar.java)。若不生成 Java 程式碼,則此選項無效。
option java_outer_classname = "Ponycopter";
複製程式碼
  • optimize_for (檔案選項): 可被設為 SPEEDCODE_SIZE,或 LITE_RUNTIME。這會影響 C++ 和 Java 程式碼生成器(可能包含第三方生成器) 的以下幾個方面:
  • SPEED (預設): protocol buffer 編譯器將生成用於序列化、解析和訊息型別常用操作的程式碼。生成的程式碼是高度優化的。
  • CODE_SIZE :protocol buffer 編譯器將生成最小化的類,並依賴於共享的、基於反射的程式碼來實現序列化、解析和各種其他操作。因此,生成的程式碼將比 SPEED 模式小的多,但操作將變慢。類仍將實現與 SPEED 模式相同的公共 API。這種模式在處理包含大量 .proto 檔案同時不需要所有操作都要求速度的應用程式中最有用。
  • LITE_RUNTIME :protocol buffer 編譯器將生成僅依賴於 “lite” 執行庫的類(libprotobuf-lite 而不是libprotobuf)。lite 執行時比完整的庫小得多(大約小一個數量級),但會忽略某些特性,比如描述符和反射。這對於在受限平臺(如行動電話)上執行的應用程式尤其有用。編譯器仍然會像在 SPEED 模式下那樣生成所有方法的快速實現。生成的類將僅用每種語言實現 MessageLite 介面,該介面只提供 Message 介面的一個子集。
option optimize_for = CODE_SIZE;	
複製程式碼
  • cc_enable_arenas(檔案選項):為生成的 C++ 程式碼啟用 arena allocation
  • objc_class_prefix (檔案選項): 設定當前 .proto 檔案生成的 Objective-C 類和列舉的字首。沒有預設值。你應該使用 recommended by Apple 的 3-5 個大寫字母作為字首。注意所有 2 個字母字首都由 Apple 保留。
  • deprecated (欄位選項):若設定為 true, 指示該欄位已被廢棄,新程式碼不應使用該欄位。在大多數語言中,這沒有實際效果。在 Java 中,這變成了一個 @Deprecated 註釋。將來,其他語言的程式碼生成器可能會在欄位的訪問器上生成棄用註釋,這將導致在編譯試圖使用該欄位的程式碼時發出警告。如果任何人都不使用該欄位,並且您希望阻止新使用者使用它,那麼可以考慮使用保留語句替換欄位宣告。
int32 old_field = 6 [deprecated=true];
複製程式碼

自定義選項

protocol buffer 還允許使用自定義選項。大多數人都不需要此高階功能。若確認要使用自定義選項,請參閱 Proto2 Language Guide 瞭解詳細資訊。注意使用 extensions 來建立自定義選項,只允許用於 proto3 中。

生成自定義類

若要生成操作 .proto 檔案中定義的訊息型別的 Java、Python、C++、Go、Ruby、Objective-C 或 C# 程式碼,需要對 .proto 檔案執行 protocol buffer 編譯器 protoc。若還沒有安裝編譯器,請 download the package 並依據 README 完成安裝。對於 Go ,還需要為編譯器安裝特定的程式碼生成器外掛:可使用 GitHub 上的 golang/protobuf 庫。

Protocol buffer 編譯器的呼叫方式如下:

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 --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto
複製程式碼
  • IMPORT_PATHimport 指令檢索 .proto 檔案的目錄。若未指定,使用當前目錄。多個匯入目錄可以通過多次傳遞 --proto_path 選項實現;這些目錄會依順序檢索。 -I=*IMPORT_PATH* 可作為 --proto_path 的簡易格式使用。

  • 可以提供一個或多個輸出指令:

  • --cpp_outDST_DIR目錄 生成 C++ 程式碼。參閱 C++ generated code reference 獲取更多資訊。

  • --java_outDST_DIR目錄 生成 Java 程式碼。參閱 Java generated code reference 獲取更多資訊。

  • --python_outDST_DIR目錄 生成 Python程式碼。參閱 Python generated code reference 獲取更多資訊。

  • --go_outDST_DIR目錄 生成 Go 程式碼。參閱 Go generated code reference 獲取更多資訊。

  • --ruby_outDST_DIR目錄 生成 Ruby 程式碼。 coming soon!

  • --objc_outDST_DIR目錄 生成 Objective-C 程式碼。參閱 Objective-C generated code reference 獲取更多資訊。

  • --csharp_outDST_DIR目錄 生成 C# 程式碼。參閱 C# generated code reference 獲取更多資訊。

  • --php_outDST_DIR目錄 生成 PHP程式碼。參閱 PHP generated code reference 獲取更多資訊。

作為額外的便利,若 DST_DIR 以 .zip.jar 結尾,編譯器將會寫入給定名稱的 ZIP 格式壓縮檔案,.jar 還將根據 Java JAR 的要求提供一個 manifest 檔案。請注意,若輸出檔案已經存在,它將被覆蓋;編譯器還不夠智慧,無法將檔案新增到現有的存檔中。

  • 必須提供一個或多個 .proto 檔案作為輸入。可以一次指定多個 .proto 檔案。雖然這些檔案是相對於當前目錄命名的,但是每個檔案必須駐留在 IMPORT_PATHs 中,以便編譯器可以確定它的規範名稱。

關注 紅牛慕課,傳送 proto3 獲取該文件的 PDF 版本。