Proto3 語言指南

由于工程項目中擬采用一種簡便高效的數(shù)據(jù)交換格式,百度了一下發(fā)現(xiàn)除了采用 xml、JSON 還有 ProtoBuf(Google 出品),趕緊去瞄了一下?;艘粋€周末的時間把它走馬觀花的學(xué)習(xí)了一下,順便將官方的指南翻譯了出來。

首先申明,哥們兒英語高中水平,借助了必應(yīng)詞典勉強將其意譯了出來,如果你發(fā)現(xiàn)翻譯中有紕漏,請一定不要告訴我~

怕有誤人子弟之嫌,先貼上官方文檔的地址,本譯文僅供參考:
https://developers.google.com/protocol-buffers/docs/proto3

Proto3 語言指南

  • 定義消息類型
  • 標準類型
  • 默認值
  • 枚舉
  • 使用其它消息類型
  • 嵌套
  • 更新消息類型
  • Any
  • Oneof
  • Map 類型
  • 定義服務(wù)
  • JSON 映射
  • 選項
  • 創(chuàng)建您的類

本指南描述如何使用 ProtoBuf 語言規(guī)范來組織你的.proto 文件,以及如何編譯.proto 文件來生成相應(yīng)的操作類。它涵蓋了proto3 語法,如果你想查看老版本 proto2 的相關(guān)信息,請參考《Proto2 語言指南》

這是一個參考指南---通過一個例子一步一步地介紹本文檔描述的 proto3 語言特性,請根據(jù)你選擇的編程語言參考基礎(chǔ)教程。

定義消息類型

讓我們先來看一個簡單的例子。假設(shè)你想定義一個搜索請求的消息格式,它包含一個查詢字符串、一個你感興趣的特定頁號、以及每頁結(jié)果數(shù)。下面就是這個.proto 文件所定義的消息類型。

syntax = "proto3";


message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}
  • 文件的第一行指定你正在使用 proto3 語法:如果你不這么做 protocol buffer 編譯器會假設(shè)您使用的是 proto2 。 這一行不允許存在空白字符或注釋。
  • 這個 SearchRequest 消息定義了三個字段(名稱/值對),每一條 SearchRequest 消息類型的數(shù)據(jù)都包含這三個字段定義的數(shù)據(jù)。每個字段包含一個名稱和類型。
指定字段類型

在上面的例子中,所有的字段都是 標準 類型:二個整形(page_number 和 resulet_per_page)和一個字符串類型(query)。然而,你也可以用復(fù)雜類型來定義字段,包括 枚舉 和其它消息類型。

指定標簽

通過上面的例子你可以看到,這里每個字段都定義了一個唯一的數(shù)值標簽。 這些唯一的數(shù)值標簽用來標識 二進制消息 中你所定義的字段,一旦定義了編譯后就無法修改。需要特別提醒的是標簽 1–15 標識的字段編碼僅占用 1 個字節(jié)(包括字段類型和標識標簽),更多詳細的信息請參考 ProtoBuf 編碼 。 數(shù)值標簽 16–2047 標識的字段編碼占用 2 個字節(jié)。因此,你應(yīng)該將標簽 1–15 留給那些在你的消息類型中使用頻率高的字段。記得預(yù)留一些空間(標簽 1–15)給將來可能添加的高頻率字段。

最小的數(shù)值標簽是 1, 最大值是 2 29 - 1, 即 536,870,911。 你不能使用的標簽范圍還有:19000–19999
( FieldDescriptor::kFirstReservedNumber – FieldDescriptor::kLastReservedNumber ),這些是 ProtoBuf 系統(tǒng)預(yù)留的,如果你在你的.proto 文件中使用了其中的數(shù)值標簽,protoc 編譯器會報錯。同樣地,你不能使用保留字段中 reserved 關(guān)鍵字定義的標簽。

定義字段的規(guī)則

消息的字段可以是一下情況之一:

  • 單數(shù)(默認):該字段可以出現(xiàn) 0 或 1 次(不能大于 1 次)。
  • 可重復(fù)(repeated):該字段可以出現(xiàn)任意次(包含 0)。 可重復(fù)字段數(shù)值的順序是系統(tǒng)預(yù)定義的。
    由于一些歷史原因,默認情況下,數(shù)值類型的可重復(fù)(repeated)字段的編碼性能沒有想象中的好,你應(yīng)該在其后用特殊選項 [packed=true] 來申
    明以獲得更高效的編碼。 例如:
    repeated int32 samples = 4 [packed=true];

你能夠在 ProtoBuf 編碼 中查閱更多的關(guān)于 packed 關(guān)鍵字的信息

添加更多的消息類型

