以下內容主要整理自官方文檔。
通常序列化和解析結構化數據的幾種方式?
Protocol Buffers
是一個更靈活、高效、自動化的解決方案。它通過一個.proto文件描述你想要的數據結構,它能夠自動生成解析 這個數據結構的Java類,這個類提供高效的讀寫二進制格式數據的API。最重要的是Protocol Buffers
的擴展性和兼容性很強,只要遵很少的規則 就可以保證向前和向后兼容。
package tutorial;option java_package = "com.example.tutorial";option java_outer_classname = "AddressBookProtos";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 Buffers 語法
.proto文件的語法跟Java的很相似,message相當于class,enum即枚舉類型, 基本的數據類型有bool
,int32
,float
,double
, 和string
,類型前的修飾符有:
NOTE 1: 由于歷史原因,數值型的repeated字段后面最好加上[packed=true],這樣能達到更好的編碼效果。 repeated int32 samples = 4 [packed=true];
NOTE 2: Protocol Buffers不支持map,如果需要的話只能用兩個repeated代替:keys和values。
字段后面的1,2,3…是它的字段編號(tag number),注意這個編號在后期協議擴展的時候不能改動。[default = HOME]
即默認值。 為了避免命名沖突,每個.proto文件最好都定義一個package
,package用法和Java的基本類似,也支持import
。
import "myproject/other_protos.proto";
擴展
PB語法雖然跟Java類似,但是它并沒有繼承機制,它有所謂的Extensions
,這很不同于我們原來基于面向對象的JavaBeans
式的協議設計。
Extensions
就是我們定義message
的時候保留一些field number
讓第三方去擴展。
message Foo { required int32 a = 1; extensions 100 to 199;}
message Bar { optional string name =1; optional Foo foo = 2;} extend Foo { optional int32 bar = 102;}
也可以嵌套:
message Bar { extend Foo { optional int32 bar = 102; } optional string name =1; optional Foo foo = 2;}
Java中設置擴展的字段:
BarProto.Bar.Builder bar = BarProto.Bar.newBuilder();bar.setName("zjd"); FooProto.Foo.Builder foo = FooProto.Foo.newBuilder();foo.setA(1);foo.setExtension(BarProto.Bar.bar,12); bar.setFoo(foo.build());System.out.println(bar.getFoo().getExtension(BarProto.Bar.bar));
個人覺得使用起來非常不方便。
有關PB的語法的詳細說明,建議看官方文檔。PB的語法相對比較簡單,一旦能嵌套就能定義出非常復雜的數據結構,基本可以滿足我們所有的需求。
編譯.proto文件可以用Google提供的一個proto程序來編譯,Windows版本下載protoc.exe?;臼褂萌缦拢?/p>
protoc.exe -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/addressbook.proto
.proto文件中的java_package
和java_outer_classname
定義了生成的Java類的包名和類名。
AddressBookProtos.java
中對應.proto文件中的每個message都會生成一個內部類:AddressBook
和Person
。 每個類都有自己的一個內部類Builder
用來創建實例。messages只有getter
只讀方法,builders既有getter
方法也有setter
方法。
Person
// 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 phone = 4;public List<PhoneNumber> getPhoneList();public int getPhoneCount();public PhoneNumber getPhone(int index);
Person.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 phone = 4;public List<PhoneNumber> getPhoneList();public int getPhoneCount();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();
除了JavaBeans風格的getter-setter方法之外,還會生成一些其他getter-setter方法:
message嵌套message會生成嵌套類,enum會生成未Java 5的枚舉類型。
public static enum PhoneType { MOBILE(0, 0), HOME(1, 1), WORK(2, 2), ; ...}Builders vs. Messages
所有的messages生成的類像Java的string一樣都是不可變的。要實例化一個message必須先創建一個builder, 修改message類只能通過builder類的setter方法修改。每個setter方法會返回builder自身,這樣就能在一行代碼內完成所有字段的設置:
Person john = Person.newBuilder() .setId(1234) .setName("John Doe") .setEmail("jdoe@example.com") .addPhone( Person.PhoneNumber.newBuilder() .setNumber("555-4321") .setType(Person.PhoneType.HOME)) .build();
每個message和builder提供了以下幾個方法:
每個message都有以下幾個方法用來讀寫二進制格式的protocol buffer。關于二進制格式,看這里(可能需要FQ)。
每個Protocol buffer
類提供了對于二進制數據的一些基本操作,在面向對象上面做的并不是很好,如果需要更豐富操作或者無法修改.proto文件 的情況下,建議在生成的類的基礎上封裝一層。
import com.example.tutorial.AddressBookProtos.AddressBook;import com.example.tutorial.AddressBookProtos.Person;import java.io.BufferedReader;import java.io.FileInputStream;import java.io.FileNotFoundException;import java.io.FileOutputStream;import java.io.InputStreamReader;import java.io.IOException;import java.io.PrintStream;class AddPerson { // This function fills in a Person message based on user input. static Person PromptForAddress(BufferedReader stdin, PrintStream stdout) throws IOException { Person.Builder person = Person.newBuilder(); stdout.print("Enter person ID: "); person.setId(Integer.valueOf(stdin.readLine())); stdout.print("Enter name: "); person.setName(stdin.readLine()); stdout.print("Enter email address (blank for none): "); String email = stdin.readLine(); if (email.length() > 0) { person.setEmail(email); } while (true) { stdout.print("Enter a phone number (or leave blank to finish): "); String number = stdin.readLine(); if (number.length() == 0) { break; } Person.PhoneNumber.Builder phoneNumber = Person.PhoneNumber.newBuilder().setNumber(number); stdout.print("Is this a mobile, home, or work phone? "); String type = stdin.readLine(); if (type.equals("mobile")) { phoneNumber.setType(Person.PhoneType.MOBILE); } else if (type.equals("home")) { phoneNumber.setType(Person.PhoneType.HOME); } else if (type.equals("work")) { phoneNumber.setType(Person.PhoneType.WORK); } else { stdout.println("Unknown phone type. Using default."); } person.addPhone(phoneNumber); } return person.build(); } // Main function: Reads the entire address book from a file, // adds one person based on user input, then writes it back out to the same // file. public static void main(String[] args) throws Exception { if (args.length != 1) { System.err.println("Usage: AddPerson ADDRESS_BOOK_FILE"); System.exit(-1); } AddressBook.Builder addressBook = AddressBook.newBuilder(); // Read the existing address book. try { addressBook.mergeFrom(new FileInputStream(args[0])); } catch (FileNotFoundException e) { System.out.println(args[0] + ": File not found. Creating a new file."); } // Add an address. addressBook.addPerson( PromptForAddress(new BufferedReader(new InputStreamReader(System.in)), System.out)); // Write the new address book back to disk. FileOutputStream output = new FileOutputStream(args[0]); addressBook.build().writeTo(output); output.close(); }}View CodeReading A Message
import com.example.tutorial.AddressBookProtos.AddressBook;import com.example.tutorial.AddressBookProtos.Person;import java.io.FileInputStream;import java.io.IOException;import java.io.PrintStream;class ListPeople { // Iterates though all people in the AddressBook and prints info about them. static void Print(AddressBook addressBook) { for (Person person: addressBook.getPersonList()) { System.out.println("Person ID: " + person.getId()); System.out.println(" Name: " + person.getName()); if (person.hasEmail()) { System.out.println(" E-mail address: " + person.getEmail()); } for (Person.PhoneNumber phoneNumber : person.getPhoneList()) { switch (phoneNumber.getType()) { case MOBILE: System.out.print(" Mobile phone #: "); break; case HOME: System.out.print(" Home phone #: "); break; case WORK: System.out.print(" Work phone #: "); break; } System.out.println(phoneNumber.getNumber()); } } } // Main function: Reads the entire address book from a file and prints all // the information inside. public static void main(String[] args) throws Exception { if (args.length != 1) { System.err.println("Usage: ListPeople ADDRESS_BOOK_FILE"); System.exit(-1); } // Read the existing address book. AddressBook addressBook = AddressBook.parseFrom(new FileInputStream(args[0])); Print(addressBook); }}View Code擴展協議
實際使用過程中,.proto
文件可能經常需要進行擴展,協議擴展就需要考慮兼容性的問題,Protocol Buffers
有良好的擴展性,只要遵守一些規則:
tag number
;required
字段;optional
和repeated
字段;optional
和repeated
字段,但是必須使用新的tag number
。向前兼容(老代碼處理新消息):老的代碼會忽視新的字段,刪除的option字段會取默認值,repeated字段會是空集合。
向后兼容(新代碼處理老消息):對新的代碼來說可以透明的處理老的消息,但是需要謹記新增的字段在老消息中是沒有的, 所以需要顯示的通過has_方法判斷是否設置,或者在新的.proto中給新增的字段設置合理的默認值, 對于可選字段來說如果.proto中沒有設置默認值那么會使用類型的默認值,字符串為空字符串,數值型為0,布爾型為false。
注意對于新增的repeated字段來說因為沒有has_
方法,所以如果為空的話是無法判斷到底是新代碼設置的還是老代碼生成的原因。
建議字段都設置為optional,這樣擴展性是最強的。
編碼英文好的可以直接看官方文檔,但我覺得博客園上這篇文章說的更清楚點。
總的來說Protocol Buffers
的編碼的優點是非常緊湊、高效,占用空間很小,解析很快,非常適合移動端。 缺點是不含有類型信息,不能自描述(使用一些技巧也可以實現),解析必須依賴.proto
文件。
Google把PB的這種編碼格式叫做wire-format
。
PB的緊湊得益于Varint這種可變長度的整型編碼設計。
(圖片轉自http://www.49028c.com/shitouer/archive/2013/04/12/google-protocol-buffers-encoding.html)
對比XML 和 JSON數據大小我們來簡單對比下Protocol Buffer
和XML
、JSON
。
.proto
message Request { repeated string str = 1; repeated int32 a = 2;}
JavaBean
public class Request { public List<String> strList; public List<Integer> iList;}
首先我們來對比生成數據大小。測試代碼很簡單,如下:
public static void main(String[] args) throws Exception { int n = 5; String str = "testtesttesttesttesttesttesttest"; int val = 100; for (int i = 1; i <=n; i++) { for (int j = 0; j < i; j++) { str += str; } protobuf(i, (int) Math.pow(val, i), str); serialize(i, (int) Math.pow(val, i), str); System.out.println(); }}public static void protobuf(int n, int in, String str) { RequestProto.Request.Builder req = RequestProto.Request.newBuilder(); List<Integer> alist = new ArrayList<Integer>(); for (int i = 0; i < n; i++) { alist.add(in); } req.addAllA(alist); List<String> strList = new ArrayList<String>(); for (int i = 0; i < n; i++) { strList.add(str); } req.addAllStr(strList); // System.out.println(req.build()); byte[] data = req.build().toByteArray(); System.out.println("protobuf size:" + data.length);}public static void serialize(int n, int in, String str) throws Exception { Request req = new Request(); List<String> strList = new ArrayList<String>(); for (int i = 0; i < n; i++) { strList.add(str); } req.strList = strList; List<Integer> iList = new ArrayList<Integer>(); for (int i = 0; i < n; i++) { iList.add(in); } req.iList = iList; String xml = SerializationInstance.sharedInstance().simpleToXml(req); // System.out.println(xml); System.out.println("xml size:" + xml.getBytes().length); String json = SerializationInstance.sharedInstance().fastToJson(req); // System.out.println(json); System.out.println("json size:" + json.getBytes().length);}View Code
隨著n的增大,int
類型數值越大,string
類型的值也越大。我們先將str
置為空:
還原str值,將val
置為1:
可以看到對于int型的字段protobuf
比xml
和json
的都要小不少,尤其是xml,這得益于它的Varint
編碼。對于string類型的話,隨著字符串內容越多, 三者之間基本就沒有差距了。
針對序列話和解析(反序列化)的性能,選了幾個我們項目中比較常用的方案和Protocol Buffer
做了下對比, 只是簡單的基準測試(用的是bb.jar
)結果如下:
可以看到數據量較小的情況下,protobuf要比一般的xml,json序列化快1-2個數量級,fastjson
已經很快了,但是protobuf比它還是要快不少。
protobuf解析的性能比一般的xml,json反序列化要快2-3個數量級,比fastjson也要快1個數量級左右。
新聞熱點
疑難解答