Google Protobuf 編解碼


Protobuf 全稱:Google Protocol Buffers,由谷歌開源而來,經谷歌內部測試使用。它將數據結構以 .proto 文件進行描述,通過代碼生成工具可以生成對應數據結構的 POJO 對象和 Protobuf 相關的方法和屬性。

一、 Protocol 的特點


【1】在谷歌內部長期使用,產品成熟度高;
【2】高效的編解碼性能,編碼后的消息更小,有利於存儲和傳輸;
【3】語言無關、平台無關、擴展性好
【4】官方支持 Java、C++ 、C#、 Python 、Go 和 Dart

Protobuf 使用二進制編碼,在空間和性能上相對於 XML具有很大的優勢。盡量 XML的可讀性和可擴展性非常好,也非常適合描述數據結構,但是 XML 解析的時間開銷和 XML為了可讀性而犧牲的空間開銷都非常大,因此不適合做高性能的通信協議。

  Protobuf 的數據描述文件和代碼生成機制(跨語言的編解碼框架,都具有此功能),優點如下:
    ■  文本化的數據結構描述語言,可以實現語言和平台無關,特別適合異構系統間的集成;
    ■  通過標識字段的順序,可以實現協議的前向兼容;
    ■  自動代碼生成,不需要手工編寫同樣數據結構的 C++ 和 Java 版本;
    ■  方便后續的管理和維護。相當於代碼,結構化的文檔更容易管理和維護。

Protobuf 的編解碼性能遠遠高於 JSON<Serializable<hession2<hession1<XStream<hession2壓縮(性能有高到底)等序列化框架的序列化和反序列化,這也是很多 RPC 框架選用 protobuf 做編解碼框架的原因。

二、Protobuf 開發環境搭建


【1】首先下載 Protobuf 的最新 Windown 版本:網站地址如下:https://github.com/protocolbuffers/protobuf/releases/tag/v3.9.1
 
  下載后對其解壓:進入包含 protoc.exe 的文件目錄,配置其環境變量;protoc.exe 工具主要根據 .proto 文件生成代碼。

官網對 java 編寫 .proto 文件,詳細說明地址:https://developers.google.cn/protocol-buffers/docs/javatutorial

  下面我們定義一個 person.proto 數據文件。如下: 注釋寫在#號后,實際不能這么操作。此處為方便理解:

#類似於c++或java。檢查一下文件的每一部分,看看它的作用。
syntax = "proto2";
#以包聲明開始,這有助於防止不同項目之間的命名沖突
package tutorial;
#在java中,包名用作java包,除非您已經顯式地指定了java_包,如我們這里所述。
#即使您確實提供了一個java_包,您也應該定義一個普通包,以避免在協議緩沖區名稱空間和非java語言中發生名稱沖突。
#如果不提供此屬性,以package 為准
#java_package指定生成的類的java包名。
#如果您沒有顯式地指定它,那么它只匹配包聲明給出的包名,但是這些名稱通常不適合Java包名(因為它們通常不以域名開頭)
option java_package = "com.example.tutorial";
#java_outer_class name選項定義類名,該類名應包含此文件中的所有類。
#如果沒有顯式地給出java_outer_類名,則將通過將文件名轉換為camel case來生成它。
#例如,“my_proto.proto”在默認情況下將使用“myProto”作為外部類名。利用駝峰命名法。
option java_outer_classname = "AddressBookProtos";

#開始定義消息,相當於內部類 Person
message Person {
  # required 表示必須字段,1是序號不是賦值的意思,表示唯一的標記。
  # 建議不要使用 required 而使用optional 因為當后期將 required 修改為 optional 會有問題。
  required string name = 1;
  required int32 id = 2;
  optional string email = 3;
}

【2】通過 protoc.exe 命令行生成 Java 代碼,命令如下:[ --java_out=生成 *.java 文件的存放路徑,我所在的目錄正是存放person.proto 文件的目錄 ]沒有任何錯誤就說明生成成功。

E:\learnWorkspacesDesign\netty_learn\src\protobuf>protoc.exe --java_out=..\main\java person.proto

