1. 程式人生 > >使用Protocol Buffers入門四步驟

使用Protocol Buffers入門四步驟

Protocol Buffers(簡稱protobuf)是谷歌的一項技術,用於將結構化的資料序列化、反序列化,經常用於網路傳輸。

protobuf是谷歌的Protocol Buffers的簡稱,用於結構化資料和位元組碼之間互相轉換(序列化、反序列化,即實現從結構體轉換為位元組流(編碼,向LIS傳送訊息時使用)以及從位元組流轉換為結構體(解碼,從LIS接收訊息時使用)的功能。),一般應用於網路傳輸,可支援多種程式語言。

這貨實際上類似於XML生成和解析,但protobuf的效率高於XML,不過protobuf生成的是位元組碼,可讀性比XML差。類似的還有json、Java的Serializable等。

protobuf支援各種語言。本文以Java為例,簡單介紹protobuf如何使用。其他語言使用方法類似。

首先需要下載:

解壓後有兩個檔案:protobuf-java-2.5.0.jar和protoc.exe。

protobuf-java-2.5.0.jar即protobuf所需要的jar包,如果用maven的話可以無視這個檔案;

protoc.exe是protobuf程式碼生成工具。

第一步:定義資料結構

首先要定義protobuf的資料結構,這裡要寫一個.proto檔案。這個檔案有點類似於定義一個類。例如定義一個Person,儲存檔案PersonMsg.proto(注意檔名和裡面的message名不要一樣)。

  1. message Person {  
  2.     // ID(必需)  
  3.     required int32 id = 1;  
  4.     // 姓名(必需)  
  5.     required string name = 2;  
  6.     // email(可選)  
  7.     optional string email = 3;  
  8.     // 朋友(集合)  
  9.     repeated string friends = 4;  
  10. }  

上面的1、2、3、4是unique numbered tag,是一個唯一標識。

上面的例子中定義了一個非常簡單的資料結構,當然還可以定義更復雜的結構,這裡不再討論,具體可以看官方文件。

第二步:protoc.exe生成Java程式碼

使用檔案protoc.exe,cmd命令列執行:

protoc.exe --java_out=E:\Java PersonMsg.proto

輸入檔案是PersonMsg.proto,也就是定義資料結構的檔案;輸出資料夾是E:\java,將java檔案生成在E:\java中。執行命令成功後會生成PersonMsg.java:


在Eclipse中建立一個專案,將java檔案拷貝到專案中。專案中需要引入protobuf-java-2.5.0.jar包。如果是maven專案則加入:

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

第三步:序列化
第四步:反序列化

一般來說,序列化和反序列化是分開的。例如網路傳輸,由一方將資料序列化後傳送給另一方來接收並解析,序列化傳送和接收反序列化並不在一起。但是下面為了例子簡單將二者寫在同一程式中。

  1. import java.io.ByteArrayInputStream;  
  2. import java.io.ByteArrayOutputStream;  
  3. import java.io.IOException;  
  4. import java.util.List;  
  5. publicclass Main {  
  6.     publicstaticvoid main(String[] args) throws IOException {  
  7.         // 按照定義的資料結構,建立一個Person
  8.         PersonMsg.Person.Builder personBuilder = PersonMsg.Person.newBuilder();  
  9.         personBuilder.setId(1);  
  10.         personBuilder.setName("叉叉哥");  
  11.         personBuilder.setEmail("[email protected]");  
  12.         personBuilder.addFriends("Friend A");  
  13.         personBuilder.addFriends("Friend B");  
  14.         PersonMsg.Person xxg = personBuilder.build();  
  15.         // 將資料寫到輸出流,如網路輸出流,這裡就用ByteArrayOutputStream來代替
  16.         ByteArrayOutputStream output = new ByteArrayOutputStream();  
  17.         xxg.writeTo(output);  
  18.         // -------------- 分割線:上面是傳送方,將資料序列化後傳送 ---------------
  19.         byte[] byteArray = output.toByteArray();  
  20.         // -------------- 分割線:下面是接收方,將資料接收後反序列化 ---------------
  21.         // 接收到流並讀取,如網路輸入流,這裡用ByteArrayInputStream來代替
  22.         ByteArrayInputStream input = new ByteArrayInputStream(byteArray);  
  23.         // 反序列化
  24.         PersonMsg.Person xxg2 = PersonMsg.Person.parseFrom(input);  
  25.         System.out.println("ID:" + xxg2.getId());  
  26.         System.out.println("name:" + xxg2.getName());  
  27.         System.out.println("email:" + xxg2.getEmail());  
  28.         System.out.println("friend:");  
  29.         List<String> friends = xxg2.getFriendsList();  
  30.         for(String friend : friends) {  
  31.             System.out.println(friend);  
  32.         }  
  33.     }  
  34. }  


