Protobuf


protobuf 的使用
proto3

本篇文章為 proto3 為非嚴格翻譯自 protobuf 官方 proto3 的文檔。這里會介紹 proto3 的語法,以及如何生成 proto3 的方法。 如果需要看 proto2 請參考 https://developers.google.com/protocol-buffers/docs/proto .

定義消息類型(Defining A Message Type)

首先,我們來看一個非常簡單的例子。 這是一個請求消息,這個請求消息里面包含了一個 query 字符串,用於檢索你中意的消息。以及你希望找到多少頁,每頁多少條這樣的消息。

syntax = "proto3";

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}
  • 該文件的文件名一般以 。proto 結尾。 該文件的第一行用於申明當前文件遵從的協議版本號,這里是 protoc3. 如果你在此處不指明, protocol buffer 的編碼器可能假設你當前是以 proto2 的協議來編寫的。 而且首行不能為空,也不能是注釋行。
  • 這個 SearchRequest 定義了三個鍵值對,每一條定義都指定了一個數據的名字和該數據的對應的數據類型。

在上面的例子中,所有的字段都為 scalar types( 標量類型 ),但是如果如果你想,也可以定義一些 composite types( 復合類型 ), 比如說 enumerations ( 枚舉 ) 或者其他。

Assigning Field Numbers(字段號),每一個字段包含一個唯一的字段號,該字段號在二進制數據結構中標識了你所定義的字段。 而且一旦字段號定義下來了,最好就不要更改了。 另外數值為 1-15 的字段采用一個 byte 存儲,數值為 16-2047 的字段會采用兩個 byte 存儲,因此您應該根據此規則合理的安排您的字段。 比如說最常用的字段數值應該在 1-15 之間。 而且要注意,最好預留一些字段,以免將來使用。

字段號最小為 1,最大為 2^29 -1 (即 536,870,911)。 但是你不能使用 [19000, 19999] ( [ FieldDescriptor::kFirstReservedNumber, FieldDescriptor::kLastReservedNumber ] ) 之間的數值,因為這是 proto 的保留號。 當然,你也不能使用你自己定義的保留字段號(見下面說明)。

Specifying Field Rules(指定字段的規則),字段規則可以為下面的兩者之一:

  • singular: proto3 的默認規則,在一個消息包中,一個字段的個數不能大於 1 。
  • repeated: 在一個消息包中,一個字段的個數可以大於 1 ,而且出現在消息中的順序將會被保留下來。

在 proto3 中,repeated 的字段將會采用 packed encoding (打包編碼), 該編碼規范可以參考: https://developers.google.com/protocol-buffers/docs/encoding#packed

添加更多消息類型

多條消息可以定義在一個 .proto 類型文件中,如下面的例子(定義了一個請求消息,以及一個與之相對應的響應消息):

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

message SearchResponse {
...
}
添加注釋

添加注釋的方法和 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.
}
保留域

如果你通過過刪除一個字段或者注釋一個字段去更新你的消息,那么在將來如果有用戶重用了該字段去與支持老版本的消息通信則有可能導致數據泄露或者比較隱秘的 Bug。 一個解決該問題的方法是將該字段設置成保留字段,並加以注釋。 在以后如果其他人想用該字段的時候編譯器將會提醒用戶。

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

注意:你不能在一行里面既使用字段名,又使用字段標號。

你可以使用 protocol buffer 編譯器編譯你的 .proto 文件, 編譯成你指定的語言文件中包含了該屬性的,get 和 set 方法,以及序列化( serializing ),和反序列化( parsing )方法。

你可以在官網的各個語言的說明文檔中找到更多幫助信息。

scalar types -- 標量類型

消息中的標量類型包括如下類型: double, float, int32, int64, uint32, uint64, sint32, sint64, fixed32, fiexd64 sfixed32, sfixed64, bool, string, bytes 標量類型中在各個語言中對應的數值類型請參考如下文章https://developers.google.com/protocol-buffers/docs/proto3#scalar

