快來看看Google出品的Protocol Buffer,別只會用Json和XML了


前言

  • 習慣用 Json、XML 數據存儲格式的你們,相信大多都沒聽過Protocol Buffer
  • Protocol Buffer 其實 是 Google出品的一種輕量 & 高效的結構化數據存儲格式,性能比 Json、XML 真的強!太!多!

    由於 Google出品,我相信Protocol Buffer已經具備足夠的吸引力

  • 今天,我將獻上一份 Protocol Buffer的介紹 & 使用攻略,希望你們會喜歡。

目錄


目錄

1. 定義

一種 結構化數據 的數據存儲格式(類似於 XML、Json )

  1. Google 出品 (開源)
  2. Protocol Buffer 目前有兩個版本:proto2 和 proto3
  3. 因為proto3 還是beta 版,所以本次講解是 proto2

2. 作用

通過將 結構化的數據 進行 串行化(序列化),從而實現 數據存儲 / RPC 數據交換的功能

  1. 序列化: 將 數據結構或對象 轉換成 二進制串 的過程
  2. 反序列化:將在序列化過程中所生成的二進制串 轉換成 數據結構或者對象 的過程

3. 特點

  • 對比於 常見的 XML、Json 數據存儲格式,Protocol Buffer有如下特點:

Protocol Buffer 特點

4. 應用場景

傳輸數據量大 & 網絡環境不穩定 的數據存儲、RPC 數據交換 的需求場景

如 即時IM (QQ、微信)的需求場景


總結

在 傳輸數據量較大的需求場景下,Protocol BufferXML、Json 更小、更快、使用 & 維護更簡單!


5. 使用流程

使用 Protocol Buffer 的流程如下:


Protocol Buffer使用流程

5.1 環境配置

  • 要使用Protocol Buffer ,需要先在電腦上安裝Protocol Buffer

  • 整個 安裝過程 只需要按照以下步驟進行即可:

    整個安裝過程請 自備梯子 以保證 網絡暢通

步驟1:下載 Protocol Buffer 安裝包

下載成功后,對文件進行解壓,如下圖:


安裝包 & 解壓后文件

步驟2:安裝 HOMEBREW(已安裝的可以跳過)

// 打開 終端 輸入以下指令 /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

步驟3:安裝 Protocol Buffer

打開 您的終端 依次輸入 下列指令 即可:

brew install autoconf automake libtool curl
// Step1:安裝 Protocol Buffer 依賴
// 注:Protocol Buffer 依賴於  autoconf、automake、libtool、curl

cd Desktop/protobuf-2.6.1
// Step2:進入 Protocol Buffer安裝包 解壓后的文件夾(我的解壓文件放在桌面)

./autogen.sh // Step3:運行 autogen.sh 腳本  ./configure // Step4:運行 configure.sh 腳本 make // Step5:編譯未編譯的依賴包 make check // Step6:檢查依賴包是否完整 make install // Step7:開始安裝Protocol Buffer

步驟4:檢查 Protocol Buffer 是否安裝成功

// 在 終端 下輸入 protoc - - version

出現 libprotoc 2.6.1 提示即表示 安裝成功,如下圖


安裝成功提示

特別注意:

  • protoc = Protocol Buffer的編譯器
  • 作用:將 .proto文件 編譯成對應平台的 頭文件和源代碼文件
  • 在下面會詳細介紹

至此, Protocol Buffer已經安裝完成。下面將講解如何具體使用Protocol Buffer


5.2 構建 Protocol Buffer 消息對象模型

5.2.1 構建步驟


構建步驟

下面將通過一個實例(Android(Java) 平台為例)詳細介紹每個步驟。

5.2.2 詳細介紹

  • 實例說明:構建一個Person類的數據結構,包含成員變量name、id、email等等
// Java類 public class Person { private String name; private Int id; private String email; ... }
  • 平台使用:以 Android(Java) 平台為例來進行演示