同一個.proto 文件中可以定義多個消息類型。這在定義多個相關(guān)的消息時非常有用。例如,如果你想針對用于搜索查詢的 SearchRequest 消息定義
一個保存查找結(jié)果的 SearchResponse 消息,你可以把它們放在同一個.proto 文件中:

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

message SearchResponse {
  ...
}

添加注釋

在.proto 文件中,使用 C/C++格式的注釋語法 // syntax

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

保留字段

如果你通過直接刪除或注釋一個字段的方式 更新 了一個消息結(jié)構(gòu),將來別人在更新這個消息的時候可能會重復(fù)使用標簽。如果他們以后加載舊版
本的相同的.proto 文件,可能會導(dǎo)致嚴重的問題。包括數(shù)據(jù)沖突、 隱秘的 bug 等等。為了保證這種情況不會發(fā)生,當你想刪除一個字段的時候,
可以使用 reserved 關(guān)鍵字來申明該字段的標簽(和/或名字,這在 JSON 序列化的時候也會產(chǎn)生問題)。 將來如果有人使用了你使用 reserved
關(guān)鍵字定義的標簽或名字,編譯器就好報錯。

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

注意:你不能同時在一條 reserved 語句中申明標簽和名字。

.proto 文件編譯生成了什么?

當你使用 protoc 編譯器 編譯一個.proto 文件的時候,編譯器會根據(jù)你選擇的語言和你在這個.proto 文件定義的消息類型生成代碼,,這些代碼的
功能包括:字段值的 getter,setter,消息序列化并寫入到輸出流,從輸入流接反序列化讀取消息等。
對于 C++語言,編譯器會根據(jù)定義的.proto 文件編譯生成一個.h 頭文件和一個.cc 源碼實現(xiàn)文件。
4

  • 對于 Java 語言,編譯器會為每一個消息類型創(chuàng)建一個帶類的.java 文件,同時這個 java 文件中包含用來創(chuàng)建該消息類型的實例的特殊 Builder 構(gòu)造
    類。
  • Python 語言有點不一樣---編譯器在你定義的.proto 文件中創(chuàng)建一個包含靜態(tài)描述符的模塊,每個消息類型對應(yīng)一個靜態(tài)描述符,在 Python 程序解
    釋運行的時候,會根據(jù)靜態(tài)描述符用一個元類去創(chuàng)建相應(yīng)的數(shù)據(jù)訪問類。
  • 對于 Go 語言,針對每一個定義的消息類型編譯器會創(chuàng)建一個帶類型的.pb.go 文件。
  • 對于 Ruby 語言,編譯器會創(chuàng)建一個帶 Ruby 模塊的.rb 文件,其中包含了所有你定義的消息類型。
  • 對于 JavaNano,編譯器會創(chuàng)建 Java 語言類似的輸出文件,但是沒有 Builder 構(gòu)造類。
  • 對于 Ojective-C,編譯器會創(chuàng)建一個 pbobjc.h 和一個 pbobjc.m 文件,為每一個消息類型都創(chuàng)建一個類來操作。
  • 對于 C#語言,編譯器會為每一個.proto 文件創(chuàng)建一個.cs 文件,為每一個消息類型都創(chuàng)建一個類來操作。
    針對所選擇的不同的編程語言,你能夠在后續(xù)的教程中找到更多的關(guān)于操作它們的編程接口(proto3 版本的即將推出)。 更詳細
    的針對特定編程語言的 API 操作細節(jié),請參考 API 參考 。
標準類型

.proto文件中消息結(jié)構(gòu)里用于定義字段的標準數(shù)據(jù)類型如下表所示,后面幾列是.proto文件中定義的標準類型編譯轉(zhuǎn)換后在編程語言中的類型對照。

如果你想了解這些數(shù)據(jù)類型在序列化的時候如何編碼,請參考 ProtoBuf 編碼
[1] 在 Java 中,無符號的 32 位整形和 64 位整形都是用的相應(yīng)的有符號整數(shù)表示,最高位儲存的是符號標志。
[2] 在任何情況下,給字段賦值都會執(zhí)行類型檢查,以確保所賦的值是有效的。
5
[3] 默認情況下 64 位整數(shù)或 32 位無符號整數(shù)通常在編碼的時候都是用 long 類型表示,但是你可以在設(shè)定字段的時候指定位 int 類型。 在任何情況
下,這個值必須匹配所設(shè)定的數(shù)據(jù)類型。參考[2]
[4] Python 中字符串通常編碼為 Unicode,但是如果給定的字符串是 ASCII 類型,可以設(shè)置位 str 類型(可能會有變化)

默認值