在解釋 protocol 編碼的那篇文章中你能找到更多關於這些類型的信息:https://developers.google.com/protocol-buffers/docs/encoding

  1. 在Java中,無符號的32位和64位整數使用帶符號的對等體表示,最高位僅存儲在符號位中。
  2. 在所有情況下,給子段賦值的時候最好要檢查其正確性,已確保其在生產環境下是可用的。
  3. 64-bit or unsigned 32-bit integers are always represented as long when decoded, but can be an int if an int is given when setting the field. In all cases, the value must fit in the type represented when set. See [2].
  4. Python strings are represented as unicode on decode but can be str if an ASCII string is given (this is subject to change).
  5. Integer is used on 64-bit machines and string is used on 32-bit machines.

 

默認值

在消息的 parse 階段,如果在消息中某個字段未被指定,那么該字段將會默認的設置成下面所描述的值:

  • string 類型,默認值為空串。
  • bytes 類型,默認值為空 bytes.
  • bool 類型,默認值為 false.
  • numeric 類型,默認為 0.
  • enums 類型,為枚舉定義中的第一個字段的值,而且必須為 0
  • message 字段,該值將會是是視編譯語言而定,具體請參考: https://developers.google.com/protocol-buffers/docs/reference/overview

當標記為 repeat 的字段為空,則會在相應的語言中標記為空的 List.

對於標量類型,在消息被解析的時候,如果發現消息解析的值和默認值一樣,解析端也無法知道是否該值是被設置成該值還是因為沒有設置而解析器給的默認值。 因此你在使用這些字段的時候,自己應該要預先想好處理此類現象的應對方法。

如果想知道更多和默認值相關的信息請參考: generated code guide

Enumerations -- 枚舉類型

在你定義消息的時候,可能你定義的字段僅僅在一個列表中。如:你定義了一個搜索請求消息,其中 corpus 字段的值在 UNIVERSAL, WEB, IMAGES, LOCAL, NEWS, PRODUCTS or VIDEO 列表中, 你可以很簡單的將該字段添加到你的 proto 消息中。如下所示:

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 值在 proto 設計的時候就被設計成了默認值,而且必須存在的這種形式。

你可以為一個為一個枚舉值設置不同的別名。 但是在為一個值設置多個別名之前,你先需要將 allow_alias option (選項) 設置為 true. 否則編譯器會報錯。

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-Bit 整形范圍內。由於 enum 在序列化時使用 varint 編碼,因此負數值是效率不高的,也是不推薦的做法。 你可以將一個枚舉值定義到消息體中,也可以定義到消息體外。如果定義子啊消息體外,則該枚舉變量可以應用到多個消息的定義中。 當然,你也可以用 _MessageType_._MessageType_ 的形式從另外的消息體中引入枚舉定義。

當你在你的消息中聲明了一個枚舉變量的時候,該枚舉變量會在你的 Java / C++ 語言中表示成相應的媒體結構, 而在 Python 中將會生成一個包含一系列整形數值的特殊的類(EnumDescriptor)。

**Caution:** 生成的代碼可能會受到特定於枚舉器數量的語言的限制(一種語言的下限為幾千)。請查看您計划使用的語言的限制。

在解序列化(反序列化,deserialization )的時候, 無法識別的枚舉消息將不會翻譯到解析的消息體。 在支持開放枚舉類型且其值超出指定符號范圍的語言(例如C ++和Go)中,未知枚舉值只是作為其基礎整數表示形式存儲。 在具有封閉枚舉類型的語言(例如Java)中,枚舉中的大小寫用於表示無法識別的值,並且可以使用特殊的訪問器訪問基礎整數。 無論哪種情況,如果消息被序列化,則無法識別的值仍將與消息一起序列化。

更多關於應用中枚舉的信息請參考如下網站: generated code guide.

Reserved Values

如上文介紹的情況,如果你以注釋或者刪除的方式更新一個枚舉類型,那么有可能會因為其在后續的版本中重用該字段(字段 ID,或者字段名 )而導致程序的異常或者崩潰。 而解決這一問題的辦法就是將這一字段 ID 或者字段名指定為保留( reserved )值。 表示字段值的時候可以用 to 關鍵字表示一個字段范圍,也可用 max 表示最大值( int32 最大值 == 2147483647 )。

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

注意,你不能將名字和值放在統一申明語句中。

其他消息類型

你可以使用其他消息類型作為字段類型。 如下所示:

message SearchResponse {
  repeated Result results = 1;
}

