Google Protocol Buffer
( 简称 Protobuf
) 是 Google公司研发的 一种灵活高效的可序列化的数据协议 。什么是序列化呢?
序列化(Serialization)将对象的状态信息转换为可以存储或传输的形式的过程
举例来说,我们接触的最多的序列化数据格式有JSON和XML。JSON相对于其他序列化来说,可读性比较强且便于快速编写,因此在前后端分离的今天,一般都采用JSON进行序列化传输。而XML的格式统一,符合标准,同样具有良好的可读性,在Java中的绝大多数配置文件都采用XML。
但是,在上面的两种序列化格式中,XML体积庞大,并且它与JSON的性能都不及今天介绍的主角——Protobuf
首先,在Github上下载 Protobuf
编译器,下载地址为: Github releases 。如果你和我一样使用的Windows系统,那么则下载 protoc-3.6.1-win32.zip
文件。解压完之后,将 Your path/protoc-3.6.1-win32/bin
添加到环境变量中。
在命令行上输入 protoc
查看是否安装成功:
首先,我们需要编写一个 proto
文件,用来定义程序中需要处理的结构化数据(即 Message
)。 proto
文件类似于Java或者C语言的数据定义。
如下,创建 person.proto
文件,定义一个 Person
的 Message
,包含三个属性: id
、 name
、 email
:
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文件,编译命令格式为:
$ 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
的序列化。
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对象。
第二种序列化方式是将 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
中,适用了同样的技巧。
第三种则是通过写入文件进行序列化:
// 序列化,将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(); 复制代码
在Netty中,对 Protobuf
做了支持,并内置了响应的编解码器实现,如下:
名称 | 描述 |
---|---|
ProtobufEncoder | 使用Protobuf对消息进行编码 |
ProtobufDecoder | 使用Protobuf对消息进行解码 |
ProtobufVarint32FrameDecoder | 根据消息中的Protobuf的 Base 128 Varints 整型长度字段值动态地分割所接受到的ByteBuf |
ProtobufVarint32LengthFieldPrepender | 向ByteBuf前追加一个Protobuf的 Base 128 Varints 整型的长度字段值 |
引导部分在此不做赘述,更多可以看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() ); } } 复制代码
同样,客户端也要添加 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的知识,可以到官网浏览文档!