當一個消息被解析的時候,如果在編碼后的消息結(jié)構(gòu)中某字段沒有初始值,相應(yīng)的字段在被解析的對象中會被設(shè)置默認值。這些默認值都是類型相關(guān)的。

  • 字符串默認值為空字符串。
  • 字節(jié)類型默認值是空字節(jié)。
  • 布爾類型默認值為 false。
  • 數(shù)值類型默認值位 0。
  • 枚舉 類型默認值是第一個枚舉元素,它必須為 0。
  • 消息類型字段默認值為 null。

可重復(fù)類型字段的默認值為(相應(yīng)編程語言中的)空列表。
需要提醒的是:對于標準數(shù)據(jù)類型的字段,當消息被解析的時候你是沒有辦法顯示地設(shè)定默認值的(例如布爾類型是否默認設(shè)置為 false),記住當你定義自己的消息類型的時候不要設(shè)置它的默認值。例如,不要在你的消息類型中定義一個表示開關(guān)變量的布爾類型字段,如果你不希望它默認初始化為 false 的話。 還要注意的是,在序列化的時候,如果標準類型的字段的值等于它的默認值,這個值是不會存儲到介質(zhì)上的。

枚舉

當你定義一個消息的時候,你可能會希望某個字段在預(yù)定的取值列表里面取值。 例如,假設(shè)你想為 SearchRequest 消息定義一個 corpus字段,它的取值可能是 UNIVERSAL,WEB,IMAGES,LOCAL,NEWS,PRODUCTS 或者 VIDEO。你只需要簡單的利用 enum 關(guān)鍵字定義一個枚舉類型,它的每一個可能的取值都是常量。

在下面的例子中,我們定義了一個名為 Corpus 的枚舉類型,并用它定義了一個字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;
}

你會發(fā)現(xiàn),這個 Corpus 枚舉類型的第一個常量被設(shè)置為 0,每個枚舉類型的定義中,它的第一個元素都應(yīng)該是一個等于 0 的常量。 這是因為:

  • 只有把它的第一個元素設(shè)置為 0,我們才能為枚舉類型定義數(shù)值類型的 默認值 。
  • 這個為 0 的元素必須是第一個元素,為了兼容 proto2 語法(proto2 中枚舉類型的第一個元素總是默認值)。

你可以通過給不同的枚舉常量賦同樣的值的方式來定義別名。 為了定義別名,你需要設(shè)置 allow_alias=true,否則編譯器會報錯。


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 位整數(shù)值的范圍。 由于枚舉的值采用 varient 編碼 方式,負數(shù)編碼效率低所以不推薦。枚舉類型可以定義在消息結(jié)構(gòu)體內(nèi)部(上例所示),也可以定義在外部。如果定義在了外部,同一個.proto 文件中的所有消息都能重用這個枚舉類型。當然,你也可以用
MessageType.EnumType 語法格式在一個消息結(jié)構(gòu)內(nèi)部引用其它消息結(jié)構(gòu)體內(nèi)部定義的枚舉類型來定義字段。

當你用 protoc 編譯器編譯一個包含枚舉類型的.proto 文件時,對于 Java 或 C++編譯生成的代碼中會包含相應(yīng)的枚舉類型,對于 Python 語言會生成
一個特殊的 EnumDescriptor 類,在 Python 運行時會生成一系列整形的符號常量供程序使用。

在消息反序列化的時候,無法識別的枚舉類型會被保留在消息中,但是這種枚舉類型如何復(fù)現(xiàn)依賴于所使用的編程語言。對于支持開放式枚舉類型的編程語言,枚舉類型取值超出特定的符號范圍,例如 C++和 Go 語言,未知的枚舉值在復(fù)現(xiàn)的時候簡單地以基礎(chǔ)型整數(shù)形式存儲。對于支持封閉式枚舉類型的編程語言,例如 Java,未知的枚舉值可以通過特殊的訪問函數(shù)讀取。在任何情況下,只要消息被序列化,無法識別的枚舉值也會跟著被序列化。

欲詳細了解枚舉類型如何在消息類型內(nèi)工作,請根據(jù)你選擇的編程語言,參考 生成代碼參考

使用其它消息類型

你可以使用其它消息類型來定義字段。 假如你想在每一個 SearchResponse 消息里面定義一個 Result 消息類型的字段,你只需要同一個.proto文件中定義 Result 消息,并用它來定義 SearchResponse 中的一個字段即可。

message SearchResponse {
    repeated Result result = 1;
}

message Result {
    string url = 1;
    string title = 2;
    repeated string snippets = 3;
}
導(dǎo)入定義

在上面的例子中,Result 消息類型和 SearchResponse 消息類型是定義在同一個文件中的,如果你想用另外一個.proto 文件中定義的消息類型來定義字段該怎么做呢?