message Result {
  string url = 1;
  string title = 2;
  repeated string snippets = 3;
}
Importing Definitions

注意,這個特性在 Java 中不可用。

在上面的例子中,Result 消息和 SearchResponse 定義在同一文件中, 而在不同文件中如果想重用,則應該使用 import 關鍵字將之倒入,而 import 導入語句應該位於文件的頂部:(如下所示)

import "myproject/other_protos.proto";

一般情況下,你可以使用直接 import 的消息,而不能使用 import 文件中 import 的消息。 但是,有時候你可能想引用一個文件,而該文件的位置將會有所變更,而你又不想一個一個去修改 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

Protocol buffer 的編譯器在編譯的時候會從 -I/--proto_path 指定的一組路徑中搜尋需要 import 的文件。 如果用戶沒有使用 -I/--proto_path 指定 .proto 的源目錄,則會默認在調用編譯器的目錄下尋找。 但一般情況下,你應該使用 --proto_path 指定你 proto 工程的根目錄,以及指定完整有效的 import 文件。

Using proto2 Message Types

在 proto3 中 import proto2 中的消息或者反過來都是有可能的, 然而,proto2 中的 enums 類型並不能直接用在 proto3 中。 (而 proto2 中的可以使用 proto3 的)

Nested Types

你可以在一個消息類型中定義另外的一個消息類型(即實現消息類型的嵌套)。 如下所示:

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

如果你想重用父級以外的消息作為自己的一個消息類型,則可以使用如下形式:

message SomeOtherMessage {
  SearchResponse.Result result = 1;
}

You can nest messages as deeply as you like:

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;
    }
  }
}
Updating A Message Type

如果現有的消息不滿足你的需求了,例如你可能需要增加一個額外的字段,但是你不想破壞原來的格式,如果你幾下一下幾點,你能很容易的做到更新你的協議,而不破壞你原有的協議。

  1. 不變更現有字段以及其對應的 ID 值。
  2. 如果你增加一個新的字段,你任然可以用舊代碼來解析新代碼生成的編碼序列。 而舊的代碼在解析二進制序列的時候會忽略不認識的字段(新增加的字段),詳情請見 未知序列 章節。 當然你也應該記住原來代碼中的值的默認值,以便新舊代碼能都交互使用
  3. 字段可以被移除,但是要確保歷史使用的字段 ID 不再被用於新的字段定義。 你可能也想要重命名一個字段,但最好的方法是在字段前加上 OBSOLETE_ 前綴, 或者將該字段 ID 設置成 reserved, 這樣做是為了防止以后的用戶意外的使用了你作廢的字段。
  4. int32,uint32,int64,uint64 和 bool 都是兼容的–這意味着您可以將字段從這些類型中的一種更改為另一種,而不會破壞向前或向后的兼容性。 如果從二進制流中解析出了一個數字,其類型與您協議中的類型不匹配,你將獲得與 C++ 中類似的類型轉換的效果。 (比如: 一個 64bit 的數字被讀成了 int32 類型,那么他將截斷成 32bit 的一個數據。
  5. sint32 和 sint64 彼此兼容,但是他們與其他整數類型不兼容。(待驗證)
  6. 只要 bytes 是服從 UTF-8 編碼的,string 類型和 bytes 類型也是兼容的;
  7. 如果 bytes 包含解碼版本消息的二進制流,那么 bytes 和 messages 也是兼容的;
  8. fixed32 與 sfixed32 兼容;
  9. fixed64 與 sfixed64 兼容;
  10. 對於 string, bytes, message 類型字段, optional 和 repeated 特性是兼容的。 比如給定一個 repeated 字段的二進制序列,客戶端在解析的時候如果以 optional 的方式解析,則會選擇最后一個輸入的值作為最終值。 如果是 message 類型字段則會 merge 所有的輸入元素作為字段最終值。
    注意,這個屬性對於數值類型來說是不安全的(包括 bool enum), 數值類型的 repeated 字段將會序列化成 packed format, 自己將使解析器無法識。
  11. 枚舉類型與 int32, uint32, int64, uint64 兼容。 但是請注意,反序列化消息時,客戶端代碼可能會以不同的方式對待它們 例如,無法識別的 proto3 枚舉類型將保留在消息中,但是在反序列化消息時如何表示它取決於語言。 Int 字段始終只是保留其值。
  12. 將單個值更改為新的 oneof 的成員是安全且二進制兼容的。 如果您確定沒有一個代碼一次設置多個字段,那么將多個字段移動到新的 oneof 字段中可能是安全的。 將任何字段移動到現有 oneof 字段中都是不安全的。
Unknown Fields

未知字段,即解析器在解析(反序列化)的時候遇到的格式正確但是無法識別的字段。比如新的 proto 協議,編碼的數據傳輸給支持舊 proto 協議的解析器解析,此時存在新協議中但不存在舊協議中的字段,即為未知字段(Unknown Field).

原來, proto3 遇到未知字段的時候總是丟棄之,但是在 3.5 版本之后,為了保持與 proto2 的行為一致。 在 3.5 以及以后,未知字段在解析的時候將會被保留,並包含在序列化輸出中。

Any

Any message 類型允許你嵌入事先未定義的 message type 到消息體中。 Any 包含任意序列化的消息(以字節為單位),以及 URL, URL 作為該消息的類型並解析為該消息的類型的全局唯一標識符。 要使用 Any 類型,您需要導入 google/protobuf/any.proto

import "google/protobuf/any.proto";

message ErrorStatus {
  string message = 1;
  repeated google.protobuf.Any details = 2;
}

Any 消息類型的 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 network_error;
    detail.UnpackTo(&network_error);
    ... processing network_error ...
  }
}

