Google高性能序列化框架Protobuf認識及與Netty的結合

本文章著做權歸Pushy全部,如需轉載請聯繫做者,並註明出處:pushy.sitejava

1. Protobuf

1.1 介紹

Google Protocol Buffer( 簡稱 Protobuf) 是 Google公司研發的一種靈活高效的可序列化的數據協議。什麼是序列化呢?git

序列化(Serialization)將對象的狀態信息轉換爲能夠存儲或傳輸的形式的過程github

舉例來講,咱們接觸的最多的序列化數據格式有JSON和XML。JSON相對於其餘序列化來講,可讀性比較強且便於快速編寫,所以在先後端分離的今天,通常都採用JSON進行序列化傳輸。而XML的格式統一,符合標準,一樣具備良好的可讀性,在Java中的絕大多數配置文件都採用XML。shell

可是,在上面的兩種序列化格式中,XML體積龐大,而且它與JSON的性能都不及今天介紹的主角——Protobuf後端

1.2 安裝

首先,在Github上下載Protobuf編譯器,下載地址爲:Github releases。若是你和我同樣使用的Windows系統,那麼則下載protoc-3.6.1-win32.zip文件。解壓完以後,將Your path\protoc-3.6.1-win32\bin添加到環境變量中。數組

在命令行上輸入protoc查看是否安裝成功:bash

1.3 使用

首先,咱們須要編寫一個proto文件,用來定義程序中須要處理的結構化數據(即Message)。proto文件相似於Java或者C語言的數據定義。服務器

以下,建立person.proto文件,定義一個PersonMessage,包含三個屬性:idnameemail前後端分離

syntax = "proto3";  // 執行protobuf的協議版本
option java_package = "site.pushy.protobuf";  // 指定包名
option java_outer_classname = "PersonEntity"; //生成的數據訪問類的類名

message Person {
  int32 id = 1;
  string name = 2;
  string email = 3;
}
複製代碼

而後,經過protoc來將該proto文件定義的結構化數據編譯成爲Java文件,編譯命令格式爲:socket

$ protoc -I=存放proto文件的目錄 --java_out=生成的Java文件輸入路徑 proto文件的路徑
複製代碼

例如,我將proto文件放在了E盤的demo下,並將它生成的Java文件放在E:\demo\protobuf\src\main\java下,則命令以下:

$ protoc -I=E:\demo --java_out=E:\demo\protobuf\src\main\java E:\demo\person.proto
複製代碼

運行完以後,將會生成PersonEntity類:

package site.pushy.protobuf;

public final class PersonEntity {
    private PersonEntity() {}
    // 代碼省略
}
複製代碼

生成的PersonEntity類,咱們能夠經過建造者模式建立Person對象:

public class CreatePerson {
    
    public static PersonEntity.Person create() {
        PersonEntity.Person person = PersonEntity.Person.newBuilder()
                .setId(1)
                .setName("Pushy")
                .setEmail("1437876073@qq.com")
                .build();
            
        System.out.println(person);
        return person;
    }
}
複製代碼

打印的結果爲:

id: 1
name: "Pushy"
email: "1437876073@qq.com"
複製代碼

怎麼樣?使用是否是很是簡單,下面咱們來了解一下Protobuf的序列化。

2. 序列化

2.1 字節數組

Protobuf最簡單序列化方式是將Person對象轉換爲字節數組,例如:

// 序列化
PersonEntity.Person person = CreatePerson.create();
byte[] data = person.toByteArray();

// 反序列化
PersonEntity.Person parsePerson = PersonEntity.Person.parseFrom(data);
System.out.println(parsePerson.name);
複製代碼

這種方式能夠適用於不少場景,Protobuf會根據本身的編碼方式將Java對象序列化成字節數組。同時Protobuf也會從字節數組中從新編碼,獲得新的Java POJO對象。

2.2 Stream

第二種序列化方式是將Protobuf對象寫入Stream:

// 序列化,粘包,將一個或者多個ProtoBuf寫入到Stream
PersonEntity.Person person = CreatePerson.create();
ByteArrayOutputStream os = new ByteArrayOutputStream();
person.writeTo(os);