你可以導(dǎo)入其它.proto 文件中已經(jīng)定義的消息,來定義該消息類型的字段。為了導(dǎo)入其它.proto 文件中的定義,你需要在你的.proto 文件頭部申明import 語句:

import "myproject/other_protos.proto";

默認情況下,你只能使用直接導(dǎo)入的.proto 文件中的定義。然而,有時候你可能需要移動一個.proto 文件到一個新的位置。如果直接移動這個.proto文件,你需要一次更新所有引用了這個.proto 文件的所有調(diào)用站點。你可以將文件移動之后(譯注:下面例子中的 new.proto),在原來的位置創(chuàng)建一個虛擬文件(譯注:下面例子中的 old.proto),在其中用 import public 申明指向新位置的.proto 文件(譯注:這樣就實現(xiàn)了跨文件引用,而不需要更新調(diào)用站點里面的代碼了)。通過 import public 申明的導(dǎo)入依賴關(guān)系可以被任何調(diào)用了包含該 import public 語句的調(diào)用者間接繼承。(譯注:這段話繞來繞去就是說,默認情況下,a 中 import b,b 中 import c,a 只能引用 b 里面的定義,而不能引用 c 里面的定義;如果你想 a 跨文件導(dǎo)入引用 c 里面的定義,就要在 b 中申明 import public c,這樣 a 既能引用 b 里面的定義,又能引用 c 里面的定義了) 例如:

// 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

protoc 編譯器通過命令行參數(shù) -I 或 --proto_path 指定導(dǎo)入文件的搜索查找目錄 。如果沒有指定該參數(shù),默認在編譯器所在目錄查找。 通常,你需要設(shè)定 --proto_path 參數(shù)為你的項目根目錄,使用全名稱目錄路徑指定導(dǎo)入文件搜索路徑。

使用 proto2 消息類型

你在 proto3 中可以引用 proto2 消息類型,反之亦可。 然而,proto2 語法格式的枚舉類型,不可以在 proto3 中引用。

消息嵌套

你可以在一個消息結(jié)構(gòu)內(nèi)部定義另外一個消息類型,如下例所示---Result 消息類型定義在 SearchResponse 消息體內(nèi)部。

message SearchResponse {

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

    repeated Result result = 1;
}

如果你想在它的父消息外部復(fù)用這個內(nèi)部定義的消息類型,可以采用 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;
            }

        }
}
更新一個消息類型

如果現(xiàn)有的消息類型無法滿足你的需要---例如,你想為這個消息添加一個字段,但是你又想沿用原來的代碼格式,不用擔心!
你可以非常簡單地就能在不破壞現(xiàn)有代碼的基礎(chǔ)上更新這個消息類型。只需要遵循以下原則:

  • 不要修改現(xiàn)有字段的數(shù)值標簽。
  • 如果你為一個消息添加了字段,所有已經(jīng)序列化調(diào)者依然可以通過舊的消息格式解析新消息生成的代碼。 你需要注意的是這些元素的 默認值 ,以便新代碼可以真確地與舊代碼生成的消息交互。 同理,舊代碼也可以解析新代碼生成的消息:原二進制程序在解析新的消息時,會忽略新添加的字段。 注意:所有未知字段在消息反序列化的時候會被自動拋棄,所以當消息傳遞給新代碼時,新字段不可用。(這和 proto2 不一樣,proto2 中位置字段也會跟消息一起序列化)
  • 字段可以被移除,只要你不再在你更新后的消息里面使用被刪除字段的數(shù)值標簽即可。 或許你想重命名一個字段,通過添加前綴"OBSOLETE_"或者通過 reserved 關(guān)鍵字申明原數(shù)值標簽,以免將來別人重用這個數(shù)值。
  • int32, uint32, int64, uint64, 和 bool 類型是相互兼容的,這意味著你可以改變這類字段的數(shù)值類型,而不必擔心破壞了向前或向后兼容性。 如果數(shù)值從傳輸介質(zhì)上解析的時候,不匹配相應(yīng)的數(shù)值類型,其效果相當于你在 C++里面做了類型轉(zhuǎn)換(例如,一個 64 位的整數(shù)被解析為 32 位整數(shù)時,它會被轉(zhuǎn)換為 32 位整數(shù))。
  • sint32 和 sint64 互相兼容,但是它們不和其它整數(shù)類型兼容。
  • 只要 bytes 類型是有效的 UTF-8 格式,它就和 string 類型兼容。
  • 內(nèi)嵌的消息類型和包含該消息類型編碼后的字節(jié)內(nèi)容的 bytes 類型兼容。
  • fxied32 與 sfixed32 兼容,同樣的,fixed64 與 sfixed64 兼容。
Any