注意目前運行時的打包庫還正在開發當中。

如果您已經熟悉 proto2 語法,則 Any 可以保存任意 proto3 消息,類似於可以擴展的 proto2 消息。

Oneof

如果你有一條消息擁有多個字段,但是同一時間只存在同一個字段,則你可以對該消息設置成 oneof 屬性,以節省內存。

Oneof 字段和普通字段一樣,只是所有的 oneof 自動共享一段內存,而且在同一時刻,只有一個字段能設置值。 當字段中一個值被設置的時候,該消息體中其他值都會被清除。 而通過 case() 或者 whichOneof() 方法可以查詢到底是那個字段被設置了值。 至於是哪個方法還要具體參考你使用的語言。

Using Oneof

使用 oneof 關鍵字后跟消息體名字則可以定義一個 oneof 消息,如下所示:

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

然后你可以在 oneof 的消息體中定義除了 map 類型或者 repeasted 類型的字段。

在你生成的代碼中,oneof 修飾的字段同樣擁有 getters 和 setters 方法。 你同樣可以獲得一個特殊的方法用於確定 oneof 消息中的哪個字段被設置了值。 你可以在 這個 鏈接中找到哦啊更多關於 oneof 消息的 API。

Oneof Features
  • 設置 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;
  • Reflection APIs work for oneof fields.
  • 如果你使用的是 C++, 請確保你的代碼不會造成內存崩潰。 比如下面的代碼在 message.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 消息的 messages,那么這兩個消息中的 oneof 消息中的值也會交換(這不廢話嗎?)
    SampleMessage msg1;
    msg1.set_name("name");
    SampleMessage msg2;
    msg2.mutable_sub_message();
    msg1.swap(&msg2);
    CHECK(msg1.has_sub_message());
    CHECK(msg2.has_name());
    
Backwards-compatibility issues

在增加或者刪除 oneof 字段的時候,如果你檢查 oneof 的值得到的返回是 None/NOT_SET, 那么這可能代表 oneof 沒有被設置,或者他被設置了字段值但是在另外一個版本的 oneof 消息中(不太明白另外一個版本的意思)。 其根因是解析器無法從序列化后的二進制代碼中區分 unknown 字段是否屬於某個 oneof 字段。

Tag Reuse Issues
  • Move fields into or out of a oneof: You may lose some of your information (some fields will be cleared) after the message is serialized and parsed. However, you can safely move a single field into a new oneof and may be able to move multiple fields if it is known that only one is ever set.
  • Delete a oneof field and add it back: This may clear your currently set oneof field after the message is serialized and parsed.
  • Split or merge oneof: This has similar issues to moving regular fields.
Maps

如果你想在你的數據中定義 map 數據。 protocol buffer 提供了如下簡單的語法:

map<key_type, value_type> map_field = N;

