protobuf 3 版本文檔翻譯

  1.  protobuf 語法 -(proto3) =========================

本文描述瞭如何使用 prototol buffer(簡稱 pb)語言來構建你的 pb 數據,內容包括:後綴爲. proto 的文件語法(語法:syntax)以及如何從. proto 文件生成自己語言的數據訪問類(數據訪問類:data access class)。本文檔中使用 pb 的 proto3 版本,proto2 版本的 pb 語法看 Proto2 Language Guide。

https://developers.google.com/protocol-buffers/docs/proto

Proto 2 language guide

本文是一個參考指南,用來一步一步展示文檔中的各種特性。

1.1.  定義一個 Message

首先,看一個非常簡單的示例。假如你想定義一個搜索請求(搜索請求:search request)的 message,那麼就需要有 query 的 string 字段、你感興趣的結果所在的數據頁的頁碼 page_number 以及每頁上的結果的數量 result_per_page。

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

1.1.1.  聲明字段類型

上面的代碼中,所有的字段都是 scale 類型:兩個 integer 類型(page_number 和 result_per_page)以及一個 string 類型(query)。然而,你也可以把字段定義成複合類型(複合類型:composite type),包括枚舉類型(枚舉類型:enumerations)和其他的 message 類型。

1.1.2.  分配字段編號