Any 類型允許你在沒有某些消息類型的.proto 定義時,像使用內(nèi)嵌的消息類型一樣使用它來定義消息類型的字段。一個 Any 類型的消息是一個包含任意字節(jié)數(shù)的序列化消息,擁有一個 URL 地址作為全局唯一標識符來解決消息的類型。為了使用 Any 類型的消息,你需要import google/protobuf/any.proto

import "google/protobuf/any.proto";

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

給定 Any 消息類型的默認 URL 是: type.googleapis.com/packagename.messagename。
不同的語言實現(xiàn)都會支持運行庫幫助通過類型安全的方式來封包或解包 Any 類型的消息。在 Java 語言中,Any 類型有專門的訪問函數(shù) pack()和unpack()。在 C++中對應(yīng)的是 PackFrom()和 PackTo()方法。

// 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.IsType<NetworkErrorDetails>()) {
          NetworkErrorDetails network_error;
          detail.UnpackTo(&network_error);
          ... processing network_error ...
    }
}

當前,Any 類型的運行時庫還在開發(fā)中。
如果你已經(jīng)熟悉 proto2 語法 ,Any 類型就是替代了 proto2 中的 extensions 。

Oneof

如果你的消息中定義了很多字段,而且最多每次只能有一個字段被設(shè)置賦值,那么你可以利用 Oneof 特性來實現(xiàn)這種行為并能節(jié)省內(nèi)存。

Oneof 字段除了擁有常規(guī)字段的特性之外,所有字段共享一片 oneof 內(nèi)存,而且每次最多只能有一個字段被設(shè)置賦值。設(shè)置 oneof組中的任意一個成員的值時,其它成員的值被自動清除。 你可以用 case()或 WhickOneof()方法檢查 oneof 組中哪個成員的值被設(shè)置了,具體選擇哪個方法取決于你所使用的編程語言。

使用 Oneof

在.proto 文件中定義 oneof 需要用到 oneof 關(guān)鍵字,其后緊跟的是 oneof 的名字。下例中的 oneof 名字是 test_oneof:

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

然后你就可以往 oneof 定義中添加 oneof 的字段了。 你可以使用任何類型的字段,但是不要使用 repeated 可重復(fù)字段。

在編譯后生成的代碼中,oneof 字段和常規(guī)字段一樣擁有 setter 或 getter 操作函數(shù)。根據(jù)你所選擇的編程語言,你能夠找到一個特殊方法函數(shù)用來檢查是哪一個 oneof 被賦值了。 更多詳情請參考 API 參考 。

Oneof 特性
  • 設(shè)置 oneof 組中某一個成員的值時,其它成員的值被自動清除。因此,當你的 oneof 組中有多個成員的時候,只有最有一個被賦
    值的字段擁有自己的值。
SampleMessage message;
message.set_name("name");
CHECK(message.has_name());
message.mutable_sub_message(); // Will clear name field.
CHECK(!message.has_name());
  • 如果解析器發(fā)現(xiàn)多個 oneof 組的成員被存儲在介質(zhì)上,只有最后一個成員被解析到消息中。
  • oneof 字段不能是 repeated 可重復(fù)的。
  • oneof 字段可以使用反射函數(shù)。
  • 如果你正在使用 C++,確認沒有代碼造成內(nèi)存崩潰。 在下面的例子中,代碼會造成內(nèi)存崩潰。因為 sub_message 在調(diào)用 set_name("name")的時候已經(jīng)被清除.
SampleMessage message;
SubMessage* sub_message = message.mutable_sub_message();
message.set_name("name"); // Will delete sub_message
sub_message->set_... // Crashes here
  • 在 C++中,如果你調(diào)用 swap()方法交換兩個擁有 oneof 的消息,被交換的消息會擁有對方的 oneof 實例:在下面的例子中,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());
向后兼容的問題

在添加或刪除 oneof 字段的時候,需要格外小心。 如果檢查一個 oneof 值的時候返回 None 或 NOT_SET,意味著這個 oneof 沒有字段被賦值過或者如果被賦值過但是使用的是其它版本的 oneof。 沒有辦法區(qū)分它們,因此沒法區(qū)分傳輸介質(zhì)上的一個未知字段是不是 oneof 成
員。(譯注:on the wire,我翻譯為傳輸介質(zhì)不知道準確否?)

標簽重用的問題
  • 將字段移進或移出 oneof 組: 在消息序列化和解析之后,可能會丟失某些信息(某些字段會被清除)。
  • 刪除一個 oneof 字段之后又添加回去: 在消息序列化和解析之后,可能會清除當前為 oneof 設(shè)置的值。
  • 分割或合并 oneof: 這種情況和移動常規(guī)字段到 oneof 組的類似。
Map

