2017年10月30日

protobuf

Protocol Buffers - Google's data interchange format Protocol Buffers 是一種輕便高效的結構化數據存儲格式,可以用於資料序列化,適合做儲存資料或 RPC 資料交換的格式,可用在通訊協定、數據儲存等領域,是一種與程式語言無關、平台無關、可擴展的序列化結構資料格式。

安裝 protobuf compiler

在 mac 可透過 macport 安裝 protobuf 的 compiler for java, cpp

sudo port install protobuf-java

安裝時會同時安裝

protobuf-cpp

如果要用 protobuf 3,則要安裝 protobuf3-java,如果已經裝了 protobuf-java,必須要先移除後才能安裝 protobuf3

sudo port uninstall protobuf-java
sudo port uninstall protobuf-cpp

sudo port install protobuf3-java

安裝時會同時安裝

maven3
maven_select
protobuf3-cpp

使用 protoc 的 command line 指令

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 --javanano_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto

ex:

protoc -I=. --java_out=. ./file.proto

.proto 資料格式文件

安裝 protobuf compiler 後,需要編寫一個 .proto 的資料格式文件,用來描述要傳遞的資料內容。

目前官方有提供 proto3 的規格,但還沒有把 tutorial 的部分由 proto2 更新到 proto3。

以下為 proto3 language guide 的內容:

Defining A Message Type 定義 message

syntax = "proto3";

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

如果是 proto2 就不需要寫 syntax = "proto3";

Specifying Field Types 指定資料型別

資料型別可以是 scalar type 或是 enum 或是其他 message

  • Assigning Tags

    等號後面的的數字 field number 代表 message 中的標記欄位,不能改變。最小的field number 設為 1, 編碼後的tag佔1 byte。最大的field number 設為 2^29 - 1, 編碼後的tag佔5 bytes。19000~19999 是 protobuf 保留的 field number,不能在 .proto 中使用

    field number 需要的 bytes 數量
    1~15 1
    16~2047 2
    2^11~2^18-1 3
    2^18~2^25-1 4
    2^25~2^32-1 5
  • Specifying Field Rules

    singular : 0 or 1 個值,不能超過1個

    repeated : 0 or 1 or more,序列化和反序列化過程中,會保留 repeated values 的順序。在proto3中,repeated fields of scalar numeric types默認使用packed方式。

  • Adding More Message Types

    在一個.proto文件中可以定義多個messages。

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

    使用 // 註釋

    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.
    }
  • Reserved Fields

    如果更新一個message時,需要完全移除某個欄位,但未來可能又重新使用這個欄位標籤,這可能會在導入舊版本的.proto文件時產生嚴重的bug。因此,需要將deleted fields標記為reserved。reserved field number 和 field name 應該分開寫。

    message Foo {
      reserved 2, 15, 9 to 11;
      reserved "foo", "bar";
    }
  • What's Generated From Your .proto?

    使用protobuf的編譯器protoc編譯.proto文件時,編譯器會產生與message類型相關的所選語言的代碼。包括但不限於:

    1. getting and setting field values
    2. serializing your messages to an output stream
    3. parsing your messages from an input stream

    以 C++ 及 Java 為例: C++ :產生1個 .h 文件和1個 .cc 文件 Java:產生1個 .java 文件,其中每個message class都包括Builder

  • Scalar Value Types