【3】查看生成的目標文件:或者在外面生成好,拷貝進來也行。建議不要對生成的文件做任何修改。我們發現代碼編譯出錯,原因是因為少了 protobuf 的 jar 包:
 

  引入 protobuf-java 相關的 jar 包,如下:

1 <dependency>
2     <groupId>com.google.protobuf</groupId>
3     <artifactId>protobuf-java</artifactId>
4     <version>3.9.1</version>
5 </dependency>

到此為止,Protobuf 開發環境已經搭建完畢,接下來進行示例展示。

三、Protobuf 編解碼開發


Protobuf 的類庫使用比較簡單,下面通過對 AddressBookProtos 編解碼來介紹 Protobuf 的使用:由於 Protobuf 支持復雜 POJO 對象編解碼,所以代碼都是通過工具自動生成,相比於傳統的 POJO 對象的賦值操作,其使用略微復雜一些。Protobuf 的編解碼接口非常簡單和實用,但是功能和性能卻非常強大,這也是它流行的一個重要原因。

 1 public class TestAddressBookProtos {
 2     public static void main(String[] args) throws InvalidProtocolBufferException {
 3         AddressBookProtos.Person person = createSubscribeReq();
 4         /*
 5          * After decode:name: "ZhengZhaoXiang"
 6          * id: 1
 7          * email: "1179278531@qq.com"
 8          */
 9         System.out.printf("Before encode :"+person.toString());
10         AddressBookProtos.Person personObj = decode(encode(person));
11         /*
12          * After decode:name: "ZhengZhaoXiang"
13          * id: 1
14          * email: "1179278531@qq.com"
15          */
16         System.out.printf("After decode:"+person.toString());
17         //輸出: Assert equal:true
18         System.out.printf("Assert equal:"+person.equals(personObj));
19     }
20 
21     //編碼 通過調用 AddressBookProtos.Person 實例的 toByteArray 即可將 Person 編碼為 byte 數組。
22     private static byte[] encode(AddressBookProtos.Person person){
23         return person.toByteArray();
24     }
25 
26     //解碼  還可以解碼流數據  parseFrom(InputStream i);
27     private static AddressBookProtos.Person decode(byte[] body) throws InvalidProtocolBufferException {
28         return AddressBookProtos.Person.parseFrom(body);
29     }
30 
31     //創建一個 person 對象
32     private static AddressBookProtos.Person createSubscribeReq(){
33         // 通過 AddressBookProtos.Person 的 newBuilder() 靜態方法創建 Builder 實例
34         // 通過 Builder 構建器對 Person 的屬性進行設置,對於集合類型,通過addAllXXX()方法將值設置到屬性中。
35         return AddressBookProtos.Person.newBuilder()
36                 .setId(1).setName("ZhengZhaoXiang").setEmail("1179278531@qq.com").build();
37     }
38 }

四、Netty 的 Protobuf 服務端開發


【1】標准的服務端:主要區別在於 childHandler 方法中的 PersonChannelInitializer 類的內容。

 1 public class PersonServer {
 2     public static void main(String[] args) throws Exception{
 3         EventLoopGroup bossGroup = new NioEventLoopGroup();
 4         EventLoopGroup workerGroup = new NioEventLoopGroup();
 5         try {
 6             ServerBootstrap bootstrap = new ServerBootstrap();
 7             bootstrap.group(bossGroup,workerGroup).channel(NioServerSocketChannel.class)
 8                     .handler(new LoggingHandler(LogLevel.INFO))
 9                     //主要查看 PersonChannelInitializer 內容
10                     .childHandler(new PersonChannelInitializer());
11             ChannelFuture future = bootstrap.bind(8899).sync();
12             future.channel().closeFuture().sync();
13         }finally {
14             bossGroup.shutdownGracefully();
15             workerGroup.shutdownGracefully();
16         }
17     }
18 }