如果你想定義 map 類型的數(shù)據(jù),ProtoBuf 提供非常便捷的語法:

map<key_type, value_type> map_field = N;

其中,key_type 可以任意整數(shù)類型或字符串類型(除浮點類型或 bytes 類型意外的任意 標準類型 )。value_Type 可以是任意類型。
例如,你可以創(chuàng)建一個 map 類型的 projects,關(guān)聯(lián)一個 string 類型和一個 Project 消息類型(譯注:作為鍵-值對),用 map 定義如下:

map<string, Project> projects = 3;

map 類型的字段不可重復(fù)(不能用 repeated 修飾)。 需要注意的是:map 類型字段的值在傳輸介質(zhì)上的順序和迭代器的順序是未定義的,你不能指望 map 類型的字段內(nèi)容按指定順序排列。
proto3 目前支持的所有語言都 map 類型的操作 API。 欲知詳情,請根據(jù)你所選擇的編程語言閱讀 API 參考 中相關(guān)內(nèi)容。

向后兼容性

在傳輸介質(zhì)上,下面的代碼等效于 map 語法的實現(xiàn)。因此,即使目前 ProtoBuf 不支持 map 可重復(fù)的特性依然可以用下面這種(變通的)方式來處理:

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

你可以在.proto 文件中選擇使用 package 說明符,避免 ProtoBuf 消息類型之間的名稱沖突。(譯注:這個和 java 里面包的概念以及 C++中命名空間的作用一樣)

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

你可以在消息類型內(nèi)部使用包描述符的引用來定義字段:

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

包描述符對編譯后生成的代碼的影響依賴于你所選擇的編程語言:

  • 在 C++中,生成的類被包裝在以包描述符命名的命名空間中。 例如,Open 類將會出現(xiàn)在命名空間 foo::bar 中。
  • 在 Java 里面,除非你在.proto 文件中顯示聲明了 option java_package,否則這個包名會被 Java 直接采用。
  • 在 Python 里面,包名被直接忽略了,因為 Python 模塊的組織依據(jù)的是 Python 文件的在文件系統(tǒng)中的存放位置。
  • 在 Go 中,除非你在.proto 文件中顯示聲明了 option go_package,否則這個包名會被 Go 作為包名使用。
  • 在 Ruby 里面,所生成的類被包裹在嵌套的 Ruby 命名空間中,包名被轉(zhuǎn)換為 Ruby 大寫樣式(第一個字母大寫,如果第一個不是字母字符,添加PB_前綴)。例如,Open 類會出現(xiàn)在命名空間 Foo::Bar 中。
  • 在 JavaNano 中,除非你在.proto 文件中顯示聲明了 option java_package,否則這個包名會被作為 Java 包名使用。
包和名稱解析

ProtoBuf 解析包和名稱的方式與 C++語言類似:最內(nèi)層的最先被查找,然后是次內(nèi)層,以此類推。每個包對于其父包來說都是“內(nèi)部”的。以一個'.'(英文句點)開頭的包和名稱 (例如 .foo.bar.Baz)表示從最外層開始查找。

protoc 編譯器通過解析被導(dǎo)入的.proto 文件來解析所有的類型名稱。 任何一種被支持的語言的代碼生成器都知道如何正確地引用每一種類型,即使該語言具有不同的作用域規(guī)則。

定義服務(wù)

如果想在 RPC(遠程過程調(diào)用)系統(tǒng)中使用自定義的消息類型,你可以在.proto 文件中定義一個 RPC 服務(wù),protoc 編譯器就會根據(jù)你選擇的編程語言生成服務(wù)器接口和存根代碼。例如,如果你想定義一個 RPC 服務(wù),擁有一個方法來根據(jù)你的 SearchRequest 返回SearchResponse,可以在你的.proto 文件中這樣定義:

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

與 ProtoBuf 最佳搭配的 RPC 系統(tǒng)是 gRPC :一個 Google 開發(fā)的平臺無關(guān)語言無關(guān)的開源 RPC 系統(tǒng)。gRPC 和 ProtoBuf 能夠非常完美的配合,你可以使用專門的 ProtoBuf 編譯插件直接從.proto 文件生成相關(guān) RPC 代碼。

如果你不想使用 gRPC,你也可以用自己的 RPC 來實現(xiàn)和 ProtoBuf 協(xié)作。 更多的關(guān)于RPC 的信息請參考 Proto2 語言指南 。

現(xiàn)在也有很多第三方的采用 ProtoBuf 的 RPC 項目在開展中。 我們已知的這類項目列表,請參考 第三方插件 WIKI 主頁 。

JSON 映射

Proto3 支持標準的 JSON 編碼,在不同的系統(tǒng)直接共享數(shù)據(jù)變得簡單。下表列出的是基礎(chǔ)的類型對照。

