使用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名不要一樣)。
- message Person {
- // ID(必需)
- required int32 id = 1;
- // 姓名(必需)
- required string name = 2;
- // email(可選)
- optional string email = 3;
- // 朋友(集合)
- repeated string friends = 4;
- }
上面的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專案則加入:
- <dependency>
- <groupId>com.google.protobuf</groupId>
- <artifactId>protobuf-java</artifactId>
- <version>2.5.0</version>
- </dependency>
第三步:序列化
第四步:反序列化
一般來說,序列化和反序列化是分開的。例如網路傳輸,由一方將資料序列化後傳送給另一方來接收並解析,序列化傳送和接收反序列化並不在一起。但是下面為了例子簡單將二者寫在同一程式中。
- import java.io.ByteArrayInputStream;
- import java.io.ByteArrayOutputStream;
- import java.io.IOException;
- import java.util.List;
- publicclass Main {
- publicstaticvoid main(String[] args) throws IOException {
- // 按照定義的資料結構,建立一個Person
- PersonMsg.Person.Builder personBuilder = PersonMsg.Person.newBuilder();
- personBuilder.setId(1);
- personBuilder.setName("叉叉哥");
- personBuilder.setEmail("[email protected]");
- personBuilder.addFriends("Friend A");
- personBuilder.addFriends("Friend B");
- PersonMsg.Person xxg = personBuilder.build();
- // 將資料寫到輸出流,如網路輸出流,這裡就用ByteArrayOutputStream來代替
- ByteArrayOutputStream output = new ByteArrayOutputStream();
- xxg.writeTo(output);
- // -------------- 分割線:上面是傳送方,將資料序列化後傳送 ---------------
- byte[] byteArray = output.toByteArray();
- // -------------- 分割線:下面是接收方,將資料接收後反序列化 ---------------
- // 接收到流並讀取,如網路輸入流,這裡用ByteArrayInputStream來代替
- ByteArrayInputStream input = new ByteArrayInputStream(byteArray);
- // 反序列化
- PersonMsg.Person xxg2 = PersonMsg.Person.parseFrom(input);
- System.out.println("ID:" + xxg2.getId());
- System.out.println("name:" + xxg2.getName());
- System.out.println("email:" + xxg2.getEmail());
- System.out.println("friend:");
- List<String> friends = xxg2.getFriendsList();
- for(String friend : friends) {
- System.out.println(friend);
- }
- }
- }
protobuf是谷歌的Protocol Buffers的簡稱,用於結構化資料和位元組碼之間互相轉換(序列化、反序列化),一般應用於網路傳輸,可支援多種程式語言。
protobuf如何使用這裡不再介紹,本文主要介紹在MINA、Netty、Twisted中如何使用protobuf,不瞭解protobuf的同學可以去參考我的另一篇博文。
在前面的一篇博文中,有介紹到一種用一個固定為4位元組的字首Header來指定Body的位元組數的一種訊息分割方式,在這裡同樣要使用到。只是其中Body的內容不再是字串,而是protobuf位元組碼。
在處理業務邏輯時,肯定不希望還要對資料進行序列化和反序列化,而是希望直接操作一個物件,那麼就需要有相應的編碼器和解碼器,將序列化和反序列化的邏輯寫在編碼器和解碼器中。有關編碼器和解碼器的實現,上一篇博文中有介紹。
Netty包中已經自帶針對protobuf的編碼器和解碼器,那麼就不用再自己去實現了。而MINA、Twisted還需要自己去實現protobuf的編碼器和解碼器。
這裡定義一個protobuf資料結構,用於描述一個學生的資訊,儲存為StudentMsg.proto檔案:
- message Student {
- // ID
- required int32 id = 1;
- // 姓名
- required string name = 2;
- optional string email = 3;
- // 朋友
- repeated string friends = 4;
- }
用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位元組數。
- publicclass TcpServer {
- publicstaticvoid main(String[] args) throws InterruptedException {
- EventLoopGroup bossGroup = new NioEventLoopGroup();
- EventLoopGroup workerGroup = new NioEventLoopGroup();
- try {
- ServerBootstrap b = new ServerBootstrap();
- b.group(bossGroup, workerGroup)
- .channel(NioServerSocketChannel.class)
- .childHandler(new ChannelInitializer<SocketChannel>() {
- @Override
- publicvoid initChannel(SocketChannel ch)
- throws Exception {
- ChannelPipeline pipeline = ch.pipeline();
- // 負責通過4位元組Header指定的Body長度將訊息切割
- pipeline.addLast("frameDecoder",
- new LengthFieldBasedFrameDecoder(1048576, 0, 4, 0, 4));
- // 負責將frameDecoder處理後的完整的一條訊息的protobuf位元組碼轉成Student物件
- pipeline.addLast("protobufDecoder",
- new ProtobufDecoder(StudentMsg.Student.getDefaultInstance()));
- // 負責將寫入的位元組碼加上4位元組Header字首來指定Body長度
- pipeline.addLast("frameEncoder", new LengthFieldPrepender(4));
- // 負責將Student物件轉成protobuf位元組碼
- pipeline.addLast("protobufEncoder", new ProtobufEncoder());
- pipeline.addLast(new TcpServerHandler());
- }
- });
- ChannelFuture f = b.bind(8080).sync();
- f.channel().closeFuture().sync();
- } finally {
- workerGroup.shutdownGracefully();
- bossGroup.shutdownGracefully();
- }
- }
- }
處理事件時,接收和傳送的引數直接就是Student物件:
- publicclass TcpServerHandler extends ChannelInboundHandlerAdapter {
- @Override