protobuf是Google建立的,是一種語言無關、平臺無關、可擴展的序列化結構化數據的方法,可用於通訊協議、數據存儲等。
在序列化結構化數據的機制中,是靈活、高效、自動化的。相比於XML,更小、更快、更簡單。java
在序列化方面,protobuf具備如下優點:編程
舉個例子:我們爲一個具備name
和emai
的person
建模。
在XML中,咱們是這樣寫的:json
<person> <name>John Doe</name> <email>jdoe@example.com</email> </person>
在protocol buffers中,咱們是這樣寫的:數組
# Textual representation of a protocol buffer. # This is *not* the binary format used on the wire. person { name: "John Doe" email: "jdoe@example.com" }
從性能上看,protocol buffers通過編碼後,用二進制的方式傳輸,只要可能有28個字節長,且須要大約100-200納秒來解析。即使刪除空白,那麼XML至少是69字節長,解析大約須要5000-10000納秒。
在編碼方面,protocol buffers也是更簡潔的。
protocol buffers讀取是這樣的:ide
cout << "Name: " << person.name() << endl; cout << "E-mail: " << person.email() << endl;
XML讀取是這樣的:性能
cout << "Name: " << person.getElementsByTagName("name")->item(0)->innerText() << endl; cout << "E-mail: " << person.getElementsByTagName("email")->item(0)->innerText() << endl;
固然,相比於protobuf,XML依然也有本身的優點的,好比基於文本的使用標記(例如 HTML)建模。並且XML對於咱們來講,是可讀的,可編輯的,它具備自解釋性。protobuf是二進制的形式,因此只有.proto定義,咱們才能夠進行解讀。測試
我ide是idea的,安裝了兩個插件,GenProtobuf是用來生成java文件的,Protobuf Support是用來高亮、語法檢查等。
GEnProtobuf設置,打開菜單:
設置protoc.exe的路徑,以及生成的文件路徑
在idea點擊.proto文件,右鍵,選擇quick gen protobuf rules,就能夠在咱們指定的地方生成java文件,若是選擇quick gen protobuf here就會在當前目錄生成java文件。
pom文件以下:ui
<dependencies> <dependency> <groupId>com.google.protobuf</groupId> <artifactId>protobuf-java</artifactId> <version>3.9.1</version> </dependency> <dependency> <groupId>com.google.protobuf</groupId> <artifactId>protobuf-java-util</artifactId> <version>3.9.1</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.9</version> <scope>test</scope> </dependency> </dependencies>
官方雖然將繼續支持proto2,但鼓勵新代碼使用Proto3,由於它更容易使用,支持更多的語言。google
Person.proto:編碼
syntax = "proto3";//指定了用的是proto3語法,不寫默認proto2語法 package ch0;//定義proto的包名,能夠經過package防止命名衝突。 /* Person包含name和age兩個屬性 */ message Person { string name = 1; int32 age =2; }
跟咱們java同樣,用//
和/* ... */
來註釋單行和一段。
文件的第一個非空、非註釋行,指定了使用了proto3語法,若是沒指定,默認是proto2語法。message Person{}
指定了這個Message的名稱是Person,裏面每一行都有三項,字段類型、字段名稱、字段編號。
字段類型與各個語言的關係圖以下:
第二個是字段名稱,名稱確定要惟一,這個沒什麼好說的。
repeated關鍵字,重複的意思,相似於數組或List。
第三個是字段編號,每一個字段都有一個唯一的編號。在上面例子中,假設name的值是張三,在轉換二進制的時候,不會是name:張三這種形式的,而是用字段編號1:張三這種形式。在解析二進制的時候,也是獲取1:張三,而後1跟上面Message對應取到name,再轉爲name,這樣二進制字節就會變短了。
這些字段編號,考慮到需求變化致使多個版本字段可能變更,因此儘可能不要修改字段編號,否則有可能1.0版本1對應的是name,1.1版本卻對應着age,系統就混亂了。因爲1到15的字段編號只須要一個字節進行編碼,因此用的比較頻繁的字段,建議都要這個範圍。
字段編號的範圍是1到2^29-1(536,870,911),中間19000到19999是做爲保留的數字,咱們不可以使用的,在編譯的時候就會報錯。並且,也不能使用以前的保留數字,這個後面會講字段刪除修改時要怎麼處理。
經過GenProtobuf生成的java文件,這邊沒有指定文件夾和類名,具體的文件生成規則後面講。爲了方便,後面我都指定文件夾和類名。
咱們能夠用生成的java類,調用name和age的set和get方法。
@Test public void test1(){ PersonOuterClass.Person.Builder person = PersonOuterClass.Person.newBuilder(); person.setAge(18); person.setName("張三"); person.getAge(); person.getName(); }
一個.proto文件中定義多個Message。好比多個有關聯的Message,就能夠添加到相同的.proto:
syntax = "proto3"; package ch2; option java_package = "com.example.ch2"; option java_outer_classname = "Animo"; message Cat{ string name = 1; int32 age =2; } message Dog{ string name = 1; int32 age =2; }
測試代碼
@Test public void test2(){ Animo.Cat.Builder cat = Animo.Cat.newBuilder(); cat.setAge(1); cat.setName("kitty"); cat.getName(); cat.getAge(); Animo.Dog.Builder dog = Animo.Dog.newBuilder(); dog.setAge(2); dog.setName("wangcai"); dog.getName(); dog.getAge(); }
合併後的java類實際上是同一個,只是經過不一樣的Builder來獲取。
上面有提過,當咱們需求變動時,可能有不用的字段,那要怎麼處理呢?若是直接刪掉或者註釋掉,有可能其餘版本的應用或其餘應用會用到這個字段,那就會致使嚴重的問題,好比數據損壞、隱私bug等。所以,咱們就好保留這些不用的字段名稱和字段編號。
編譯器會提示咱們錯誤,好比2經過reserved標記爲保留的字段編號,在age使用的時候,就會提示了:
注意,不能在同一個保留語句中混合字段名和字段號。須要分開填寫。
syntax = "proto3"; package ch4; option java_package = "com.example.ch4"; option java_outer_classname = "MyEnum"; message Goods { string name = 1; enum Colors { red = 0; green = 1; blue = 2; } Goods.Colors color = 2; Sizes size = 3; } enum Sizes { option allow_alias = true; X = 0; XL = 1; XXL = 1; }
用枚舉的時候,爲了與proto2兼容,必須有一個0值,且0值必須是第一個元素。
在上面的例子中,定義了兩個枚舉,一個是在Message中,一個是Message外,若是在其餘Message中,須要用Message的名稱加枚舉獲取,我這裏演示的是本身用本身的,因此能夠把Goods.去掉也是能夠的。枚舉的常數,必須在32位整數範圍內,不推薦使用負值。
能夠看到,Sizes有兩個值都是1,這個時候,須要設置allow_alias爲true。咱們看看測試代碼和運行結果:
@Test public void test4() { MyEnum.Goods.Builder goods = MyEnum.Goods.newBuilder(); goods.setColorValue(1); goods.setSizeValue(1); System.out.println(goods.getColor()); System.out.println(goods.getColorValue()); System.out.println(goods.getSize()); System.out.println(goods.getSizeValue()); }
在反序列化期間,沒法識別的enum值將保留在Message中,不一樣的語言有不一樣的處理。好比C和Go,未識別的enum值只是做爲其基礎整數表示形式存儲。在Java中,未識別的enum值會標識沒法識別的值。在任何一種狀況下,若是消息被序列化,未被識別的值仍將與消息一塊兒序列化。
枚舉的保留值寫法跟保留字段同樣。
proto文件:
syntax = "proto3"; package ch5; option java_package = "com.example.ch5"; option java_outer_classname = "MyDefault"; message DefaultValue { string name = 1; int32 age = 2; bytes bt = 3; bool bl = 4; Sizes size = 5; } enum Sizes { X = 0; XL = 1; }
測試代碼:
@Test public void test5() { MyDefault.DefaultValue.Builder builder = MyDefault.DefaultValue.newBuilder(); System.out.println(builder.getName()); System.out.println(builder.getAge()); System.out.println(builder.getBt()); System.out.println(builder.getBl()); System.out.println(builder.getSize()); }
運行結果:
strings:空的字符串
bytes:長度爲0的ByteString
bools:false
numeric:0
enums:默認第一個
用其餘的Message類型做爲字段類型,就好像java的PO中引入了其餘的PO。
syntax = "proto3"; package ch6; option java_package = "com.example.ch6"; option java_outer_classname = "MyOtherMsg"; message MyMsg { MyOther myOther = 1; } message MyOther { string name = 1; }
測試代碼:
@Test public void test6() { MyOtherMsg.MyMsg.Builder builder = MyOtherMsg.MyMsg.newBuilder(); MyOtherMsg.MyOther.Builder myOther =MyOtherMsg.MyOther.newBuilder(); myOther.setName("張三"); builder.setMyOther(myOther); MyOtherMsg.MyOther myOther2 = builder.getMyOther(); System.out.println(myOther2.getName()); }
運行結果:
上面的例子中,若是MyMsg和MyOther不在一個文件中呢,那就須要用import引入。
MyMsg.proto
syntax = "proto3"; package ch7; option java_package = "com.example.ch7"; option java_outer_classname = "MyMsg2"; import "MyOther.proto"; message MyMsg { MyOther myOther = 1; }
MyOther.proto
syntax = "proto3"; package ch7; option java_package = "com.example.ch7"; option java_outer_classname = "MyOther2"; message MyOther { string name = 1; }
測試代碼:
@Test public void test7() { MyMsg2.MyMsg.Builder builder = MyMsg2.MyMsg.newBuilder(); MyOther2.MyOther.Builder myOther =MyOther2.MyOther.newBuilder(); myOther.setName("張三"); builder.setMyOther(myOther); MyOther2.MyOther myOther2 = builder.getMyOther(); System.out.println(myOther2.getName()); }
運行結果以下:
也就是說在Message中定義了Message類型,好比下面在Nest中定義了Inner:
syntax = "proto3"; package ch8; option java_package = "com.example.ch8"; option java_outer_classname = "NestType"; message Nest { message Inner{ string name = 1; } Inner inner = 1; }
測試代碼,有點像內部類
@Test public void test8() { NestType.Nest.Builder builder = NestType.Nest.newBuilder(); NestType.Nest.Inner.Builder inner = NestType.Nest.Inner.newBuilder(); inner.setName("張三"); builder.setInner(inner); NestType.Nest.Inner inner2 = builder.getInner(); System.out.println(inner2.getName()); }
運行結果:
更新message,主要是不能更改任何現有字段的字段編號,以及字段類型的兼容。
好比int3二、uint3二、int6四、uint6四、bool兼容,sint32和sint64兼容,string和bytes(若是bytes是合法的UTF-8)兼容,fixed32與sfixed32兼容,fixed64與sfixed64兼容,enum和int3二、uint3二、int6四、uint64兼容(值不適合會被截斷)。
未知字段是protobuf序列化數據,表示解析器沒法識別的字段。例如,當舊的二進制代碼解析帶有新字段的新二進制代碼發送的數據時,這些新字段將成爲舊二進制代碼中的未知字段。好比咱們Message加了一個新的字段name,那這個name就是未知字段。
Any字段直接用其餘Message類型,相似於泛型,須要導入 google/protobuf/any.proto。
syntax = "proto3"; package ch9; option java_package = "com.example.ch9"; option java_outer_classname = "MyAny"; import "google/protobuf/any.proto"; message Any { string name = 1; google.protobuf.Any any = 2; }
測試代碼,這個Person是第一個例子的。
@Test public void test9() throws InvalidProtocolBufferException { PersonOuterClass.Person.Builder person = PersonOuterClass.Person.newBuilder(); person.setAge(18); person.setName("張三"); MyAny.Any.Builder builder = MyAny.Any.newBuilder(); builder.setAny(Any.pack(person.build())); builder.setName("李四"); PersonOuterClass.Person person2 = builder.getAny().unpack(PersonOuterClass.Person.class); System.out.println(person2.getName()+"-"+person2.getAge()); System.out.println(builder.getName()); }
運行結果以下:
須要鍵值對能夠用map,其中key_type能夠是init或string 類型(排除floate和byte)。key_type不能是枚舉。
syntax = "proto3"; package ch11; option java_package = "com.example.ch11"; option java_outer_classname = "MyMap"; message Map { map<string, string> filedMap = 1; }
測試代碼:
@Test public void test11() throws InvalidProtocolBufferException { MyMap.Map.Builder builder = MyMap.Map.newBuilder(); builder.putFiledMap("name","張三"); builder.putFiledMap("age","18"); System.out.println(builder.getFiledMapMap().get("name")); System.out.println(builder.getFiledMapMap().get("age")); }
運行結果
map不能用repeated修飾。
Proto3支持JSON格式的規範編碼。
proto文件就用第一個例子的。
測試代碼:
@Test public void test12() throws InvalidProtocolBufferException { JsonFormat.Printer printer = JsonFormat.printer(); JsonFormat.Parser parser = JsonFormat.parser(); PersonOuterClass.Person.Builder builder = PersonOuterClass.Person.newBuilder(); builder.setAge(18); builder.setName("lilei"); String jsonStr = printer.print(builder); System.out.println(jsonStr); PersonOuterClass.Person.Builder builder2 = PersonOuterClass.Person.newBuilder(); parser.merge(jsonStr,builder2); PersonOuterClass.Person person = builder2.build(); System.out.println(person); }
運行結果:
對應關係:
若是要在RPC(遠程過程調用)系統中使用Message,能夠在.proto文件中定義 RPC 服務接口,protocol buffer編譯器將根據所選語言生成服務接口代碼和 stubs。若是咱們定義一個RPC服務,入參是SearchRequest返回值是SearchResponse,就能夠這樣在.proto文件中定義它:
service SearchService { rpc Search (SearchRequest) returns (SearchResponse); }
與protocol buffer一塊兒使用的最直接的RPC系統是gRPC:在谷歌開發的與語言和平臺無關的開放源碼RPC系統。gRPC在protocol buffer中工做得很是好,還能夠容許咱們使用特殊的protocol buffer編譯插件,直接從.proto文件中生成 RPC 相關的代碼。
option用於對文件的聲明,google/protobuf/descriptor.proto定義了option的完整列表。
咱們看看上面的proto文件,第一個Person.proto,沒有用option對文件進行聲明,生成的目錄結果以下:
能夠看到的是,java文件在根目錄下,並且java類名是PersonOuterClass。
咱們看看Map那個例子的proto文件,跟Person.proto不一樣是下面兩個option定義:
option java_package = "com.example.ch11"; option java_outer_classname = "MyMap";
生成的目錄結果以下:目錄是由java_package指定的,類名是由java_outer_classname決定的。