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的知识,能够到官网浏览文档!

相关文章
相关标签/搜索