在 JSON 編碼中,如果某個值被設(shè)置為 null 或丟失,在映射為 ProtoBuf 的時候會轉(zhuǎn)換為相應(yīng)的 默認值 。 在 ProtoBuf 中如果一個字段是默認值,在映射為 JSON 編碼的時候,這個默認值會被忽略以節(jié)省空間??梢酝ㄟ^選項設(shè)置,使得 JSON 編碼輸出中字段帶有默認值。

選項

你可以在.proto 文件中可以聲明若干選項。 選項不會改變整個聲明的含意,但可能會影響在特定的上下文中它的處理方式。 完整的可用選項列表在文件google/protobuf/descriptor.proto 中定義。

一些選項是文件級的,它們應(yīng)該聲明在最頂級的作用域范圍內(nèi),不要在消息、枚舉或服務(wù)定義中使用它們。一些選項是消息級的,應(yīng)該在消息結(jié)構(gòu)內(nèi)聲明。一些選項是字段級的,它們應(yīng)該聲明在字段定義語句中。選項也可以聲明在枚舉類型、枚舉值、服務(wù)類型、服務(wù)方法中。然而,目前沒有任何有用的選項采用這種方式。

下面列出最常用的一些選項:

  • java_package (文件級選項):這個選項聲明你想生成的 Java 類擁有的包名。 如果沒有在.proto 文件中顯示地聲明 java_package 選項,默認采用 proto 的包名(在.proto 文件中用 package 關(guān)鍵字定義)作為生成的 Java 類的包名。 然而,通常情況下 proto 包名不是好的 Java 格式的包名,proto 包名一般不會是以 Java 包名所期望的反向域名的格式命名。如果沒有生成 Java 代碼,這個選項沒有任何作用。
option java_package = "com.example.foo";
  • java_outer_classname (文件級選項):這個選項定義了你想要編譯生成的 Java 輸出類的最外層類名(也是文件名)。 如果沒有在.proto文件中定義 java_outer_classname 選項,默認將.proto 文件名轉(zhuǎn)換為駝峰格式的文件名(例如 foo_bar.proto 編譯生成 FooBar.java) 。如果沒有生成 Java 代碼,這個選項沒有任何作用。
option java_outer_classname = "Ponycopter";\
  • optimize_for (文件級選項): 它的取值可以是 SPEED,CODE_SIZE,或 LITE_RUNTIME。 這個選項按如下方式影響 C++和 Java 的代碼生成
    器(也可能影響第三方的生成器):
  • SPEED --- 默認值: protoc 編譯器會根據(jù)你定義的消息類型生成序列化、解析和執(zhí)行其它常規(guī)操作的代碼。這些代碼是高度優(yōu)化了的。
  • CODE_SIZE:protoc 編譯器會采用共享、反射技術(shù)來實現(xiàn)序列化、解析和其它操作的代碼,以生成最小的類。 因此,所生成的代碼比采用選項 SPEED 的要小得多,但是操作性能降低了。類的公共成員函數(shù)依然是一樣的(用 SPEED 優(yōu)化選項也是如此)。 這種模式是最有用的應(yīng)用情況是:程序包含很多.proto 文件,但并不追求所有模塊都有極快的速度。
  • LITE_RUNTIME:protoc 編譯器生成的類,僅依賴"lite"運行時庫 (libprotobuf-lite , 而不是 libprotobuf)。"lite"運行時庫比完整庫 (約一個數(shù)量級小) 小得多,但省略了某些功能,如描述符和反射。這對于運行在像手機這樣的有空間限制的平臺上的應(yīng)用程序特別有用。 protoc 編譯器仍將為所有方法生成最快速的代碼,這和在 SPEED 模式一樣。生成的類將為每一種語言實現(xiàn) MessageLite版本的接口,只提供了完整的 Message 接口的一個子集的實現(xiàn)。選項 optimize_for = CODE_SIZE;
  • cc_enable_arenas(文件級選項): 為生成的 C++代碼 啟用 arena 內(nèi)存管理功能。(譯注:Arena Allocation,是一種 GC 優(yōu)化技術(shù),它可以有效地減少因內(nèi)存碎片導(dǎo)致的 Full GC,從而提高系統(tǒng)的整體性能。)
  • objc_class_prefix(文件級選項):這個選項用來設(shè)置編譯器從.proto 文件生成的類和枚舉類型的名稱前綴。這個選型沒有默認值。您應(yīng)該使用 3–5 個大寫字母作為前綴來自 Apple 的建議 。 請注意:所有的 2 個字母的前綴由蘋果公司保留使用。
  • packed (字段級選項): 這個選項 如果被設(shè)置為 true ,對于基本數(shù)值類型的可重復(fù)字段可以獲得更緊湊的編碼。使用此選項,沒有負面影響。然而,請注意,在 2.3.0 版本之前是用此選項解析器會忽略被打包的數(shù)據(jù)。因此,更改現(xiàn)有字段為 packed,會破壞數(shù)據(jù)傳輸?shù)募嫒菪?。對?2.3.0 及以后的版本,這種改變是安全的并且解析器能夠同時接受這兩種格式的打包字段的數(shù)據(jù),但要小心,如果你要處理舊的程序使用了舊版本的 protobuf。
