Protobuf詳解(.Java檔案)
我們在開發一些RPC呼叫的程式時,通常會涉及到物件的序列化/反序列化的問題,比如一個“Person”物件從Client端通過TCP方式傳送到Server端;因為TCP協議(UDP等這種低階協議)只能傳送位元組流,所以需要應用層將Java物件序列化成位元組流,資料接收端再反序列化成Java物件即可。“序列化”一定會涉及到編碼(encoding,format),目前我們可選擇的編碼方式:
1)使用JSON,將java物件轉換成JSON結構化字串。在web應用、移動開發方面等,基於Http協議下,這是常用的,因為JSON的可讀性較強。效能稍差。
2)基於XML,和JSON一樣,資料在序列化成位元組流之前,都轉換成字串。可讀性強,效能差,異構系統、open api型別的應用中常用。
3)使用JAVA內建的編碼和序列化機制,可移植性強,效能稍差。無法跨平臺(語言)。
4)其他開源的序列化/反序列化框架,比如Apache Avro,Apache Thrift,這兩個框架和Protobuf相比,效能非常接近,而且設計原理如出一轍;其中Avro在大資料儲存(RPC資料交換,本地儲存)時比較常用;Thrift的亮點在於內建了RPC機制,所以在開發一些RPC互動式應用時,Client和Server端的開發與部署都非常簡單。
評價一個序列化框架的優缺點,大概有2個方面:1)結果資料大小,原則上說,序列化後的資料尺寸越小,傳輸效率越高。 2)結構複雜度,這會影響序列化/反序列化的效率,結構越複雜,越耗時。
Protobuf是一個高效能、易擴充套件的序列化框架,它的效能測試有關資料可以參看官方文件。通常在TCP Socket通訊(RPC呼叫)相關的應用中使用;它本身非常簡單,易於開發,而且結合Netty框架可以非常便捷的實現一個RPC應用程式,同時Netty也為Protobuf解決了有關Socket通訊中“半包、粘包”等問題(反序列化時,位元組成幀)。
1、安裝Protobuf
從“https://developers.google.com/protocol-buffers/docs/downloads”下載安裝包,windows下的使用不再贅言;在linux或者mac下,下載tar.gz的壓縮包,解壓後執行:
- $ ./configure
- $ make
- $ make check
- $ make install
此後,可以通過“protoc --version”檢視是否安裝成功了,安裝過程不需要配置環境變數。安裝主要是為了能夠使用命令編譯proto檔案,實際部署環境並不需要。
2、樣例
Protobuf需要一個schema宣告檔案,字尾為“.proto”的文字檔案,內容樣例如下:
Java程式碼- option java_package = "com.test.protobuf";
- option java_outer_classname="PersonProtos";
- 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;
- }
如果你曾經使用過thrift、avro,你會發現它們都需要一個類似的schema檔案,只是結構規則不同罷了。特別備註:protbuf和thrift的宣告檔案相似度極高。
“message”表示,宣告一個“類”,即java中的class。message中可以內嵌message,就像java的內部類一樣。一個message有多個filed,“required string name = 1”則表示:name欄位在序列化、反序列化時為第一個欄位,string型別,“required”表示這個欄位的值是必選;可以看出每個filed都至少有著三個部分組成,其中filed的“位置index”全域性唯一。“optional”表示這個filed是可選的(允許為null)。“repeated”表示這個filed是一個集合(list)。也可以通過[default = ]為一個“optional”的filed指定預設值。
我們可以在一個.proto檔案中宣告多個“message”,不過大部分情況下我們把互相繼承或者依賴的類寫入一個.proto檔案,將那些沒有關聯關係的類分別寫入不同的檔案,這樣便於管理。
我們可以在.proto檔案的頭部宣告一些額外的資訊,比如“java_package”表示當“generate code”時將生成的java程式碼放入指定的package中。“java_outer_classname”表示生成的java類的名稱。
然後執行如下命令,生成JAVA程式碼:
Java程式碼- protoc --java_out=./ Persion.proto
通過“--java_out”指定生成JAVA程式碼儲存的目錄,後面緊跟“.proto”檔案的路徑。此後我們看到生成 了Package和一個PersonProto.java檔案,我們只需要把此java檔案複製到專案中即可。
3、JAVA例項
1)pom.xml
Java程式碼- <dependency>
- <groupId>com.google.protobuf</groupId>
- <artifactId>protobuf-java</artifactId>
- <version>2.6.1</version>
- </dependency>
2)測試:
Java程式碼- PersonProtos.Person.Builder personBuilder = PersonProtos.Person.newBuilder();
- personBuilder.setEmail("[email protected]");
- personBuilder.setId(1000);
- PersonProtos.Person.PhoneNumber.Builder phone = PersonProtos.Person.PhoneNumber.newBuilder();
- phone.setNumber("18610000000");
- personBuilder.setName("張三");
- personBuilder.addPhones(phone);
- PersonProtos.Person person = personBuilder.build();
獲得到person例項後,我們可以通過如下方式,將person物件序列化、反序列化。
Java程式碼- //第一種方式
- //序列化
- byte[] data = person.toByteArray();//獲取位元組陣列,適用於SOCKET或者儲存在磁碟。
- //反序列化
- PersonProtos.Person result = PersonProtos.Person.parseFrom(data);
- System.out.println(result.getEmail());
這種方式,適用於很多場景,Protobuf會根據自己的encoding方式,將JAVA物件序列化成位元組陣列。同時Protobuf也可以從位元組陣列中重新decoding,得到Java新的例項。
Java程式碼- //第二種序列化:粘包,將一個或者多個protobuf物件位元組寫入stream。
- ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
- //生成一個由:[位元組長度][位元組資料]組成的package。特別適合RPC場景
- person.writeDelimitedTo(byteArrayOutputStream);
- //反序列化,從steam中讀取一個或者多個protobuf位元組物件
- ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
- result = PersonProtos.Person.parseDelimitedFrom(byteArrayInputStream);
- System.out.println(result.getEmail());
第二種方式,是RPC呼叫中、Socket傳輸時適用,在序列化的位元組陣列之前,新增一個varint32的數字表示位元組陣列的長度;那麼在反序列化時,可以通過先讀取varint,然後再依次讀取此長度的位元組;這種方式有效的解決了socket傳輸時如何“拆包”“封包”的問題。在Netty中,適用了同樣的技巧。
Java程式碼- //第三種序列化,寫入檔案或者Socket
- FileOutputStream fileOutputStream = new FileOutputStream(new File("/test.dt"));
- person.writeTo(fileOutputStream);
- fileOutputStream.close();
- FileInputStream fileInputStream = new FileInputStream(new File("/test.dt"));
- result = PersonProtos.Person.parseFrom(fileInputStream);
- System.out.println(result);
第三種方式,比較少用。但是比較通用,意思為將序列化的位元組陣列寫入到OutputStream中,具體的拆包工作,交給了高層框架。
4、protobuf入門介紹
以上述Person.proto檔案為例:
Java程式碼- message Person {
- required string name = 1;
- required int32 id = 2;
- optional string email = 3;
- }
聲明瞭三個filed,每個filed都“規則”、“型別”、“欄位名稱”和一個“唯一的數字tag”。
1)其中“規則”可以為如下幾個值:
“required”:表示此欄位值必填,一個結構良好的message至少有一個flied為“required”。
“optional”:表示此欄位值為可選的。對於此型別的欄位,可以通過default來指定預設值,這是一個良好的設計習慣。
Java程式碼- optional int32 page = 3 [default = 10];
如果沒有指定預設值,在encoding時protobuf將會用一個特殊的預設值來替代。對於string,預設值為空,bool型別預設為false,數字型別預設位0,對於enum則預設值為列舉列表的第一個值。
“repeated”:表示這個欄位的值可以允許被重複多次,如果轉換成JAVA程式碼,此filed資料結構為list,有序的。可以在“repeated”型別的filed後使用“packed”--壓縮,提高資料傳輸的效率。
Java程式碼- repeated int32 numbers = 4 [packed=true];
特別需要注意:當你指定一個filed位required時,需要慎重考慮這個filed是否永遠都是“必須的”。將一個required調整為optional,需要同時重新部署資料通訊的Client和Server端,否則將會對解析帶來問題。
2)可以在一個.proto檔案中,同時宣告多個message,這樣是允許的。
3)為message或者filed添加註釋,風格和JAVA一樣:
Java程式碼- optional int32 page = 3;// Which page number do we want?
4)資料型別與JAVA對應關係:
protobuf | java |
double | double |
float | float |
int32 | int |
int64 | long |
bool | boolean |
string | String |
bytes | ByteString |
其中“ByteString”是Protobuf自定義的JAVA API。
5)列舉:和JAVA中Enum API一致,如果開發者希望某個filed的值只能在一些限定的列表中,可以將次filed宣告為enum型別。Protobuf中,enum型別的每個值是一個int32的數字,不像JAVA中那樣enum可以定義的非常複雜。如果enum中有些值是相同的,可以將“allow_alias”設定為true。
Java程式碼- message Person {
- required Type type = 1;
- enum Type {
- option allow_alias = true;
- TEACHER = 0;
- STUDENT = 1;
- OTHER = 1;//the same as STUDENT
- }
- }
6)import:如果當前.proto檔案中引用了其他proto檔案的message型別,那麼可以在此檔案的開頭宣告import。
Java程式碼- import "other_protos.proto";
不過這會引入一個小小的麻煩,如果你的“other_protos.proto”檔案變更了目錄,需要連帶修改其他檔案。
7)嵌入message:類似於java的內部類,即在message中,嵌入其他message。如Person.proto例子中的PhoneNumber。
8)更新message型別:如果一個現有的message型別無法滿足當前的需要,比如你需要新增一個filed,但是仍然希望使用生成的舊程式碼來解析。
(1)不要修改現有fileds的數字tag,即欄位的index數字。
(2)新增欄位必須為optional或者repeated型別,同時還要為它們設定“default”值,這意味著“old”程式碼序列化的messages能夠被“new”程式碼解析。“new”程式碼生成的資料也能被“old”程式碼解析,對於“old”程式碼而言,那些沒有被宣告的filed將會在解析式忽略。
(3)非“required”filed可以被刪除,但是它的“數字tag”不能被其他欄位重用。
(4)int32、uint32、int64、uint64、bool,是互相相容的,它們可以從一個型別修改成另外一個,而不會對程式帶來錯誤。參見原始碼WireFormat.FiledType
(5)sint32和sint64是相容的,但和其他數字型別是不相容的。
(6)string和bytes是相容的,只要為UTF-8編碼的。注意protobuf中string預設是UTF-8編碼的。
(7)optional與repeated是相容的。如果輸入的資料格式是repeated,但是client希望接受的資料是optional,對於原生型別,那麼client將會使用repeated的最後一個值,對於message型別,client將會merge這些輸入的資料。
(8)修改“default”值通常不會有任何問題,只要保證這個預設值不會被真正的使用。
9)Map結構:
Java程式碼- map<key_type, value_type> map = 3;
其中key_type可以為任何“整形”或者string型別,value_type可以為任意型別,只要JAVA API能夠支援。map型別不能被“repeated”、“optional”或者“required”修飾,傳輸過程中無法確保map中資料的順序,
對於文字格式,map是按照key排序。
10)如下為一些有用的選項:
(1)java_package:在.proto檔案的頂部設定,指定生成JAVA檔案時類所在的package。
Java程式碼- option java_package = "com.example.foo";
(2)java_outer_classname:在.proto檔案的頂部設定,指定生成JAVA檔案時類的名字。一個.proto檔案只會生成一個JAVA類。
Java程式碼- option java_outer_classname = "FooProtos";
(3)packed:對於repeated型別有效,指定輸入的資料是否“壓縮”。
5、protobuf序列化原理:
其實protobuf的序列化原理並不是什麼高超的“絕技”:如果你曾經瞭解過thrift、avro,或者從事過socket通訊,那麼你對protobuf的序列化方式並不感到驚奇;如下為protobuf的序列化format:
Java程式碼- [serializedSize]{[int32(tag,type)][value]...}
對於一個message,序列化時首先就算這個message所有filed序列化需要佔用的位元組長度,計算這個長度是非常簡單的,因為protobuf中每種型別的filed所佔用的位元組數是已知的(bytes、string除外),只需要累加即可。這個長度就是serializedSize,32為integer,在protobuf的某些序列化方式中可能使用varint32(一個壓縮的、根據數字區間,使用不同位元組長度的int);此後是filed列表輸出,每個filed輸出包含int32(tag,type)和value的位元組陣列,從上文我們知道每個filed都有一個唯一的數字tag表示它的index位置,type為欄位的型別,tag和type分別佔用一個int的高位、低位位元組;如果filed為string、bytes型別,還會在value之前額外的補充新增一個varint32型別的數字,表示string、bytes的位元組長度。
那麼在反序列化的時候,首先讀取一個32為的int表示serializedSize,然後讀取serializedSize個位元組儲存在一個bytebuffer中,即讀取一個完整的package。然後讀取一個int32數字,從這個數字中解析出tag和type,如果type為string、bytes,然後補充讀取一個varint32就知道了string的位元組長度了,此後根據type或者位元組長度,讀取後續的位元組陣列並轉換成java type。重複上述操作,直到整個package解析完畢。
protobuf的這種序列化format,極大的介紹了輸入、輸出的資料大小,而且複雜度非常低,從而效能較高。
6、protobuf與Netty程式設計:
1)Netty Server端樣例
Java程式碼- public class ProtobufNettyServerTestMain {
- public static void main(String[] args) {
- //bossGroup : NIO selector threadPool
- EventLoopGroup bossGroup = new NioEventLoopGroup();
- //workerGroup : socket data read-write worker threadPool
- EventLoopGroup workerGroup = new NioEventLoopGroup();
- try {
- ServerBootstrap bootstrap = new ServerBootstrap();
- bootstrap.group(bossGroup,workerGroup)
- .channel(NioServerSocketChannel.class)
- .childHandler(new ChannelInitializer<SocketChannel>() {
- @Override
- protected void initChannel(SocketChannel ch) throws Exception {
- ch.pipeline().addLast(new ProtobufVarint32FrameDecoder())
- .addLast(new ProtobufDecoder(PersonProtos.Person.getDefaultInstance()))
- .addLast(new ProtobufVarint32LengthFieldPrepender())
- .addLast(new ProtobufEncoder())
- .addLast(new ProtobufServerHandler());//自定義handler
- }
- }).childOption(ChannelOption.TCP_NODELAY,true);
- System.out.println("begin");
- //bind到本地的18080埠
- ChannelFuture future = bootstrap.bind(18080).sync();
- //阻塞,直到channel.close
- future.channel().closeFuture().sync();
- System.out.println("end");
- } catch (Exception e) {
- e.printStackTrace();
- } finally {
- //輔助執行緒優雅退出
- workerGroup.shutdownGracefully();
- bossGroup.shutdownGracefully();
- }
- }
- }
備註:channel內部維護一個pipeline,類似一個filter連結串列一樣,所有的socket讀寫都會經過,對於write操作(outbound)會從pipeline列表的last-->first方向依次呼叫Encoder處理器;對於read操作(inbound)會從first-->last依次呼叫Decoder處理器。此外Encoder處理對於read操作不起效,Decoder處理器對write操作不起效,原理 稍後在Netty相關章節介紹。
ProtobufEncoder:非常簡單,內部直接使用了message.toByteArray()將位元組資料放入bytebuf中輸出(out中,交由下一個encoder處理)。
ProtobufVarint32LengthFieldPrepender:因為ProtobufEncoder只是將message的各個filed按照規則輸出了,並沒有serializedSize,所以socket無法判定package(封包)。這個Encoder的作用就是在ProtobufEncoder生成的位元組陣列前,prepender一個varint32數字,表示serializedSize。
ProtobufVarint32FrameDecoder:這個decoder和Prepender做的工作正好對應,作用就是“成幀”,根據seriaziedSize讀取足額的位元組陣列--一個完整的package。
ProtobufDecoder:和ProtobufEncoder對應,這個Decoder需要指定一個預設的instance,decoder將會解析byteArray,並根據format規則為此instance中的各個filed賦值。
2)ProtobufServerHandler.java
傳送Protobuf資料和接收client傳送的資料。一個自定義的處理器,通常我們的業務會在這裡處理。
Java程式碼- public class ProtobufServerHandler extends ChannelInboundHandlerAdapter {
- @Override
- public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
- PersonProtos.Person person = (PersonProtos.Person)msg;
- //經過pipeline的各個decoder,到此Person型別已經可以斷定
- System.out.println(person.getEmail());
-
相關推薦
Protobuf詳解(.Java檔案)
我們在開發一些RPC呼叫的程式時,通常會涉及到物件的序列化/反序列化的問題,比如一個“Person”物件從Client端通過TCP方式傳送到Server端;因為TCP協議(UDP等這種低階協議)只能傳送位元組流,所以需要應用層將Java物件序列化成位元組流,資料接收端再反
詳解java定時任務
導致 println 正常 延遲執行 first 指定 線程終止 ont 打印 在我們編程過程中如果需要執行一些簡單的定時任務,無須做復雜的控制,我們可以考慮使用JDK中的Timer定時任務來實現。下面LZ就其原理、實例以及Timer缺陷三個方面來解析java Timer定
詳解Java類的生命周期
字段 view 數據類型 分配內存 lar ati final 並不是 編譯 引言 最近有位細心的朋友在閱讀筆者的文章時,對Java類的生命周期問題有一些疑惑,筆者打開百度搜了一下相關的問題,看到網上的資料很少有把這個問題講明白的,主要是因為目前國內Java
caffe protobuf詳解
方法 type source protobuf 輸出 filter out cat hdf 1.數據層 layer { name: "cifar" type: "Data" top: "data" #一般用bottom表示輸入,top表示輸出,多個t
windows命令行中java和javac、javap使用詳解(java編譯命令)
路徑 point 目錄 pan static article 字節碼 區別 string 如題,首先我們在桌面,開始->運行->鍵入cmd 回車,進入windows命令行。進入如圖所示的畫面: 可知,當前默認目錄為C盤Users文件夾下的Administr
詳解java中的數據結構
span 通過 組成 ret hashcode p s 函數 arr 均衡 線性表,鏈表,哈希表是常用的數據結構,在進行Java開發時,JDK已經為我們提供了一系列相應的類來實現基本的數據結構。這些類均在java.util包中。本文試圖通過簡單的描述,向讀者闡述各個類的
詳解Java的自動裝箱與拆箱(Autoboxing and unboxing)
初始 BE 運算 null 異常 內存 判斷 運行 double 一、什麽是自動裝箱拆箱 很簡單,下面兩句代碼就可以看到裝箱和拆箱過程 1 //自動裝箱 2 Integer total = 99; 3 4 //自定拆箱 5 int totalprim = total;
詳解Java的Spring框架中的註解的用法
控制 extends 進行 -i 場景 1.7 遞歸 ins 規範 轉載:http://www.jb51.net/article/75460.htm 1. 使用Spring註解來註入屬性 1.1. 使用註解以前我們是怎樣註入屬性的 類的實現: class UserMa
幹貨——詳解Java中的關鍵字
java虛擬機 color bsp cfi 為什麽 max main spa 不能 在平時編碼中,我們可能只註意了這些static,final,volatile等關鍵字的使用,忽略了他們的細節,更深層次的意義。 本文總結了Java中所有常見的關鍵字以及一些例子。
Java 基礎之詳解 Java 反射機制
一行代碼 strac classname for 內部 系統資源 用戶 管理 ann 一、什麽是 Java 的反射機制? ??反射(Reflection)是Java的高級特性之一,是框架實現的基礎,定義:JAVA反射機制是在運行狀態中,對於任意一個類,都能夠知道這個類的所有
詳解java中的byte類型
font 資料 結果 可能 詳解 小程序 工作 定義 值範圍 Java也提供了一個byte數據類型,並且是基本類型。java byte是做為最小的數字來處理的,因此它的值域被定義為-128~127,也就是signed byte。下面這篇文章主要給大家介紹了關於java中by
數據結構 - 單源最短路徑之迪傑斯特拉(Dijkstra)算法詳解(Java)
previous 代碼 map class matrix () count 就是 可能 給出一個圖,求某個端點(goal)到其余端點或者某個端點的最短路徑,最容易想到的求法是利用DFS,假設求起點到某個端點走過的平均路徑為n條,每個端點的平均鄰接端點為m,那求出這個最短
詳解 Java 中的三種代理模式
繼承 jvm 保存 3.2 指令集 throwable eth args 代理類 代理模式 代理(Proxy)是一種設計模式,提供了對目標對象另外的訪問方式;即通過代理對象訪問目標對象.這樣做的好處是:可以在目標對象實現的基礎上,增強額外的功能操作,即擴展目標
詳解Java中的時區類TimeZone的用法
void system類 深入 pri comment 相對 系統 就會 lean 一、TimeZone 簡介 TimeZone 表示時區偏移量,也可以計算夏令時。 在操作 Date, Calendar等表示日期/時間的對象時,經常會用到TimeZone;因為不同的時區,
面向介面程式設計詳解-Java篇
相信看到這篇文字的人已經不需要了解什麼是介面了,我就不再過多的做介紹了,直接步入正題,介面測試如何編寫。那麼在這一篇裡,我們用一個例子,讓各位對這個重要的程式設計思想有個直觀的印象。為充分考慮到初學者,所以這個例子非常簡單,望各位高手見諒。 為了擺脫新手的概念,我這裡也儘量不用main
詳解Java中的Object.getClass()方法
詳解Java中的Object.getClass()方法 詳解Object.getClass()方法,這個方法的返回值是Class型別,Class c = obj.getClass(); 通過物件c,我們可以獲取該物件的所有成員方法,每個成員方法都是一個Method物件;我們也可以獲取該物件的
詳解smali檔案
詳解smali檔案 上面我們介紹了Dalvik的相關指令,下面我們則來認識一下smali檔案.儘管我們使用java來寫Android應用,但是Dalvik並不直接載入.class檔案,而是通過dx工具將.class檔案優化成.dex檔案,然後交由Dalvik載入.這樣說來,我們無法通
舉例詳解java例項變數,靜態變數,區域性變數
public class Variable { public int m,n;//對子類可見的例項變數 private double k;//只對本類可見的例項變數,一般情況下,設為私有,通過使用訪問修飾符來被子類使用。 public static String P;//靜態變數(
RoundingMode 幾個引數詳解 java.math.RoundingMode 幾個引數詳解
第一版 java.math.RoundingMode 幾個引數詳解 java.math.RoundingMode裡面有幾個引數搞得我有點暈,現以個人理解對其一一進行總結: 為了能更好理解,我們可以畫一個XY軸 RoundingMode
詳解Java解析XML的四種方法(轉載)
出處:http://developer.51cto.com/art/200903/117512.htm XML現在已經成為一種通用的資料交換格式,它的平臺無關性,語言無關性,系統無關性,給資料整合與互動帶來了極大的方便。對於XML本身的語法知識與技術細節,需要閱讀相關的技術文獻,這裡