.proto Type Java Type C++ Type 備註
double double double
float float float
int32 int int32 使用variable-length encoding。如果你的資料可能含有負數,那麼請使用sint32。
int64 long int64 使用variable-length encoding。如果你的字段可能含有負數,那麼請使用sint64。
uint32 int[1] uint32 使用variable-length encoding。
uint64 long[1] uint64 使用variable-length encoding。
sint32 int int32 使用variable-length encoding。對於有負數的整數,比平常使用的int32效率還好。
sint64 long int64 使用variable-length encoding。
fixed32 int[1] uint32 總是4個 bytes。如果數值總是比 228 大的話,這個資料型別會比uint32效率還好。
fixed64 long[1] uint64 總是8個 bytes。如果數值總是比 256 大的話,這個資料型別會比uint64效率還好。
sfixed32 int int32 4個 bytes
sfixed64 long int64 8個 bytes
bool boolean bool
string String string 必須是UTF-8編碼或者7-bit ASCII編碼
bytes ByteString string 可能包含任意順序的資料
  • Default Values

    當parse一個message時,如果編碼的message不包括某個singular field,解碼的message的相應的field則被設為默認值。

    1. strings : empty string
    2. bytes : empty bytes
    3. bools : false
    4. numeric types : 0
    5. enums : first defined enum value, which must be 0
    6. message fields : 依賴於不同的語言實現。
    7. repeated fields : empty list

    一旦一個message被解析,我們並不知道某個field是明確地被設為默認值,還是未設定從而自動被設為默認值。因此,我們定義message時,並不能依賴默認值的行為。 例如,如果你有一個bool field isMine, true表示這個方塊(是Mine),false表示這個方塊(不是Mine)。當你收到一個message isMine = false,那這個方塊到底(不是雷)還是(未設定)呢? 因此,不要依賴任何默認值的行為。更應該注意的是,如果一個field的value是默認值,將不會被序列化。

  • Enumerations

    1. 第一個 enum value 必須為0。
    2. 必須有1個0值作為枚舉的默認值。proto2使用第一個枚舉值作為默認值,為了和proto2兼容,必須將一個枚舉值設為0。
    3. 可以定義aliases來使得不同的枚舉量映射到同一枚舉值。
    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;
    }
  • Using Other Message Types

    可在一個 message 中使用另一個 message

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

    Nested Types

    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;
        }
      }
    }
  • Importing Definitions

    使用另一個.proto文件中的定義,需要import

    1. import : 只能直接使用,不可傳遞
    2. import public : 可以傳遞
  • Using proto2 Message Types

    proto2的enum不能用在proto3中,其他的都理論上是相容的。

  • Updateing Message Type

    更新 Message Type 定義時,要注意以下事項

    1. 不要修改任一個 field 的 numeric 標籤
    2. 增加新的 field 時,舊訊息可以用新的 code parsing,同樣的,parsing 舊訊息的程式碼還是能用來 parsing 新訊息。
    3. 可以移除 fields,只要沒有重用 rag number,但也可以修改欄位名稱作為標記 ex: 加上 prefix: "OBSOLETE_",或是將該欄位標記為 reserved。
    4. int32, uint32, int64, uint64, and bool are all compatible,也就是說可以隨時改變為這幾種資料型別中的任一種。
    5. sint32 and sint64 可以互換,但跟其他整數型別不相容。
    6. string and bytes 如果使用 UTF-8 就是相容的。
    7. 內嵌的訊息是跟 bytes 相容的。
    8. fixed32 跟 sfixed32 相容。fixed64 跟 sfixed64 相容
    9. enum 跟 int32, uint32, int64, and uint64 相容。
  • Unknown Fields proto3 可處理未定義的欄位資料,但不同語言的實作有可能會刪除這些未知的欄位,這跟 proto2 的做法不同。

  • Any

    可使用 Any 嵌入沒有 .proto 定義的 message,Any 就是以 bytes 的方式序列化,使用 Any 必須要 import google/protobuf/any.proto

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

    Any 用來取代 proto2 的 extensions

  • Oneof 訊息只會包含多個欄位其中的某一個欄位,Oneof 不能設定為 repeated

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

    map<string, Project> projects = 3;

    Map 不能為 repeated,maps are sorted by key

  • Packages

    package foo.bar;
    message Open { ... }
  • Defining Services

    可定義 RPC service interface,compiler 會自動產生 code and stubs

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

    protobuf 最直覺的 RPC 系統為 gRPC: a language- and platform-neutral open source RPC system developed at Google

proto3 與 proto2 的差異

proto3 比 proto2 支持更多語言,語法更簡單,去掉了一些複雜的語法和特性。

  1. 在第一行非空白非註釋行,必須寫:

    syntax = "proto3";
  2. 移除了 "required"

  3. "repeated" 默認採用 packed 編碼;

    在 proto2 中,需要明確使用 [packed=true] 來指定 packed 編碼方式

  4. 語言增加 Go、Ruby、JavaLite

  5. 移除了 default

    在 proto2 中,可以使用 default 選項為某一字段指定默認值。在 proto3 中,字段的默認值只能根據資料類型由系統決定。在資料被設置為默認值的時候,該字段不會被序列化。這樣可以節省空間,提高效率。

    但這樣就無法區分某資料是根本沒有數值,還是被設定為 default value。這在 proto3 中問題不大,但在 proto2 中會有問題。更新協議的時候使用 default 選項為某個字段指定了一個與原來不同的默認值,舊的程式取到的該值會與新程式不一樣。

  6. enum 類型的第一個數值必須為 0

  7. 移除了對分組的支援

    分組的功能完全可以用消息嵌套的方式來實現,並且更清晰。在 proto2 中已經把分組語法標註為"expired"了。

  8. 舊程式在解析新增字段時,會把不認識的變數丟棄,再序列化後新增的變數就不見了

    在 proto2 中,舊程式雖然會忽視不認識的新增變數,但並不會將其丟棄,再序列化的時候那些字段會被原樣保留。

    但官方終於同意在 proto3 中恢復 proto2 的處理方式了。

  9. 移除了對擴展的支持,新增了 Any 類型

    Any 類型是用來替代 proto2 中的擴展的。目前還在開發中。

    proto2 中的擴展特性很像 Swift 語言中的擴展。理解起來有點困難,使用起來更是會帶來不少混亂。

    相比之下,proto3 中新增的 Any 類型有點像 C/C++ 中的 void* 。

  10. 增加了 JSON 映射特性

ref: Protobuf 的 proto3 與 proto2 的區別

References

比起 JSON 更方便、更快速、更簡短的 Protobuf 格式

Protocol_Buffers wiki

Google Protocol Buffer 的使用和原理

Protobuf 語法指南

protoc java

ScalaPB: protobuf for scala


erlang gpb

erlang_protobuffs