步驟1:通過 Protocol Buffer 語法 描述 需要存儲的數據結構

  • 新建一個文件,命名規則為:文件名 = 類名,后綴為 .proto

    此處叫Demo.proto


Demo.proto
  • 根據上述數據結構的需求,在Demo.proto里 通過 Protocol Buffer 語法寫入對應 .proto對象模型的代碼,如下:
package protocobuff_Demo; // 關注1:包名 option java_package = "com.carson.proto"; option java_outer_classname = "Demo"; // 關注2:option選項 // 關注3:消息模型 // 下面詳細說明 // 生成 Person 消息對象(包含多個字段,下面詳細說明) message Person { required string name = 1; required int32 id = 2; optional string email = 3; enum PhoneType { MOBILE = 0; HOME = 1; WORK = 2; } message PhoneNumber { required string number = 1; optional PhoneType type = 2 [default = HOME]; } repeated PhoneNumber phone = 4; } message AddressBook { repeated Person person = 1; }
  • 下面將結合 上述例子 對 Protocol Buffer 語法 進行詳細介紹

Protocol Buffer語法

關注1:包名

package protocobuff_Demo; // 關注1:包名
  • 作用:防止不同 .proto 項目間命名 發生沖突
  • Protocol buffer包的解析過程如下:
    1. Protocol buffer 的類型名稱解析與 C++ 一致:從 最內部 開始查找,依次 向外 進行

      每個包會被看作是其父類包的內部類

    2. Protocol buffer 編譯器會解析 .proto文件中定義的所有類型名
    3. 生成器會根據 不同語言 生成 對應語言 的代碼文件

      a. 即對 不同語言 使用了 不同的規則 進行處理
      b. Protoco Buffer提供 C++、Java、Python 三種語言的 API

關注2:Option選項

option java_package = "com.carson.proto"; option java_outer_classname = "Demo"; // 關注2:option選項
  • 作用:影響 特定環境下 的處理方式

    但不改變整個文件聲明的含義

  • 常用Option選項如下:

option java_package = "com.carson.proto"; // 定義:Java包名 // 作用:指定生成的類應該放在什么Java包名下 // 注:如不顯式指定,默認包名為:按照應用名稱倒序方式進行排序 option java_outer_classname = "Demo"; // 定義:類名 // 作用:生成對應.java 文件的類名(不能跟下面message的類名相同) // 注:如不顯式指定,則默認為把.proto文件名轉換為首字母大寫來生成 // 如.proto文件名="my_proto.proto",默認情況下,將使用 "MyProto" 做為類名 option optimize_for = ***; // 作用:影響 C++ & java 代碼的生成 // ***參數如下: // 1. SPEED (默認)::protocol buffer編譯器將通過在消息類型上執行序列化、語法分析及其他通用的操作。(最優方式) // 2. CODE_SIZE::編譯器將會產生最少量的類,通過共享或基於反射的代碼來實現序列化、語法分析及各種其它操作。 // 特點:采用該方式產生的代碼將比SPEED要少很多, 但是效率較低; // 使用場景:常用在 包含大量.proto文件 但 不追求效率 的應用中。 //3. LITE_RUNTIME::編譯器依賴於運行時 核心類庫 來生成代碼(即采用libprotobuf-lite 替代libprotobuf)。 // 特點:這種核心類庫要比全類庫小得多(忽略了 一些描述符及反射 );編譯器采用該模式產生的方法實現與SPEED模式不相上下,產生的類通過實現 MessageLite接口,但它僅僅是Messager接口的一個子集。 // 應用場景:移動手機平台應用 option cc_generic_services = false; option java_generic_services = false; option py_generic_services = false; // 作用:定義在C++、java、python中,protocol buffer編譯器是否應該 基於服務定義 產生 抽象服務代碼(2.3.0版本前該值默認 = true) // 自2.3.0版本以來,官方認為通過提供 代碼生成器插件 來對 RPC實現 更可取,而不是依賴於“抽象”服務 optional repeated int32 samples = 4 [packed=true]; // 如果該選項在一個整型基本類型上被設置為真,則采用更緊湊的編碼方式(不會對數值造成損失) // 在2.3.0版本前,解析器將會忽略 非期望的包裝值。因此,它不可能在 不破壞現有框架的兼容性上 而 改變壓縮格式。 // 在2.3.0之后,這種改變將是安全的,解析器能夠接受上述兩種格式。 optional int32 old_field = 6 [deprecated=true]; // 作用:判斷該字段是否已經被棄用 // 作用同 在java中的注解@Deprecated
  • 在 ProtocolBuffers 中允許 自定義選項 並 使用
  • 該功能屬於高級特性,使用頻率很低,此處不過多描述。有興趣可查看官方文檔