repeated int32 samples = 4 [packed=true];
  • deprecated (字段選項): 這個選項如果設(shè)置為 true ,表示該字段已被廢棄,你不應(yīng)該在后續(xù)的代碼中使用它。 在大多數(shù)語言中這沒有任何實際的影響。在 Java 中,它會變@Deprecated 注釋。將來,其他特定于語言的代碼生成器在為被標注為 deprecated 的字段生成操作函數(shù)的時候,編譯器在嘗試使用該字段的代碼時發(fā)出警告。如果不希望將來有人使用使用這個字段,請考慮用 reserved 關(guān)鍵字 聲明該字段。
int32 old_field = 6 [deprecated=true];
自定義選項

ProtoBuf 允許你使用自定義選項。這是一個 高級的功能,大多數(shù)人不需要。如果你認為你需要創(chuàng)建自定義選項,請參見 Proto2 語言指南 中 更詳細的信息。請注意,創(chuàng)建自定義選項使用 extensions 關(guān)鍵字 ,只能用在 proto3 中的創(chuàng)建自定義選項。

編譯創(chuàng)建類

要想從.proto 文件 生成 Java,Python,C++、 Go、 Ruby,JavaNano,Objective-C 或 C#代碼,你需要在.proto 文件中定義自己的消息類型,并且使用 protoc 編譯器來編譯它。如果你還沒有安裝編譯器,請下載安裝包并按照自述文件中的說明來執(zhí)行安裝。

對于 Go 語言,還需要為編譯器安裝特殊的代碼生成器插件: 請訪問它在 Github 上的代碼倉儲 golang/protobuf ,下載并按照 安裝提示操作。
編譯器的調(diào)用格式如下:

--javanano_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto
  • IMPORT_PATH 指在.proto 文件中解析 import 指令時的查找目錄路徑。如果省略,則使用當前目錄。通過多次傳遞參數(shù)--proto_path,可以實現(xiàn)在多個導(dǎo)入目錄中按順序查找。-I=IMPORT_PATH 是 --proto_path 的縮寫形式。
  • 您可以提供一個或多個 輸出指令 :
  • --cpp_out 在目錄 DST_DIR 中 生成 C++代碼中 。更多細節(jié),請參考 C++代碼的生成。
  • --java_out 在目錄 DST_DIR 中 生成 Java 代碼。 更多細節(jié),請參考 Java 代碼的生成。
  • --python_out 在目錄 DST_DIR 中 生成 Python 代碼 。更多細節(jié),請參考 Python 代碼的生成。
  • --go_out 在目錄 DST_DIR 中 生成 Go 代碼 。 Go 代碼的生成參考文檔即將推出!
  • --ruby_out 在目錄 DST_DIR 中生成 Ruby 代碼。Ruby 代碼的生成參考文檔即將推出!
  • --javanano_out 在目錄 DST_DIR 中 生成 JavaNano 代碼 。 JavaNano 的代碼生成器有很多選項,你可以自定義編譯輸出,欲知詳情請參考代碼生成器的自述文件 。 JavaNano 代碼的生成參考文檔即將推出!
  • --objc_out 在目錄 DST_DIR 中生成 Objective-C 代碼。Objective-C 代碼的生成參考文檔即將推出!
  • --csharp_out 在目錄 DST_DIR 中生成 C#代碼。C# 生成代碼的參考文檔即將推出!
    額外福利,如果 DST_DIR 以.zip 或 .jar 結(jié)尾,編譯器會將代碼輸出寫入到給定名稱的單個 ZIP 格式壓縮文檔中。.jar 輸出也將根據(jù) Java JAR 規(guī)范的要求提供 manifest 清單文件。請注意,如果輸出路徑中具有與編譯輸出文件同名的文件,編譯器將直接覆蓋它而不會保存為另外一個名字。
  • 您必須提供一個或多個.proto 文件作為編譯輸入。多個.proto 文件可以同時編譯。雖然文件是相對于當前目錄來命名的,每個文件至少要在一個 IMPORT_PATH 指定的路徑范圍內(nèi),這樣編譯器可以為其確定規(guī)范的名稱。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容