// 反序列化,拆包,從stream中讀出一個或者多個Protobuf字節對象
ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray());
PersonEntity.Person parsePerson = PersonEntity.Person.parseFrom(is);
System.out.println(parsePerson);
複製代碼

這種方式比較適用於RPC調用和Socket傳輸,在序列化的字節數組以前,添加一個varint32的數字表示字節數組的長度;那麼在反序列化時,能夠經過先讀取varint,而後再依次讀取此長度的字節;這種方式有效的解決了socket傳輸時如何「拆包」「封包」的問題。在Netty中,適用了一樣的技巧。

2.3 文件

第三種則是經過寫入文件進行序列化:

// 序列化,將Protobuf對象保存爲文件
PersonEntity.Person person = CreatePerson.create();
FileOutputStream fos = new FileOutputStream("pushy.dt");
person.writeTo(fos);
fos.close();

// 反序列化,從文件中讀取和解析Protobuf對象
FileInputStream fis = new FileInputStream("pushy.dt");
PersonEntity.Person parsePerson = PersonEntity.Person.parseFrom(fis);
System.out.println(parsePerson);
fis.close();
複製代碼

3. 結合Netty

在Netty中,對Protobuf作了支持,並內置了響應的編解碼器實現,以下:

名稱 描述
ProtobufEncoder 使用Protobuf對消息進行編碼
ProtobufDecoder 使用Protobuf對消息進行解碼
ProtobufVarint32FrameDecoder 根據消息中的Protobuf的Base 128 Varints整型長度字段值動態地分割所接受到的ByteBuf
ProtobufVarint32LengthFieldPrepender 向ByteBuf前追加一個Protobuf的Base 128 Varints整型的長度字段值

3.1 服務端

引導部分在此不作贅述,更多能夠看demo源碼。咱們主要來介紹一下ChannelPipeline中的設置。

服務端部分,須要添加關於Protobuf相應的編解碼器,另外,還添加ServerHandler來處理服務端的業務邏輯:

public class ServerChannelInitializer extends ChannelInitializer<SocketChannel> {

    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        pipeline.addLast(new ProtobufVarint32FrameDecoder());
        pipeline.addLast(new ProtobufEncoder());
        pipeline.addLast(new ProtobufDecoder(PersonEntity.Person.getDefaultInstance()));
        pipeline.addLast(new ServerHandler());
    }
}
複製代碼

服務器端的解碼器會自動將類型轉換爲PersonEntity.Person

public class ServerHandler extends SimpleChannelInboundHandler<PersonEntity.Person> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, PersonEntity.Person person) throws Exception {
        System.out.println("chanelRead0 =>" + person.getName() );
    }
}
複製代碼

3.2 客戶端

一樣,客戶端也要添加Protobuf相應的編解碼器:

public class ClientChannelInitializer extends ChannelInitializer<SocketChannel> {

    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        pipeline.addLast(new ProtobufVarint32FrameDecoder());
        pipeline.addLast(new ProtobufDecoder(PersonEntity.Person.getDefaultInstance()));
        pipeline.addLast(new ProtobufVarint32LengthFieldPrepender());
        pipeline.addLast(new ProtobufEncoder());
        pipeline.addLast(new ClientHandler());
    }
}
複製代碼

並使用ClientHandler來向服務端發送Protobuf的消息,用於配置了客戶端的解碼器,所以在使用writeAndFlush寫入數據時能夠直接傳入PersonEntity.Person類型數據:

public class ClientHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        ctx.writeAndFlush(getPerson());
    }

    private PersonEntity.Person getPerson() {
        return PersonEntity.Person.newBuilder()
                .setName("Pushy")
                .setEmail("1437876073@qq.com")
                .build();
    }

}
複製代碼

測試一下,能夠看到服務端確實能經過Protobuf序列化收到客戶端發送的消息:

最後,代碼已上傳到Github,想要了解更多關於Protobuf的知識,能夠到官網瀏覽文檔!

相關文章
相關標籤/搜索