protobuf是谷歌的Protocol Buffers的簡稱,用於結構化資料和位元組碼之間互相轉換(序列化、反序列化),一般應用於網路傳輸,可支援多種程式語言。

protobuf如何使用這裡不再介紹,本文主要介紹在MINA、Netty、Twisted中如何使用protobuf,不瞭解protobuf的同學可以去參考我的另一篇博文

前面的一篇博文中,有介紹到一種用一個固定為4位元組的字首Header來指定Body的位元組數的一種訊息分割方式,在這裡同樣要使用到。只是其中Body的內容不再是字串,而是protobuf位元組碼。


在處理業務邏輯時,肯定不希望還要對資料進行序列化和反序列化,而是希望直接操作一個物件,那麼就需要有相應的編碼器和解碼器,將序列化和反序列化的邏輯寫在編碼器和解碼器中。有關編碼器和解碼器的實現,上一篇博文中有介紹。

Netty包中已經自帶針對protobuf的編碼器和解碼器,那麼就不用再自己去實現了。而MINA、Twisted還需要自己去實現protobuf的編碼器和解碼器。

這裡定義一個protobuf資料結構,用於描述一個學生的資訊,儲存為StudentMsg.proto檔案:

  1. message Student {  
  2.     // ID  
  3.     required int32 id = 1;    
  4.     // 姓名  
  5.     required string name = 2;  
  6.     // email  
  7.     optional string email = 3;  
  8.     // 朋友  
  9.     repeated string friends = 4;  
  10. }  

用StudentMsg.proto分別生成Java和Python程式碼,將程式碼加入到相應的專案中。生成的程式碼就不再貼上來了。

下面分別介紹在Netty、MINA、Twisted如何使用protobuf來傳輸Student資訊。

Netty:

Netty自帶protobuf的編碼器和解碼器,分別是ProtobufEncoder和ProtobufDecoder。需要注意的是,ProtobufEncoder和ProtobufDecoder只負責protobuf的序列化和反序列化,而處理訊息Header字首和訊息分割的還需要LengthFieldBasedFrameDecoder和LengthFieldPrepender。LengthFieldBasedFrameDecoder即用於解析訊息Header字首,根據Header中指定的Body位元組數擷取Body,LengthFieldPrepender用於在wirte訊息時在訊息前面新增一個Header字首來指定Body位元組數。

  1. publicclass TcpServer {  
  2.     publicstaticvoid main(String[] args) throws InterruptedException {  
  3.         EventLoopGroup bossGroup = new NioEventLoopGroup();  
  4.         EventLoopGroup workerGroup = new NioEventLoopGroup();  
  5.         try {  
  6.             ServerBootstrap b = new ServerBootstrap();  
  7.             b.group(bossGroup, workerGroup)  
  8.                     .channel(NioServerSocketChannel.class)  
  9.                     .childHandler(new ChannelInitializer<SocketChannel>() {  
  10.                         @Override
  11.                         publicvoid initChannel(SocketChannel ch)  
  12.                                 throws Exception {  
  13.                             ChannelPipeline pipeline = ch.pipeline();  
  14.                             // 負責通過4位元組Header指定的Body長度將訊息切割
  15.                             pipeline.addLast("frameDecoder",   
  16.                                     new LengthFieldBasedFrameDecoder(10485760404));  
  17.                             // 負責將frameDecoder處理後的完整的一條訊息的protobuf位元組碼轉成Student物件
  18.                             pipeline.addLast("protobufDecoder",  
  19.                                     new ProtobufDecoder(StudentMsg.Student.getDefaultInstance()));  
  20.                             // 負責將寫入的位元組碼加上4位元組Header字首來指定Body長度
  21.                             pipeline.addLast("frameEncoder"new LengthFieldPrepender(4));  
  22.                             // 負責將Student物件轉成protobuf位元組碼
  23.                             pipeline.addLast("protobufEncoder"new ProtobufEncoder());  
  24.                             pipeline.addLast(new TcpServerHandler());  
  25.                         }  
  26.                     });  
  27.             ChannelFuture f = b.bind(8080).sync();  
  28.             f.channel().closeFuture().sync();  
  29.         } finally {  
  30.             workerGroup.shutdownGracefully();  
  31.             bossGroup.shutdownGracefully();  
  32.         }  
  33.     }  
  34. }  

處理事件時,接收和傳送的引數直接就是Student物件:
  1. publicclass TcpServerHandler extends ChannelInboundHandlerAdapter {  
  2.     @Override