關注3:消息模型

  • 作用:真正用於描述 數據結構
// 消息對象用message修飾 message Person { required string name = 1; required int32 id = 2; optional string email = 3; enum PhoneType { MOBILE = 0; HOME = 1; WORK = 2; } message PhoneNumber { optional PhoneType type = 2 [default = HOME]; } repeated PhoneNumber phone = 4; } message AddressBook { repeated Person person = 1; }
  • 組成:在 ProtocolBuffers 中:
    1. 一個 .proto 消息模型 = 一個 .proto文件 = 消息對象 + 字段
    2. 一個消息對象(Message) = 一個 結構化數據
    3. 消息對象(Message)里的 字段 = 結構化數據 里的成員變量

結構化數據 & 消息對象 對比

下面會詳細介紹 .proto 消息模型里的 消息對象 & 字段


消息模型

1. 消息對象

在 ProtocolBuffers 中:

  • 一個消息對象(Message) = 一個 結構化數據
  • 消息對象用 修飾符 message 修飾
  • 消息對象 含有 字段:消息對象(Message)里的 字段 = 結構化數據 里的成員變量

結構化數據 & 消息對象 對比

特別注意:


特別注意

a. 添加:在一個 .proto文件 中可定義多個 消息對象

  • 應用場景:盡可能將與 某一消息類型 對應的響應消息格式 定義到相同的 .proto文件 中
  • 實例:
message SearchRequest { required string query = 1; optional int32 page_number = 2; optional int32 result_per_page = 3; } // 與SearchRequest消息類型 對應的 響應消息類型SearchResponse message SearchResponse { … }

b. 一個消息對象 里 可以定義 另外一個消息對象(即嵌套)

