簡介
Protocol Buffer是google出品的一種對象序列化的方式,它的體積小傳輸快,深得大家的喜愛。protobuf是一種平台無關和語言無關的協定,通過protobuf的定義檔案,可以輕松的将其轉換成多種語言的實作,非常友善。
今天将會給大家介紹一下,protobuf的基本使用和同java結合的具體案例。
為什麼使用protobuf
我們知道資料在網絡傳輸中是以二進制進行的,一般我們使用位元組byte來表示, 一個byte是8bits,如果要在網絡上中傳輸對象,一般需要将對象序列化,序列化的目的就是将對象轉換成byte數組在網絡中傳輸,當接收方接收到byte數組之後,再對byte數組進行反序列化,最終轉換成java中的對象。
那麼将java對象序列化可能會有如下幾種方法:
- 使用JDK自帶的對象序列化,但是JDK自帶的序列化本身存在一些問題,并且這種序列化手段隻适合在java程式之間進行傳輸,如果是非java程式,比如PHP或者GO,那麼序列化就不通用了。
- 你還可以自定義序列化協定,這種方式的靈活程度比較高,但是不夠通用,并且實作起來也比較複雜,很可能出現意想不到的問題。
- 将資料轉換成為XML或者JSON進行傳輸。XML和JSON的好處在于他們都有可以區分對象的起始符号,通過判斷這些符号的位置就可以讀取到完整的對象。但是不管是XML還是JSON的缺點都是轉換成的資料比較大。在反序列化的時候對資源的消耗也比較多。
是以我們需要一種新的序列化的方法,這就是protobuf,它是一種靈活、高效、自動化的解決方案。
通過編寫一個.proto的資料結構定義檔案,然後調用protobuf的編譯器,就會生成對應的類,該類以高效的二進制格式實作protobuf資料的自動編碼和解析。 生成的類為定義檔案中的資料字段提供了getter和setter方法,并提供了讀寫的處理細節。 重要的是,protobuf可以向前相容,也就是說老的二進制代碼也可以使用最新的協定進行讀取。
定義.proto檔案
.proto檔案中定義的是你将要序列化的消息對象。我們來一個最基本的student.proto檔案,這個檔案定義了student這個對象中最基本的屬性。
先看一個比較簡單的.proto檔案:
syntax = "proto3";
package com.flydean;
option java_multiple_files = true;
option java_package = "com.flydean.tutorial.protos";
option java_outer_classname = "StudentListProtos";
message Student {
optional string name = 1;
optional int32 id = 2;
optional string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
}
message PhoneNumber {
optional string number = 1;
optional PhoneType type = 2;
}
repeated PhoneNumber phones = 4;
}
message StudentList {
repeated Student student = 1;
}
第一行定義的是protobuf中使用的syntax協定,預設情況下是proto2,因為目前最新的協定是proto3,是以這裡我們使用proto3作為例子。
然後我們定義了所在的package,這個package是指編譯的時候生成檔案的包。這是一個命名空間,雖然我們在後面定義了java_package,但是為了和非java語言中的協定相沖突,是以定義package還是非常有必要的。
然後是三個專門給java程式使用的option。java_multiple_files, java_package, 和 java_outer_classname.
其中java_multiple_files指編譯過後java檔案的個數,如果是true,那麼将會一個java對象一個類,如果是false,那麼定義的java對象将會被包含在同一個檔案中。
java_package指定生成的類應該使用的Java包名稱。 如果沒有明确的指定,則會使用之前定義的package的值。
java_outer_classname選項定義将表示此檔案的包裝類的類名。 如果沒有給java_outer_classname指派,它将通過将檔案名轉換為大寫駝峰來生成。 例如,預設情況下,“student.proto”将使用”Student”作為包裝類名稱。
接下來的部分是消息的定義,對于簡單類型來說可以使用bool, int32, float, double, 和 string來定義字段的類型。
上例中我們還使用了複雜的組合屬性,和嵌套類型。還定義了一個枚舉類。
上面我們為每個屬性值配置設定了ID,這個ID是二進制編碼中使用的唯一“标簽”。因為在protobuf中标記數字1-15比16以上的标記數字占用的位元組空間要更少,是以作為一種優化,通常将1-15這些标記用于常用或重複的元素,而将标記16和更高的标記用于不太常用的可選元素。
然後再來看看字段的修飾符,有三個修飾符分别是optional,repeated和required。
optional表示該字段是可選的,可以設定也可以不設定,如果沒有設定,則會使使用預設值,對于簡單類型來說,我們可以自定義預設值,如果不自定義,就會使用系統的預設值。對于系統的預設值來說,數字為0,字元串為空字元串,布爾值為false。
repeated表示該字段是可以重複的,這種重複實際上就是一種數組的結構。
required表示該字段是必須的,如果該字段沒有值,那麼該字段将會被認為是沒有初始化,嘗試建構未初始化的消息将抛出 RuntimeException,解析未初始化的消息将抛出 IOException。
注意,在Proto3中不支援required字段。
編譯協定檔案
定義好proto檔案之後,就可以使用protoc指令對其進行編譯了。
protoc是protobuf提供的編譯器,一般情況下,可以從github的release庫中直接下載下傳即可。如果你不想直接下載下傳,或者官方提供的庫中并沒有你需要的版本,則可以使用源代碼直接進行編譯。
protoc的使用的指令如下:
protoc --experimental_allow_proto3_optional -I=SRC_DIR --java_out=DST_DIR $SRC_DIR/student.proto
如果編譯proto3,則需要添加–experimental_allow_proto3_optional選項。
我們運作一下上面的代碼。會發現在com.flydean.tutorial.protos包裡面生成了5個檔案。分别是:
Student.java
StudentList.java
StudentListOrBuilder.java
StudentListProtos.java
StudentOrBuilder.java
其中StudentListOrBuilder和StudentOrBuilder是兩個接口,Student和StudentList是這兩個類的實作。
詳解生成的檔案
在proto檔案中,我們主要定義了兩個類Student和StudentList, 他們中定義了一個内部類Builder,以Student為例,看下這個兩個類的定義:
public final class Student extends
com.google.protobuf.GeneratedMessageV3 implements
StudentOrBuilder
public static final class Builder extends
com.google.protobuf.GeneratedMessageV3.Builder<Builder> implements
com.flydean.tutorial.protos.StudentOrBuilder
可以看到他們實作的接口都是一樣的,表示他們可能提供了相同的功能。實際上Builder是對消息的一個封裝器,所有對Student的操作都可以由Builder來完成。
對于Student中的字段來說,Student類隻有這些字段的get方法,而Builder中同時有get和set方法。
對于Student來說,對于字段的方法有:
// required string name = 1;
public boolean hasName();
public 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 phones = 4;
public List<PhoneNumber> getPhonesList();
public int getPhonesCount();
public PhoneNumber getPhones(int index);
對于Builder來說,每個屬性多了兩個方法:
// required string name = 1;
public boolean hasName();
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 phones = 4;
public List<PhoneNumber> getPhonesList();
public int getPhonesCount();
public PhoneNumber getPhones(int index);
public Builder setPhones(int index, PhoneNumber value);
public Builder addPhones(PhoneNumber value);
public Builder addAllPhones(Iterable<PhoneNumber> value);
public Builder clearPhones();
多出的兩個方法是set和clear方法。clear是清空字段的内容,讓其變回初始狀态。
我們還定義了一個枚舉類PhoneType:
public enum PhoneType
implements com.google.protobuf.ProtocolMessageEnum
這個類的實作和普通的枚舉類沒太大差別。
Builders 和 Messages
如上一節所示,Message對應的類隻有get和has方法,是以它是不可以變的,消息對象一旦被構造,就不能被修改。要建構消息,必須首先建構一個建構器,将要設定的任何字段設定為你選擇的值,然後調用建構器的 build()方法。
每次調用Builder的方法都會傳回一個新的Builder,當然這個傳回的Builder和原來的Builder是同一個,傳回Builder隻是為了友善進行代碼的連寫。
下面的代碼是如何建立一個Student執行個體:
Student xiaoming =
Student.newBuilder()
.setId(1234)
.setName("小明")
.setEmail("[email protected]")
.addPhones(
Student.PhoneNumber.newBuilder()
.setNumber("010-1234567")
.setType(Student.PhoneType.HOME))
.build();
Student中提供了一些常用的方法,如isInitialized()檢測是否所有必須的字段都設定完畢。toString()将對象轉換成為字元串。使用它的Builder還可以調用clear()用來清除已設定的狀态,mergeFrom(Message other)用來對對象進行合并。
序列化和反序列化
生成的對象中提供了序列化和反序列化方法,我們隻需要在需要的時候對其進行調用即可:
- byte[] toByteArray();: 序列化消息并傳回一個包含其原始位元組的位元組數組。
- static Person parseFrom(byte[] data);: 從給定的位元組數組中解析一條消息。
- void writeTo(OutputStream output);: 序列化消息并将其寫入 OutputStream.
- static Person parseFrom(InputStream input);: 從一個消息中讀取并解析消息 InputStream.
通過使用上面的方法,可以很友善的将對象進行序列化和反序列化。
協定擴充
我們在定義好proto之後,假如後續還希望對其進行修改,那麼我們希望新的協定對曆史資料是相容的。那麼我們需要考慮下面幾點:
- 不能更改現有字段的ID編号。
- 不能添加和删除任何必填字段。
- 可以 删除可選或重複的字段。
- 可以 添加新的可選字段或重複字段,但您必須使用新的ID編号。
總結
好了,protocol buf的基本用法就介紹到這裡,下一篇文章我們會更加詳細的介紹proto協定的具體内容,敬請期待。
本文的例子可以參考:
learn-java-base-9-to-20本文已收錄于 http://www.flydean.com/01-protocolbuf-guide/最通俗的解讀,最深刻的幹貨,最簡潔的教程,衆多你不知道的小技巧等你來發現!
歡迎關注我的公衆号:「程式那些事」,懂技術,更懂你!