...where the key_type can be any integral or string type (so, any scalar type except for floating point types and bytes). Note that enum is not a valid key_type. The value_type can be any type except another map. 這里的 key_type 可以是任何的整形或者字符串類型數據(即任何除浮點類型的標量類型和 bytes 類型)。 注意 enum 類型也不能作為 key_type。 而 value_type 則可以是任何除了 map 類型的值。

下面是一個 map 類型的例子:

map<string, Project> projects = 3;

map 類型有如下值得關注的屬性:

  • Map 類型不能被設置 repeated 屬性。
  • Map 類型在序列化的二進制序列中沒有明確的順序,因此解碼端不應該對 map 對象的順序有任何假定操作。
  • 當將 .proto 文件轉換成 text 格式的時候, map 將會對 key 的字面值做排序處理。(例如如果 key 是數值,則會按照數值從小到大排序)
  • 當在對 wire 數據進行解析的時候,如果在解析序列中發現有重復的 map 鍵值,則會選擇后者。 而如果是對 text 數據進行解析的時候遇到這種情況則直接會報錯。
  • 如果你為一個 map 對象提供了 key 但是沒有給之賦值,那么在序列化的時候,C++ / java / python 將會將值序列化成值類型的默認值。 而其他語言將不會對之進行序列化。

目前關於 map 的 API 已經適應於支持的所有編程語言。 你可以從 這個 文檔找到更多關於 map API 的參考信息。

Backwards compatibility

wire 數據中 map 的的表示方式與下面的這種形式等效,因此,即便 protobuf 的實現沒有支持 map, 也可以處理 map 發送過來的數據。

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

repeated MapFieldEntry map_field = N;

同時,任何 protobuf 的實現在處理 map 的時候都應該產生( produce )或者能夠處理(handle)成上敘這種形式。

Packages

你可以在 .proto 中添加包明,以防止字段名沖突。如下所示:

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

然后你還可以用一下的方式來快速定義新的字段。

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

package specifier 究竟會生成何種代碼則會取決於你選擇的語言。

  • 在 C++ 中生成的類將會被包含在 namespace 中。比如你可能這樣使用你的類: foo:bar
  • 在 Java 中則會使用 java 的包機制。除非你在你的 .proto 文件中利用 java_package 屬性指定了包名。
  • 在 Python 中,會直接忽略 package 屬性,因為 python 中是以目錄結構來區分模塊的。
  • 在 Go 語言中,會使用 Go package 的方式,除非你使用 go_package 明確指定你想要的包名;
  • (不熟) In Ruby, the generated classes are wrapped inside nested Ruby namespaces, converted to the required Ruby capitalization style (first letter capitalized; if the first character is not a letter, PB_ is prepended). For example, Open would be in the namespace Foo::Bar.
  • (不熟) In C# the package is used as the namespace after converting to PascalCase, unless you explicitly provide an option csharp_namespace in your .proto file. For example, Open would be in the namespace Foo.Bar.
Packages and Name Resolution

Protobuf 中的類型名稱的解析方式類似與 C++: 首先搜索最里面的作用域,然后搜索前一層的作用域, 依此類推,每個包都被認為是其父包的 “內部“。 以 '.' 來銜接父子(包含)關系。

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

Defining Services

如果你是將 protobuf 用於 RPC,你可以直接在 .proto 文件中定義 RPC 的接口,並會更具你所選擇的語言生成對應的代碼。 下面是一個例子:

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

大部分使用 protobuf 作為 RPC 通訊協議的系統都使用一個叫 gRPC 的協議: 該協議有 Google 開發,是一個跨平台的通訊學醫。 gRPC 可以完美的兼容 protobuf 協議,你使用特殊的編譯插件即可從 .proto 文件編譯出 RPC 相關的代碼。

如果你不想使用 gRPC, 而想自己實現自己的 RPC 程序,你可以參考 proto2 的 User Guide.

這里還有一些第三方給予 protobuf 實現的方案,請參考 third-party add-ons wiki page.

JSON Mapping

Proto3 支持 JSON 中的規范編碼,從而使在系統之間共享數據更加容易。下表按類型對編碼進行了描述。