【2】PersonChannelInitializer 內容展示:重點關注自定義 handler(PersonHandler)

 1 public class PersonChannelInitializer extends ChannelInitializer{
 2     @Override
 3     protected void initChannel(Channel channel) throws Exception {
 4         ChannelPipeline pipeline = channel.pipeline();
 5         //主要用於半包處理
 6         pipeline.addLast(new ProtobufVarint32FrameDecoder());
 7         //解碼器,參數 com.google.protobuf.MessageLite 實際上是告訴 ProtobufDecoder 解碼的目標類
 8         pipeline.addLast(new ProtobufDecoder(AddressBookProtos.Person.getDefaultInstance()));
 9         pipeline.addLast(new ProtobufVarint32LengthFieldPrepender());
10         pipeline.addLast(new StringEncoder());
11         //自定義handler
12         pipeline.addLast(new PersonHandler());
13     }
14 }

【3】自定義 PersonHandler 的內容如下:由於 ProtobufDecoder  已經對消息進行了自動解碼,因此接收到的 Person 消息可以直接使用。對用戶進行校驗,校驗通過后構造應答消息返回給客戶端,由於使用了 StringEncoder 因此不需要手工編碼。

 1 public class PersonHandler extends SimpleChannelInboundHandler {
 2 
 3     @Override
 4     protected void channelRead0(ChannelHandlerContext channelHandlerContext, Object msg) throws Exception {
 5         AddressBookProtos.Person person = (AddressBookProtos.Person)msg;
 6         System.out.printf(String.valueOf(channelHandlerContext.channel().remoteAddress()));
 7         System.out.printf("服務端收到的消息    "+person);
 8         channelHandlerContext.writeAndFlush("from client"+ LocalDateTime.now());
 9     }
10 
11     @Override
12     public void channelActive(ChannelHandlerContext channelHandlerContext){
13         channelHandlerContext.writeAndFlush("來着服務端的問候:Active"+"\r\n");
14     }
15 
16     @Override
17     public void exceptionCaught(ChannelHandlerContext channelHandlerContext,Throwable e){
18         e.printStackTrace();
19         channelHandlerContext.close();
20     }
21 }

五、Netty 的 Protobuf 客戶端開發


【1】客戶端:主要區別在於 childHandler 方法中的 PersonClientInitializer 類的內容。

 1 public class PersonClient {
 2     public static void main(String[] args) throws Exception{
 3         EventLoopGroup workerGroup = new NioEventLoopGroup();
 4         try {
 5             Bootstrap bootstrap = new Bootstrap();
 6             bootstrap.group(workerGroup).channel(NioSocketChannel.class)
 7                     .handler(new PersonClientInitializer());
 8             ChannelFuture future = bootstrap.connect("127.0.0.1",8899).sync();
 9             future.channel().closeFuture().sync();
10         }finally {
11             workerGroup.shutdownGracefully();
12         }
13     }
14 }

【2】PersonClientInitializer 內容展示:重點關注自定義 handler(PersonClientHandler)

 1 public class PersonClientInitializer extends ChannelInitializer{
 2     @Override
 3     protected void initChannel(Channel channel) throws Exception {
 4         ChannelPipeline pipeline = channel.pipeline();
 5         pipeline.addLast(new ProtobufVarint32FrameDecoder());
 6         pipeline.addLast(new StringDecoder());
 7         pipeline.addLast(new ProtobufVarint32LengthFieldPrepender());
 8         pipeline.addLast(new ProtobufEncoder());
 9         pipeline.addLast(new PersonClientHandler());
10     }
11 }

【3】自定義 PersonClientHandler 的內容如下:

 1 public class PersonClientHandler extends SimpleChannelInboundHandler {
 2 
 3     @Override
 4     protected void channelRead0(ChannelHandlerContext channelHandlerContext, Object msg) throws Exception {
 5         System.out.printf(String.valueOf(channelHandlerContext.channel().remoteAddress()));
 6         System.out.printf("客戶端收到的消息:   "+"\r\n" + msg);
 7     }
 8 
 9     @Override
10     public void channelActive(ChannelHandlerContext channelHandlerContext){
11         AddressBookProtos.Person person = AddressBookProtos.Person.newBuilder().setId(1)
12                 .setName("zhengzhaoxiang")
13                 .setEmail("1179278531@qq.com").build();
14         channelHandlerContext.channel().writeAndFlush(person);
15     }
16 
17     @Override
18     public void exceptionCaught(ChannelHandlerContext channelHandlerContext,Throwable e){
19         e.printStackTrace();
20         channelHandlerContext.close();
21     }
22 }