如你所見,message 定義中的每個字段都有一個唯一編號。這些編號用來在 message 編碼後的二進制數據中來區分各個字段,一旦你的 message 開始使用就不可以改變其字段的編號。1-15 的字段號使用一個字節進行編碼,內容包括字段號和字段類型(在 Protocol Buffer Encoding 這一章中可以找到更多編碼信息。16-2047 的字段號佔用兩個字節進行編碼。因此,你應該把 1-15 保留爲經常使用的字段編號。記得要保留幾個 1-15 的編號,以便爲後續添加的頻繁使用的字段進行編號。

https://developers.google.com/protocol-buffers/docs/encoding#structure

Protocol buffer encoding

可以使用的最小字段號是 1,最大是  或 536870911. 你不可以使用 19000 到 19999 作爲字段號,因爲它們是爲 Protocol Buffer 的實現而保留的,如果你在. proto 文件中用了某一個保留的字段號,pb 的編譯器就會報錯。同樣的,你也不可以使用之前使用 reserved 標記的保留字段號。

1.1.3.  指定字段規則

message 的字段可以是下面的一種:

在 proto3 中,repeated 修飾的字段默認使用 packed 編碼。

在 Protocol Buffer Encoding 中可以找到更多關於 packed 編碼的信息。

https://developers.google.com/protocol-buffers/docs/encoding#packed

Protocol buffer encoding

1.1.4.  添加更多的 message

在一個. proto 文件中可以定義多個 message。如果你想定義多個有聯繫的 message,這個特性是很有用的。例如,你想定義 SearchResponse 作爲你的響應 message,你就可以把它加到相同的. proto 文件中。

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

1.1.5.  添加註釋

在. proto 文件中,使用 C/C++ 風格的註釋語法:// 或 /.../

 /* 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.
 }

1.1.6.  保留字段

如果你通過刪除或註釋字段修改了一個 message,之後的開發者在修改這個 message 的時候可能會重新使用這個字段號。如果他們之後加載舊版本的. proto 文件,可能導致嚴重的問題,例如數據污染、權限漏洞等。避免這個問題的方法就是指定你刪除的字段號(和字段名,因爲字段名重複使用也可能導致 JSON 序列化的問題)是 reserved。後續的開發者如果想使用這些字段標識,pb 編譯器都會報錯。

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

注意,在同一個 reserved 語句中不能同時聲明字段號和字段名。

1.1.7.  從你的. proto 文件生成什麼東西

當你在. proto 文件上運行 pb 編譯器,編譯器以所選語言生成代碼。你使用這種語言來操作這些 message 類型,包括獲取和設置字段值,將 message 序列化爲輸出流,以及從輸入流解析 message。

文檔後續有相關語言的 API,具體可見 API reference

https://developers.google.com/protocol-buffers/docs/reference/overview

API reference

1.2.  Scalar 值類型

一個 message 的字段可以是以下的類型 - 這個表中顯示了. proto 文件中可以使用的類型以及在不同語言中生成對應類型。

ppADWv

在 Protocol Buffer Encoding 中你可以看到這些類型在序列化時是如何編碼的。

https://developers.google.com/protocol-buffers/docs/encoding

Protocol buffer encoding

1.3.  默認值

解析編碼後的 message 時,如果 message 有個 singular 類型的字段沒有指定值,那麼解析出來的相關字段都會設置爲默認值。

repeated 字段的默認值爲空(通常是一個空數組)。

注意:message 的字段中,一旦一個 message 被解析出來,我們就不知道它是被設置爲默認值(例如有個 bool 被設置爲 false)或者它並沒有被設置;你在定義 message 類型的時候需要注意這一點。例如,不要定義一個在某些情況需要設置爲 false 的 bool 類型。同時注意,如果一個 message 字段設置爲默認值,它的值在傳輸時就不會被序列化。

在 generated code guide 可以查看你使用的語言如何生成代碼。

1.4.  Enumerations

當你定義一個 message,可能需要某一個字段的值在一個預定義的列表中。例如,你想要爲 SearchRequest 添加一個 corpus 字段,corpus 有這幾種值:UNIVERSAL、WEB、IMAGES、LOCAL、NEWS、PRODUCES 或 VIDEO。通過在你的 message 定義處添加一個具有多個常量的枚舉類型就可以實現這個功能。

下面代碼中,我們添加了一個 Corpus 的枚舉類型,這個類型具有上面提到的七種值;同時在 message 中添加了一個類型爲 Corpus 的字段。

 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 的常量作爲它的第一個成員。這是因爲:

你可以通過給不同的枚舉變量賦予相同的值來定義別名(別名:alias)。前提是你需要設置 allow_alias 選項爲 true,否則 pb 編譯器會報 error 異常。

 message MyMessage1 {
   enum EnumAllowingAlias {
     option allow_alias = true;
     UNKNOWN = 0;
     STARTED = 1;
     RUNNING = 1;
  }
 }
 message MyMessage2 {
   enum EnumNotAllowingAlias {
     UNKNOWN = 0;
     STARTED = 1;
     // RUNNING = 1; // Uncommenting this line will cause a compile error inside Google and a warning message outside.
  }
 }

枚舉變量必須是 32 位的 integer。因爲枚舉使用 varint encoding 進行編碼,負數在編碼時的效率很低因此是不推薦使用的。如上面代碼所示,你可以在 message 內部定義枚舉,也可以在 message 外部定義。你也可以在把一個 message 的枚舉作爲另一個 message 中的字段,使用這個語法:MessageType.EnumType

https://developers.google.com/protocol-buffers/docs/encoding

varint encoding

當使用 pb 編譯器編譯. proto 文件中的枚舉類型時,生成的代碼中將具有對應的 Java、Kotlin 或 C++ 枚舉,或用於 Python 的特殊 EnumDescriptor 類,用於在運行時生成的類中創建一組具有整數值的符號常量。

注意: 生成的代碼可能會受到特定語言的枚舉數限制(一種語言低至數千)。請查看你使用的語言的限制。

反序列化時,message 中會保留未識別出來的枚舉值,具體的表現形式取決於語言的實現。在 C++ 和 golang 中,枚舉類型的值可以在其指定範圍之外,因此未識別出來的值會簡單存儲爲一個 integer。在 Java 等的枚舉類型中,枚舉中專門有一種情況用來標識未識別的值,它可以通過特殊的訪問機制來獲取具體內容。在其他情況下,如果 message 被序列化了,這個無法識別的值也會隨 message 序列化。

1.4.1.  保留值

如果你通過刪除或註釋掉枚舉的一個條目來更新這個枚舉類型,未來的開發者可能會重新使用這個枚舉值來實現他們對這個枚舉類型的修改。如果他們在之後使用了舊版本的. proto 文件會導致嚴重問題,例如數據污染,權限漏洞等。保證這個不再發生的方法就是枚舉條目的值和名字都聲明爲 reserved。如果未來開發者使用這些條目名或者值,pb 編譯器就會報錯。你可以使用 max 關鍵字來聲明你保留的條目值的範圍一直到最大值。

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

注意:不能在同一行 reserved 語句中同時添加枚舉成員的名和值。

1.5.  使用其他 message 類型

你可以使用其他的 message 作爲當前 message 字段的類型。例如,你想在 SearchResponse 中包括 Result,你可以在當前. proto 文件中定義一個 Result,然後在 SearchResponse 中聲明一個 Result 字段。

 message SearchResponse {
   repeated Result results = 1;
 }
 message Result {
   string url = 1;
   string title = 2;
   repeated string snippets = 3;
 }

1.5.1.  導入已定義的數據類型

本節的特性不適合 Java。

在上面的例子中,Result 和 SearchResponse 定義在同一個. proto 文件中,如果你想使用的 message 已經定義在另一個. proto 文件中呢?

你可以通過在當前. proto 文件中 import 其他的. proto 文件以使用已有的類型,例如,在文件的開頭可以這麼寫:

 import "myproject/other_protos.proto";

這樣,你只能使用 other_protos.proto 中的類型定義。然而,你可能需要把一個. proto 文件移動到一個新的位置。你可以把. proto 文件移動到新的位置並在所有 import 它的. proto 文件中更新其路徑;但是還有更簡單的方法,就是在原來的路徑上放一個假的. proto 文件,使用 import public 來將 import 重定向到新的. proto 文件中。任何導入具有 import public 聲明的. proto 文件的. proto 文件,可以進行依賴的傳遞。(這麼說有點不明白,看下面的代碼。new.proto 就是原來的. proto 文件的新名字,而在. proto 文件的原來位置即 old.proto 中使用了 import public 聲明。這樣,當 client.proto 中 import 了 old.proto 文件,client.proto 也可以使用 new.proto 的定義。從另一個角度說,old.proto 繼承了 new.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";
 // You use definitions from old.proto and new.proto, but not other.proto

pb 編譯器通過 - I/--proto_path 標誌獲取的路徑下查找導入的. proto 文件。如果沒有這個標誌的參數,它會在運行編譯器的當前路徑下查找。通常情況下,你應該將 --proto_path 標誌設置爲項目的根目錄,併爲所有的需要導入的. proto 文件提供完全的路徑名。

1.5.2.  使用 proto2 的 message 類型

在 proto3 的 message 中可以導入使用 proto2 的 message,反之亦然。然而,proto2 的枚舉無法直接在 proto3 語法中使用,除非一個 ptoro2 的 message 使用了它們。

1.6.  嵌套類型

你可以在一個 message 定義內部再定義或使用另一個 message,如下代碼所示。這裏,這個 Result 就是定義在 SearchResponse 內部。

 message SearchResponse {
   message Result {
     string url = 1;
     string title = 2;
     repeated string snippets = 3;
  }
   repeated Result results = 1;
 }

如果你想在與 SearchResponse 同級的 message 中使用 Result,就需要使用 SearchResponse.Result 來進行定義。

 message SomeOtherMessage {
   SearchResponse.Result result = 1;
 }

在 message 中你想嵌套多少就能嵌套多少層 message。

 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;
    }
  }
 }

1.7.  更新一個 message

如果已有的 message 已經無法滿足你的需求了,例如,你需要這個 message 增加一個額外的字段,但是你還需要使用原來的 message 來開發。不要怕,更新你的 message 是很簡單的,而且不會破壞你原有的代碼。只要記住以下幾條規則:

1.8.  未知的字段

未知字段(未知字段:unknown field)是序列化的數據中,pb 解析器無法識別的內容。例如,當使用基於舊. proto 文件的解析器解析具有新增字段的數據時,這些新字段就是未知字段。

起初,proto3 的 message 在解析時會丟棄未知字段,但是 3.5 版本我們重新引入了對未知字段的保留以兼容 proto2。在 3.5 或更高版本中,未知字段在解析過程中會保留並且包含在序列化的輸出中。

1.9.  Any 類型

Any 類型可以讓你在不知道 message 定義的情況下把一個 message 作爲字段的類型。一個 Any 類型中包含任何類型 message 序列化後的數據,同時還有一個 URL 作爲標識符並且用於解析該數據的類型。使用 Any,需要導入 google/protobuf/any.proto。

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

對於給定 message 類型的 URL 是:type.googleapis.com/packagename.messagename

不同的語言實現將支持 runtime library helper 以類型安全的方式打包和解包 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 的 runtime libraries 還在開發中。

如果你已經熟悉了 proto2 的語法,Any 可以持有任意 proto3 的 message 與 proto2 的 message 可以允許 extension 是相似的。

1.10.  Oneof

如果你在一個 message 中有很多個字段,但是同時最多隻有一個字段被設置,你可以通過 oneof 來解決這個問題同時節省內存。

oneof 與普通的字段相似,但是 oneof 中的所有字段共享一塊內存,並且最多隻有一個字段可以被設置。設置 oneof 中的任意字段都會清除其他的字段。你可以使用 case() 或者 WhichOneof() 方法來判斷哪個字段被設置,具體實現依賴於具體的語言。

1.10.1.  使用 Oneof

使用 oneof 關鍵字後面跟 oneof 名來定義一個 oneof 類型。

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

接下來可以向 oneof 定義中添加 oneof 字段。除了 map 和 repeated 字段,其他類型都可以作爲 oneof 的字段。

在生成的代碼中,oneof 字段與普通的字段都有 getter 和 setter 方法。你也會有一個特殊的方法來檢查 oneof 中哪個字段被設置了。具體查看 API 文檔。

https://developers.google.com/protocol-buffers/docs/reference/overview

API 文檔

1.10.2.  oneof 特性

SampleMessage message;
message.set_name("name");
CHECK(message.has_name());
message.mutable_sub_message();   // Will clear name field.
CHECK(!message.has_name());
SampleMessage message;
SubMessage* sub_message = message.mutable_sub_message();
message.set_name("name");      // Will delete sub_message
sub_message->set_...            // Crashes here

1.10.3.  向後兼容性問題

當刪除或增加 oneof 的字段時需要小心。如果檢查一個 oneof 字段返回 None 或者 NOT_SET,這說明這個字段沒有被設置或者在 oneof 的之前某一個版本中是被設置的。這是很難區分的,因爲無法知道傳輸過來的一個未知字段是不是 oneof 的成員。

1.10.3.1.  標籤重用問題

1.11.  字典(字典:map)

如果你想使用字典,pb 提供了一個語法:

map<key_type, value_type> map_field = N;

其中,key_type 可以是數字或者 string 類型(可以是任何的 scalar 類型,除了浮點類型和 bytes)。注意,枚舉不是一個有效的 key_type。value_type 可以是除了 map 以外的任何的類型。

例如,你想創建一個 map,其中每個 Project 都和一個 string 相關聯:

map<string, Project> projects = 3;

map 的 API 見 API 文檔。

https://developers.google.com/protocol-buffers/docs/reference/overview

API 文檔

1.11.1  向後兼容

map 在傳輸時等效於下面的代碼,因此不支持 map 的 pb 實現仍然可以解析 map 數據。

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

任意支持 map 的 pb 實現在構造和接收的數據時都必須與上述代碼中定義生成的數據相同。

1.12.  package

在一個. proto 文件中,你可以添加一個可選的 package 標識來防止 message 重名。

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

在你自己的 message 中定義字段時可以使用上述的包定義:

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

package 標識符對後續生成的代碼的影響取決於你使用的語言:

1.12.1.  package 和名稱解析

在 pb 語言中類型名稱的解析工作和 C++ 一樣:首先搜索最內層的範圍,然後是下一個最內層,依此類推,每個包都比它的父包更靠內。一個前置的'.'(例如,.foo.bar.baz)意味着從最外面的包開始。

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

1.13.  定義 service

如果你想在 RPC 系統中使用你定義的 message,你可以在. proto 文件中定義一個 RPC 服務接口,pb 編譯器會根據你使用的語言生成一個服務接口代碼與樁代碼(樁代碼:stub)。例如,如果你想定義一個 RPC 服務,裏面有一個方法,輸入 SearchRequest,輸出 SearchResponse,你可以在. proto 文件中這麼寫:

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

最直接了當使用 pb 的 RPC 系統是 gRPC:Google 實現的語言和平臺中立性的開源 RPC 系統。gRPC 特別適用於 pb,並且通過一個特殊的 pb 編譯器插件讓你直接從. proto 文件中生成 RPC 代碼。

如果你不想使用 gRPC,也可以在你自己的 RPC 實現上使用 pb。具體看 Proto2 Language Guide。

https://developers.google.com/protocol-buffers/docs/proto#services

Proto2 language guide

目前還有很多第三方工程在開發適用於 pb 的 RPC 實現。具體見 third-party add-on wiki page。

https://github.com/protocolbuffers/protobuf/blob/master/docs/third_party.md

third-party add-on wiki page

1.14.  JSON 映射

proto3 支持 JSON 的規範編碼,使系統之間的數據傳輸更加容易。編碼方式在下表中。

如果 JSON 數據中一個值丟失或者是 null,轉換成 pb 時會使用該類型默認值。如果 pb 中一個字段是默認值,轉換成 JSON 時就會被省略掉。有的實現可能會提供選項,從而可以在 JSON 中保留具有默認值的字段。

g2PJF5

1.14.1.  JSON 選項

一個 proto3 的 JSON 實現可能會提供如下選項:

1.15.  選項

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

有些選項是文件級別的,意味着它們應該寫在最頂層的範圍內,而不是在 message、枚舉或者 service 定義中。有些選項是 message 級的,意味着他們應該寫在 message 定義內部。有些選項是字段級的,意味着它們應該被寫在字段定義的內部。選項也可以寫在枚舉類型、枚舉值、oneof、service 類型和 service 方法上,然而,現在沒有選項提供使用。

具體的選項在生成各種語言的樁代碼中用的比較多,這個用到再查就可以了。

1.16.  生成你的類代碼

通過. proto 文件中的 message 定義,你可以生成 Java、Kotlin、Python、C++、Go、Ruby 等的類代碼,這需要在. proto 文件上執行 pb 編譯器 protoc 得到。如果你沒有安裝這個編譯器,下載安裝包然後按照 README 的指引進行。對於 Go 語言,你還需要安裝一個特殊的代碼生成插件:你可以在 github 的 golang/protobuf 上找到它和它的安裝指南。

https://developers.google.com/protocol-buffers/docs/downloads

protobuf 安裝包

https://github.com/golang/protobuf/

golang/protobuf

這個 pb 編譯器的運行指令如下:

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

https://developers.google.com/protocol-buffers/docs/reference/cpp-generated

C++ generated code reference

https://developers.google.com/protocol-buffers/docs/reference/java-generated

Java generated code reference

https://developers.google.com/protocol-buffers/docs/reference/kotlin-generated

Kotlin generated code reference

https://developers.google.com/protocol-buffers/docs/reference/python-generated

Python generated code reference

https://developers.google.com/protocol-buffers/docs/reference/go-generated

Go generated code reference

https://developers.google.com/protocol-buffers/docs/reference/objective-c-generated

Objective-C generated code reference

https://developers.google.com/protocol-buffers/docs/reference/csharp-generated

C# generated code reference

https://developers.google.com/protocol-buffers/docs/reference/php-generated

PHP generated code reference

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/vufLI2J-pTt3QBnRezqc_A