如果 JSON 編碼的數據中缺少某個值,或者該值為 null,則在解析為協議緩沖區時,它將被解釋為適當的默認值。 如果字段在 protobuf 中具有默認值,則默認情況下會在 JSON 編碼數據中將其省略以節省空間。 有些實現方案可能會提供選項,以在 JSON 編碼的輸出中默認值的字段。

proto3 的官方文檔中有個表格定義了 JSON 的類型和, protobuf 消息類型的對應關系。 詳情請請參考 https://developers.google.com/protocol-buffers/docs/proto3#json

JSON options

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

  • Emit fields with default values 默認情況下,proto3 JSON 輸出中會省略具有默認值的字段。 實現可能(可以)提供一個選項,以使用默認值覆蓋此行為和輸出字段。
  • Ignore unknown fields Proto3 JSON 解析器默認情況下應拒絕未知字段,但可以提供在解析時忽略未知字段的選項。
  • Use proto field name instead of lowerCamelCase name: 默認情況下,proto3 JSON 輸出應將字段名稱轉換為 lowerCamelCase 並將其用作 JSON 名稱。 但也可能(可以)提供一個選項,改為使用原型字段名稱作為 JSON 名稱。 Proto3 JSON 解析器必須接受轉換后的 lowerCamelCase 名稱和原型字段名稱。
  • Emit enum values as integers instead of strings 默認情況下,JSON 輸出中使用枚舉值的名稱。可以提供一個選項來代替使用枚舉值的數字值。
Options

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

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

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

  • java_package 該 Option 指定生成 java 代碼使用的包名。 如果在 proto 文件中沒有顯示指定 java_package, 那么 protobuf 的編譯器將會以默認的的方式生成 java 包名(包名由 package 字段指定)。 但是,proto packages 包通常不能成為良好的 Java package 名,因為 proto 軟件包不應以反向域名開頭。 如果未生成Java代碼,則此選項無效。
    option java_package = "com.example.foo";
    
  • java_multiple_files 使 top level 的 messages, enums,和 services 在程序包級別定義,而不是在以 .proto 文件命名的類內部定義。
    option java_multiple_files = true;
    
  • java_outer_classname 您要生成的最外層 Java 類的類名(以及文件名)。 如果在 .proto 文件中未指定顯式的 java_outer_classname, 則通過將 .proto 文件名轉換為駝峰式大小寫來構造類名(因此 foo_bar.proto 變為 FooBar.java)。 如果未生成 Java 代碼,則此選項無效。
    option java_outer_classname = "Ponycopter
    
  • optimize_for(file option) 可以為如下的值。這個選項作用於生成 C++ 和 JAVA 代碼的時候。
    • SPEED( default ) protobuf 編譯器將生成代碼,用於對消息類型進行序列化,解析和執行其他常見操作。 此代碼已高度優化。
    • CODE_SIZE protobuf 編譯器將生成最少的類,並將依賴於基於反射的共享代碼來實現序列化,解析和其他各種操作。 因此,生成的代碼將比使用 SPEED 的代碼小得多,但是操作會更慢。 類仍將實現與 SPEED 模式下完全相同的公共 API。 此模式在包含大量 .proto 文件且不需要所有文件都快速達到要求的應用程序中最有用。
    • LITE_RUNTIME protobuf 編譯器將生成僅依賴於 “lite” 運行時庫的類(libprotobuf-lite 而非 libprotobuf)。 精簡版運行時比完整庫要小得多(大約小一個數量級),但省略了某些功能,例如描述符和反射。 這對於在受限平台(例如手機)上運行的應用程序特別有用。 編譯器仍將像在 SPEED 模式下一樣快速生成所有方法的實現。 生成的類將僅以每種語言實現 MessageLite 接口,這些接口是完整 Message 接口的方法的子集。
    option optimize_for = CODE_SIZE;
    
  • cc_enable_arenas (file option) 使能 C++ arenas allocation 能力
  • objc_class_prefix (file option) 設置 Objective-C 類的前綴,該前綴是所有 Objective-C 生成的類以及此 .proto 枚舉的前綴。 沒有默認值。您應該使用 Apple 推薦的 3-5 個大寫字符之間的前綴。 請注意,Apple 保留所有 2 個字母前綴。
  • deprecated (field option) 如果設置為 true,則表明該字段已棄用,並且不應由新代碼使用。 在大多數語言中,這沒有實際效果。 在 Java 中,成為 @Deprecated 注解。 將來,其他特定於語言的代碼生成器可能會在字段的訪問器上生成棄用注釋,這反過來將導致在編譯嘗試使用該字段的代碼時發出警告。 如果該字段未被任何人使用,並且您想阻止新用戶使用該字段,請考慮使用保留語句替換該字段聲明。
    int32 old_field = 6 [deprecated = true];
    