六、測試


  啟動服務端——>啟動客戶端,運行結果如下:
【1】服務端結果展示:
 

【2】客戶端結果展示:
 

七、問題


當 .proto 中存在多個 message 時,在解碼 ProtobufDecode(目標對象)中,添加的目標對象不唯一,會根據情況進行變化的問題及解決方案。

【1】.proto 文件內容如下:包含多個 message 對象。oneof 關鍵字表示:多個可選項,但允許選擇一個。設置的新值會替換掉舊值。

 1 syntax = "proto2";
 2 
 3 package tutorial;
 4 
 5 option java_package = "com.protobuf";
 6 option java_outer_classname = "AddressBookProtos";
 7 
 8 message myMessage {
 9   enum data {
10     personType = 1;
11     dogType = 2;
12     pigType = 3;
13   }
14   
15   required string type = 1;
16   oneof zoo {
17     Person person = 2;
18     Dog dog = 3;
19     Pig pig =4;
20   }
21 }
22 
23 message Person {
24   optional string name = 1;
25   optional int32 id = 2;
26   optional string email = 3;
27 }
28 
29 message Dog {
30   optional string name = 1;
31 }
32 
33 message Pig {
34   optional string name = 1;
35   optional int32 price = 2;
36 }

【2】編輯碼出的問題,便可以修改為最外層的 myMessage 對象,服務端解碼設置如下:

pipeline.addLast(new ProtobufDecoder(AddressBookProtos.myMessage.getDefaultInstance()));

【3】客戶端發送發送消息,內容如下:需要什么對象,就往 oneof 中傳入目標對象即可。

 1 @Override
 2 public void channelActive(ChannelHandlerContext channelHandlerContext){
 3     int random = new Random().nextInt(3);
 4     AddressBookProtos.myMessage message = null;
 5     if(random == AddressBookProtos.myMessage.data.personType_VALUE){
 6         message = AddressBookProtos.myMessage.newBuilder()
 7                 .setType("1").setPerson(AddressBookProtos.Person.newBuilder()
 8                 .setId(1).setName("zheng").setEmail("117278531@qq.com").build()).build();
 9     }else if(random == AddressBookProtos.myMessage.data.dogType_VALUE){
10         message = AddressBookProtos.myMessage.newBuilder()
11                 .setType("2").setDog(AddressBookProtos.Dog.newBuilder()
12                         .setName("一條狗").build()).build();
13     }else{
14         message = AddressBookProtos.myMessage.newBuilder()
15                 .setType("3").setPig(AddressBookProtos.Pig.newBuilder()
16                         .setName("一只豬").setPrice(20).build()).build();
17     }
18     channelHandlerContext.channel().writeAndFlush(message);
19 }

【4】服務端接受客戶端的消息,根據 type 的值判斷需要解析的數據信息,具體內容如下:

 1 @Override
 2 protected void channelRead0(ChannelHandlerContext channelHandlerContext, Object msg) throws Exception {
 3     AddressBookProtos.myMessage message = (AddressBookProtos.myMessage)msg;
 4     if(Integer.valueOf(message.getType()) == (AddressBookProtos.myMessage.data.personType_VALUE)){
 5         System.out.printf("服務端收到的消息    "+message.getPerson().toString());
 6     }else if(Integer.valueOf(message.getType()) == (AddressBookProtos.myMessage.data.dogType_VALUE)){
 7         System.out.printf("服務端收到的消息    "+message.getDog().getName());
 8     }else{
 9         System.out.printf("服務端收到的消息    "+message.getPig().getName()+"\r\n"+message.getPig().getPrice());
10     }
11 }

【5】不斷重啟客戶端,會根據隨機數得到不同的結果,如下:

 1 //第一次輸入結果展示:
 2 /*服務端收到的消息    name: "zheng"
 3 id: 1
 4 email: "117278531@qq.com"*/
 5 
 6 //第三次輸入結果展示:
 7 /*服務端收到的消息    一條狗*/
 8 
 9 //第四次輸入結果展示:
10 /*服務端收到的消息    一只豬
11 20*/


免責聲明!

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



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