message Person { required string name = 1; required int32 id = 2; optional string email = 3; // 該消息類型 定義在 Person消息類型的內部 // 即Person消息類型 是 PhoneNumber消息類型的父消息類型 message PhoneNumber { required string number = 1; } } <-- 多重嵌套 --> message Outer { // Level 0 message MiddleAA { // Level 1 message Inner { // Level 2 required int64 ival = 1; optional bool booly = 2; } } }

2. 字段

  • 消息對象的字段 組成主要是:字段 = 字段修飾符 + 字段類型 +字段名 +標識號

字段組成
  • 下面將對每一項詳細介紹

a. 字段修飾符

  • 作用:設置該字段解析時的規則
  • 具體類型如下:

示意圖

b. 字段類型
字段類型主要有 三 類:

  • 基本數據 類型
  • 枚舉 類型
  • 消息對象 類型
message Person { // 基本數據類型 字段 required string name = 1; required int32 id = 2; optional string email = 3; enum PhoneType { MOBILE = 0; HOME = 1; WORK = 2; } message PhoneNumber { optional PhoneType type = 2 [default = HOME]; // 枚舉類型 字段 } repeated PhoneNumber phone = 4; // 消息類型 字段 }

1. 基本數據類型

.proto基本數據類型 對應於 各平台的基本數據類型如下:


基本數據類型對應表

2. 枚舉類型

  • 作用:為字段指定一個 可能取值的字段集合

    該字段只能從 該指定的字段集合里 取值

  • 說明:如下面例子,電話號碼 可能是手機號、家庭電話號或工作電話號的其中一個,那么就將PhoneType定義為枚舉類型,並將加入電話的集合( MOBILE、 HOMEWORK
// 枚舉類型需要先定義才能進行使用 // 枚舉類型 定義 enum PhoneType { MOBILE = 0; HOME = 1; WORK = 2; // 電話類型字段 只能從 這個集合里 取值 } // 特別注意: // 1. 枚舉類型的定義可在一個消息對象的內部或外部 // 2. 都可以在 同一.proto文件 中的任何消息對象里使用 // 3. 當枚舉類型是在一消息內部定義,希望在 另一個消息中 使用時,需要采用MessageType.EnumType的語法格式 message PhoneNumber { required string number = 1; optional PhoneType type = 2 [default = HOME]; // 使用枚舉類型的字段(設置了默認值) } // 特別注意: // 1. 枚舉常量必須在32位整型值的范圍內 // 2. 不推薦在enum中使用負數:因為enum值是使用可變編碼方式的,對負數不夠高

額外說明
當對一個 使用了枚舉類型的.proto文件 使用 Protocol Buffer編譯器編譯時,生成的代碼文件中:

  • 對 Java 或 C++來說,將有一個對應的 enum 文件
  • 對 Python 來說,有一個特殊的EnumDescriptor 類

    被用來在運行時生成的類中創建一系列的整型值符號常量(symbolic constants)

3. 消息對象 類型

一個消息對象 可以將 其他消息對象類型 用作字段類型,情況如下:


消息對象 類型情況

3.1 使用同一個 .proto 文件里的消息類型

a. 使用 內部消息類型

  • 目的:先在 消息類型 中定義 其他消息類型 ,然后再使用

    即嵌套,需要 用作字段類型的 消息類型 定義在 該消息類型里

  • 實例:
message Person { required string name = 1; required int32 id = 2; optional string email = 3; // 該消息類型 定義在 Person消息類型的內部 // 即Person消息類型 是 PhoneNumber消息類型的父消息類型 message PhoneNumber { required string number = 1; } repeated PhoneNumber phone = 4; // 直接使用內部消息類型 }

b. 使用 外部消息類型

即外部重用,需要 用作字段類型的消息類型 定義在 該消息類型外部

message Person { required string name = 1; required int32 id = 2; optional string email = 3; } message AddressBook { repeated Person person = 1; // 直接使用了 Person消息類型作為消息字段 }

c. 使用 外部消息的內部消息類型

message Person { required string name = 1; required int32 id = 2; optional string email = 3; // PhoneNumber消息類型 是 Person消息類型的內部消息類型 message PhoneNumber { required string number = 1; optional PhoneType type = 2 [default = HOME]; } } // 若父消息類型外部的消息類型需要重用該內部消息類型 // 需要以 Parent.Type 的形式去使用 // Parent = 需要使用消息類型的父消息類型,Type = 需要使用的消息類型 // PhoneNumber父消息類型Person 的外部 OtherMessage消息類型 需要使用 PhoneNumber消息類型 message OtherMessage { optional Person.PhoneNumber phonenumber = 1; // 以 Parent.Type = Person.PhoneNumber 的形式去使用 }

3.2 使用不同 .proto 文件里的消息類型

  • 目的:需要在 A.proto文件 使用 B.proto文件里的消息類型
  • 解決方案:在 A.proto文件 通過導入( import) B.proto文件中來使用 B.proto文件 里的消息類型
import "myproject/other_protos.proto" // 在A.proto 文件中添加 B.proto文件路徑的導入聲明 // ProtocolBuffer編譯器 會在 該目錄中 查找需要被導入的 .proto文件 // 如果不提供參數,編譯器就在 其調用的目錄下 查找

當然,在使用 不同 .proto 文件里的消息類型 時 也會存在想 使用同一個 .proto 文件消息類型的情況,但使用都是一樣,此處不作過多描述。

3.3 將 消息對象類型 用在 RPC(遠程方法調用)系統

  • 解決方案:在 .proto 文件中定義一個 RPC 服務接口,Protocol Buffer編譯器會根據所選擇的不同語言平台 生成服務接口代碼
  • 由於使用得不多,此處不作過多描述,具體請看該文檔

c. 字段名

該字段的名稱,此處不作過多描述。


d. 標識號

  • 作用:通過二進制格式唯一標識每個字段

    1. 一旦開始使用就不能夠再改變
    2. 標識號使用范圍:[1,2的29次方 - 1]
    3. 不可使用 [19000-19999] 標識號, 因為 Protobuf 協議實現中對這些標識號進行了預留。假若使用,則會報錯
  • 編碼占有內存規則:
    每個字段在進行編碼時都會占用內存,而 占用內存大小 取決於 標識號:

    1. 范圍 [1,15] 標識號的字段 在編碼時占用1個字節;
    2. 范圍 [16,2047] 標識號的字段 在編碼時占用2個字節
  • 使用建議

    1. 為頻繁出現的 消息字段 保留 [1,15] 的標識號
    2. 為將來有可能添加的、頻繁出現的 消息字段預留 [1,15] 標識號

關於 字段 的高級用法


高級用法

1. 更新消息對象 的字段

  • 目的:為了滿足新需求,需要更新 消息類型 而不破壞已有消息類型代碼

    即新、老版本需要兼容

  • 更新字段時,需要符合下列規則:

更新規則

2. 擴展消息對象 的字段

  • 作用:使得其他人可以在自己的 .proto 文件中為 該消息對象 聲明新的字段而不必去編輯原始文件
    1. 注:擴展 可以是消息類型也可以是字段類型
    2. 以下以 擴展 消息類型 為例

A.proto

message Request { … extensions 100 to 199; // 將一個范圍內的標識號 聲明為 可被第三方擴展所用 // 在消息Request中,范圍 [100,199] 的標識號被保留為擴展用 // 如果標識號需要很大的數量時,可以將可擴展標符號的范圍擴大至max // 其中max是2的29次方 - 1(536,870,911)。 message Request { extensions 1000 to max; // 注:請避開[19000-19999] 的標識號,因為已被Protocol Buffers實現中預留 }

現在,其他人 就可以在自己的 .proto文件中 添加新字段到Request里。如下:

B.proto

extend Request {

  optional int32 bar = 126; // 添加字段的 標識號必須要在指定的范圍內 // 消息Request 現在有一個名為 bar 的 optional int32 字段 // 當Request消息被編碼時,數據的傳輸格式與在Request里定義新字段的效果是完全一樣的 // 注:在同一個消息類型中一定要確保不會擴展新增相同的標識號,否則會導致數據不一致;可以通過為新項目定義一個可擴展標識號規則來防止該情況的發生 }
  • 要訪問 擴展字段 的方法與 訪問普通的字段 不同:使用專門的擴展訪問函數
  • 實例:
// 如何在C++中設置 bar 值 Request request; request.SetExtension(bar, 15); // 類似的模板函數 HasExtension(),ClearExtension(),GetExtension(),MutableExtension(),以及 AddExtension() // 與對應的普通字段的訪問函數相符

嵌套的擴展

可以在另一個 消息對象里 聲明擴展,如:

message Carson { extend Request { optional int32 bar = 126; } … } // 訪問此擴展的C++代碼: Request request; request.SetExtension(Baz::bar, 15);
  • 對於嵌套的使用,一般的做法是:在擴展的字段類型的范圍內定義該擴展
  • 實例:一個 Request 消息對象需要擴展(擴展的字段類型是Car 消息類型),那么,該擴展就定義在 Car消息類型 里:
message Car { extend Request { optional Car request_ext = 127; // 注:二者並沒有子類、父類的關系 } }
  • 至此,Protoco Buffer的語法已經講解完畢
  • 關於如何根據需求 通過Protoco Buffer語法 去構建 數據結構 相信大家已經非常熟悉了。
  • 在將 .proto文件保存后,進入下一個步驟

步驟2:通過 Protocol Buffer 編譯器 編譯 .proto 文件

  • 作用:將 .proto 文件 轉換成 對應平台的代碼文件

    Protoco Buffer提供 C++、Java、Python 三種開發語言的 API

  • 具體生成文件與平台有關:

對應平台生成文件
  • 編譯指令說明
// 在 終端 輸入下列命令進行編譯 protoc -I=$SRC_DIR --xxx_out=$DST_DIR $SRC_DIR/addressbook.proto // 參數說明 // 1. $SRC_DIR:指定需要編譯的.proto文件目錄 (如沒有提供則使用當前目錄) // 2. --xxx_out:xxx根據需要生成代碼的類型進行設置 // 對於 Java ,xxx = java ,即 -- java_out // 對於 C++ ,xxx = cpp ,即 --cpp_out // 對於 Python,xxx = python,即 --python_out // 3. $DST_DIR :編譯后代碼生成的目錄 (通常設置與$SRC_DIR相同) // 4. 最后的路徑參數:需要編譯的.proto 文件的具體路徑 // 編譯通過后,Protoco Buffer會根據不同平台生成對應的代碼文件
  • 具體實例
// 編譯說明 // 1. 生成Java代碼 // 2. 需要編譯的.proto文件在桌面,希望編譯后生成的代碼也放在桌面 protoc -I=/Users/Carson_Ho/Desktop --java_out=/Users/Carson_Ho/Desktop /Users/Carson_Ho/Desktop/Demo.proto // 編譯通過后,Protoco Buffer會按照標准Java風格,生成Java類及目錄結構

在指定的目錄能看到一個Demo的包文件(含 java類文件)


生成的文件

編譯功能的拓展

a. 使用Android Studio插件進行編譯

  • 需求場景:每次手動執行 Protocol Buffer 編譯器將 .proto 文件轉換為 Java 文件 操作不方便
  • 解決方案:使用 Android Studio的 gradle 插件 protobuf-gradle-plugin,以便於在項目編譯時 自動執行 Protocol Buffers 編譯器

關於protobuf-gradle-plugin插件有興趣的讀者可自行了解,但個人還是建議使用 命令行,畢竟太過折騰插件沒必要

b. 動態編譯

  • 需求場景:某些情況下,人們無法預先知道 .proto 文件,他們需要動態處理一些未知的 .proto 文件

    如一個通用的消息轉發中間件,它無法預先知道需要處理什么類型的數據結構消息

  • 解決方案:動態編譯.proto文件

由於使用得不多,此處不作過多描述,具體請看官方文檔

c. 編寫新的 .proto 編譯器

  • 需求場景: Protocol Buffer 僅支持 C++、java 和 Python 三種開發語言,一旦超出該三種開發語言,Protocol Buffer將無法使用
  • 解決方案:使用 Protocol Buffer 的 Compiler 包 開發出支持其他語言的新的.proto編譯器

由於使用得不多,此處不作過多描述,具體請看官方文檔


5.3 應用到具體平台(Android平台)

  • 終於到了應用到具體平台項目中的步驟了。

    此處以 Android平台 為例

  • 具體步驟如下:

具體步驟

步驟1:將生成的 代碼文件 放入到項目中

  • 對於Android(Java)平台,即將編譯.proto文件生成的Java包文件 整個復制到 Android項目中
  • 放置路徑: app/src/main/java的 文件夾里

項目結構示意圖

步驟2:在 Gradle 添加 Protocol Buffer 版本依賴

compile 'com.google.protobuf:protobuf-java:2.6.1' // 注:protobuf-java的版本 一定要和 安裝protocobuffer的版本 一致

步驟3:具體在Android項目中使用

3.1 消息對象類介紹

通過.proto文件 轉換的 Java源代碼 = Protocol Buffer 類 + 消息對象類(含Builder內部類)

消息對象類 是 Protocol Buffer 類的內部類

由於最常用的都是 消息對象類 和其內部類Builder類 的方法&成員變量,所以此處主要講解這兩者。

3.1.1 消息對象類(Message類)
  • 消息對象類 類通過 二進制數組 寫 和 讀 消息類型
  • 使用方法包括:
<-- 方式1:直接序列化和反序列化 消息 --> protocolBuffer.toByteArray(); // 序列化消息 並 返回一個包含它的原始字節的字節數組 protocolBuffer.parseFrom(byte[] data); // 從一個字節數組 反序列化(解析) 消息 <-- 方式2:通過輸入/ 輸出流(如網絡輸出流) 序列化和反序列化消息 --> protocolBuffer.writeTo(OutputStream output); output.toByteArray(); // 將消息寫入 輸出流 ,然后再 序列化消息 protocolBuffer.parseFrom(InputStream input); // 從一個 輸入流 讀取並 反序列化(解析)消息 // 只含包含字段的getters方法 // required string name = 1; public boolean hasName();// 如果字段被設置,則返回true public java.lang.String getName(); // required int32 id = 2; public boolean hasId(); public int getId(); // optional string email = 3; public boolean hasEmail(); public String getEmail(); // repeated .tutorial.Person.PhoneNumber phone = 4; // 重復(repeated)字段有一些額外方法 public List<PhoneNumber> getPhoneList(); public int getPhoneCount(); // 列表大小的速記 // 作用:通過索引獲取和設置列表的特定元素的getters和setters

常用的如上,更多請看官方文檔

3.1.2 Builder

作用:創建 消息構造器 & 設置/ 獲取消息對象的字段值 & 創建 消息類 實例

屬於 消息對象類 的內部類

a. 創建 消息構造器

Demo.Person.Builder person = Person.newBuilder();

b. 設置/ 獲取 消息對象的字段值 具體方法如下:

// 標准的JavaBeans風格:含getters和setters // required string name = 1; public boolean hasName();// 如果字段被設置,則返回true public java.lang.String getName(); public Builder setName(String value); public Builder clearName(); // 將字段設置回它的空狀態 // required int32 id = 2; public boolean hasId(); public int getId(); public Builder setId(int value); public Builder clearId(); // optional string email = 3; public boolean hasEmail(); public String getEmail(); public Builder setEmail(String value); public Builder clearEmail(); // repeated .tutorial.Person.PhoneNumber phone = 4; // 重復(repeated)字段有一些額外方法 public List<PhoneNumber> getPhoneList(); public int getPhoneCount(); // 列表大小的速記 // 作用:通過索引獲取和設置列表的特定元素的getters和setters public PhoneNumber getPhone(int index); public Builder setPhone(int index, PhoneNumber value); public Builder addPhone(PhoneNumber value); // 將新元素添加到列表的末尾 public Builder addAllPhone(Iterable<PhoneNumber> value); // 將一個裝滿元素的整個容器添加到列表中 public Builder clearPhone(); public Builder isInitialized() // 檢查所有 required 字段 是否都已經被設置 public Builder toString() : // 返回一個人類可讀的消息表示(用於調試) public Builder mergeFrom(Message other) // 將 其他內容 合並到這個消息中,覆寫單數的字段,附接重復的。 public Builder clear() // 清空所有的元素為空狀態。

3.2 具體使用

  • 使用步驟如下:
    步驟1:通過 消息類的內部類Builder類 構造 消息構造器
    步驟2:通過 消息構造器 設置 消息字段的值
    步驟3:通過 消息構造器 創建 消息類 對象
    步驟4:序列化 / 反序列化 消息

  • 具體使用如下:(注釋非常清晰)

public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 步驟1:通過 消息類的內部類Builder類 構造 消息類的消息構造器 Demo.Person.Builder personBuilder = Demo.Person.newBuilder(); // 步驟2:設置你想要設置的字段為你選擇的值 personBuilder.setName("Carson");// 在定義.proto文件時,該字段的字段修飾符是required,所以必須賦值 personBuilder.setId(123);// 在定義.proto文件時,該字段的字段修飾符是required,所以必須賦值 personBuilder.setEmail("carson.ho@foxmail.com"); // 在定義.proto文件時,該字段的字段修飾符是optional,所以可賦值 / 不賦值(不賦值時將使用默認值) Demo.Person.PhoneNumber.Builder phoneNumber = Demo.Person.PhoneNumber.newBuilder(); phoneNumber.setType( Demo.Person.PhoneType.HOME);// 直接采用枚舉類型里的值進行賦值 phoneNumber.setNumber("0157-23443276"); // PhoneNumber消息是嵌套在Person消息里,可以理解為內部類 // 所以創建對象時要通過外部類來創建 // 步驟3:通過 消息構造器 創建 消息類 對象 Demo.Person person = personBuilder.build(); // 步驟4:序列化和反序列化消息(兩種方式) /*方式1:直接 序列化 和 反序列化 消息 */ // a.序列化 byte[] byteArray1 = person.toByteArray(); // 把 person消息類對象 序列化為 byte[]字節數組 System.out.println(Arrays.toString(byteArray1)); // 查看序列化后的字節流 // b.反序列化 try { Demo.Person person_Request = Demo.Person.parseFrom(byteArray1); // 當接收到字節數組byte[] 反序列化為 person消息類對象 System.out.println(person_Request.getName()); System.out.println(person_Request.getId()); System.out.println(person_Request.getEmail()); // 輸出反序列化后的消息 } catch (IOException e) { e.printStackTrace(); } /*方式2:通過輸入/ 輸出流(如網絡輸出流) 序列化和反序列化消息 */ // a.序列化 ByteArrayOutputStream output = new ByteArrayOutputStream(); try { person.writeTo(output); // 將消息序列化 並寫入 輸出流(此處用 ByteArrayOutputStream 代替) } catch (IOException e) { e.printStackTrace(); } byte[] byteArray = output.toByteArray(); // 通過 輸出流 轉化成二進制字節流 // b. 反序列化 ByteArrayInputStream input = new ByteArrayInputStream(byteArray); // 通過 輸入流 接收消息流(此處用 ByteArrayInputStream 代替) try { Demo.Person person_Request = Demo.Person.parseFrom(input); // 通過輸入流 反序列化 消息 System.out.println(person_Request.getName()); System.out.println(person_Request.getId()); System.out.println(person_Request.getEmail()); // 輸出消息 } catch (IOException e) { e.printStackTrace(); } } }

Demo 地址

Carson_Ho的Github :https://github.com/Carson-Ho/ProtocolBuffer

高級功能

  • 貼心的Google還提供將Protocol Buff 編碼方式 轉化為 其他編碼方式,如 JsonXML等等

    即將 Protocol Buff 對象 轉化為其他編碼方式的數據存儲對象

  • 下面展示的是 將 Protocol Buff 對象 轉化為 Json對象

// 步驟1:在Gradle加入依賴 compile 'com.googlecode.protobuf-java-format:protobuf-java-format:1.4' // 步驟2:將`Protocol Buff` 對象 序列化 為 `Json`對象 JsonFormat jsonFormat = new JsonFormat(); String person2json = jsonFormat.printToString(mProtoBuffer);

6. 總結

  • 在 傳輸數據量較大的需求場景下,Protocol BufferXML、Json 更小、更快、使用 & 維護更簡單!
  • 下面用 一張圖 總結在 Android平台中使用 Protocol Buffer 的整個步驟流程:


免責聲明!

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



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