Custom Options

協議緩沖區還允許您定義和使用自己的選項。 這是大多數人不需要的高級功能。 如果您確實需要創建自己的選項, 請參閱 Proto2 Language Guide 以了解詳細信息。 請注意,自定義的選項使用時需要用 extension 關鍵字,而且其僅適用於 proto3 中的自定義選項。

Generating Your Classes

要生成 Java,Python,C ++,Go,Ruby,Objective-C 或 C# 代碼,您需要使用 .proto 文件中定義的消息類型, 您需要在 .proto 上運行 protobuf 編譯器。 如果尚未安裝編譯器,請下載 軟件包 並按照 README 中的說明進行操作。 對於 Go,您還需要為編譯器安裝一個特殊的代碼生成器插件:您可以在 GitHub 上的 golang/protobuf 存儲庫中找到此代碼以及安裝說明。

protobuf 編譯器的調用方式如下:

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_PATH protobuf 編譯器將會在該指定目錄中尋找 .proto 的文件。 如果省略則會在當前目錄中查找。 可以通過多次設置 --proto_path 來指定多個導入目錄。 編譯器會按照你設置的目錄順序搜索編譯文件。 -I--proto_path 的縮寫形式。
  • 你需要提供至少一個輸出參數:
    • --cpp_out 生成 C++ 的目錄,參考 C++ Generated Code Refrence generates C++ code in DST_DIR. See the C++ generated code reference for more.
    • --java_out 生成 C++ 的目錄,參考 java Generated Code Refrence -- generates Java code in DST_DIR. See the Java generated code reference for more.
    • --python_out 生成 C++ 的目錄,參考 python Generated Code Refrence -- generates Python code in DST_DIR. See the Python generated code reference for more.
    • --go_out 生成 C++ 的目錄,參考 Go Generated Code Refrence -- generates Go code in DST_DIR. See the Go generated code reference for more.
    • --ruby_out 生成 C++ 的目錄,參考 Ruby Generated Code Refrence -- generates Ruby code in DST_DIR. Ruby generated code reference is coming soon!
    • --objc_out 生成 C++ 的目錄,參考 objective-c Generated Code Refrence -- generates Objective-C code in DST_DIR. See the Objective-C generated code reference for more.
    • --csharp_out 生成 C++ 的目錄,參考 C# Generated Code Refrence -- generates C# code in DST_DIR. See the C# generated code reference for more.
    • php_out 生成 C++ 的目錄,參考 PHP Generated Code Refrence

    為了更加方便,如果 DST_DIR 以 .zip 或 .jar 結尾,則編譯器會將輸出寫入具有給定名稱的單個 ZIP 格式的存檔文件。 根據 Java JAR 規范的要求,還將為 .jar 輸出提供清單文件。 但請注意,如果輸出存檔已經存在,它將被覆蓋; 編譯器不夠智能,無法將文件添加到現有存檔中。
  • 您必須提供一個或多個.proto 文件作為輸入。 可以一次指定多個 .proto 文件。 盡管這些文件是相對於當前目錄命名的, 但是每個文件都必須位於 IMPORT_PATH 指定的目錄中,同時也為了方便編譯器可以確定其規范名稱。
Protobuf 的編碼
參考文檔
參考文檔
  • [link]機器學習即服務之BigML特性介紹和入門教程
  • [link]【深度開源 open經驗】機器學習平台、框架、庫和軟件集合
  • [link]軟件形式化方法概述
  • [link]形式化方法--百度百科
  • [link]敏捷開發中的持續集成
  • [link]github開源項目-算法實現之路

機器學習 開發pingtai 機器人開發平台

原創文章,版權所有,轉載請獲得作者本人允許並注明出處
我是留白;我是留白;我是留白;(重要的事情說三遍)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM