一文玩转"Protobuf"通信协议
Protobuf全称是Google Protocol Buffer
Protobuf是Google提出的一种数据交换的格式,是一套类似JSON或者XML的数据传输格式和规范,用于不同应用或进程之间进行通信。Protobuf具有以下特点:
(1)语言无关,平台无关;Protobuf支持Java、 C++,、Python、JavaScript等多种语言,支持跨多个平台。
(2)高效;比XML更小(3~10倍),更快(20 ~ 100倍),更为简单。
(3)扩展性,兼容性好;可以更新数据结构,而不影响和破坏原有的旧程序。
Protobuf既独立于语言,又独立于平台。Google官方提供了多种语言的实现:Java、C#、C++、GO、JavaScript和Python。Protobuf的编码过程为:使用预先定义的Message数据结构将实际的传输数据进行打包,然后编码成二进制的码流进行传输或者存储。Protobuf的解码过程则刚好与编码过程相反:将二进制码流解码成Protobuf自己定义的Message结构的POJO实例。
与JSON、XML相比,Protobuf算是后起之秀,只是Protobuf更加适合于高性能、快速响应的数据传输应用场景。Protobuf数据包是一种二进制的格式,相对于文本格式的数据交换(JSON、XML)来说,速度要快很多。由于Protobuf优异的性能,使得它更加适用于分布式应用场景下的数据通信或者异构环境下的数据交换。
另外,JSON、XML是文本格式,数据具有可读性;而Protobuf是二进制数据格式,数据本身不具有可读性,只有反序列化之后才能得到真正可读的数据。正因为Protobuf是二进制数据格式,数据序列化之后,体积相比JSON和XML要小,更加适合网络传输。
总体来说,在一个需要大量数据传输的应用场景中,因为数据量很大,那么选择Protobuf可以明显地减少传输的数据量和提升网络IO的速度。对于打造一款高性能的通信服务器来说,Protobuf传输协议是最高性能的传输协议之一。微信的消息传输就采用了Protobuf协议。
一个简单的proto文件的实践案例
Protobuf使用proto文件来预先定义的消息格式。数据包是按照proto文件所定义的消息格式完成二进制码流的编码和解码。proto文件简单地说,就是一个消息的协议文件,这个协议文件的后缀文件名为“.proto”。
作为演示,下面介绍一个非常简单的proto文件:仅仅定义一个消息结构体,并且该消息结构体也非常简单,仅包含两个字段。实例如下:
// [开始头部声明]
syntax = "proto3";
package com.talkweb.netty.protocol;
// [结束头部声明]
// [开始 java 选项配置]
option java_package = "com.talkweb.netty.protocol";
option java_outer_classname = "MsgProtos";
// [结束 java 选项配置]
// [开始消息定义]
message Msg {
uint32 id = 1; //消息 ID
string content = 2;//消息内容
}
// [结束消息定义]
在“.proto”文件的头部声明中,需要声明一下所使用的Protobuf协议版本,示例中使用的是"proto3"版本。也可以使用旧一点的"proto2"版本,两个版本的消息格式有一些细微的不同,默认的协议版本为"proto2"。
Protobuf支持很多语言,所以它为不同的语言提供了一些可选的配置选项,配置选项的使用option关键字。“option java_package”选项的作用为:在生成“proto”文件中消息的POJO类和Builder(构造者)的Java代码时,将生成的Java代码放入该选项所指定的package类路径中。“option java_outer_classname”选项的作用为:在生成“proto”文件所对应Java代码时,生产的Java外部类使用配置的名称。
在“proto”文件中,使用message关键字来定义消息的结构体。在生成“proto”对应的Java代码时,每个具体的消息结构体将对应于一个最终的Java POJO类。结构体的字段(Field)对应到POJO类的属性(Attribute)。也就是说,每定义一个“message”结构体相当于声明一个Java中的类。“proto”文件的message可以内嵌message,就像java的内部类一样。
每一个消息结构体可以有多个字段。定义一个字段的格式为“类型名称 = 编号”。例如“string content = 2;”,表示该字段是string类型,字段名为content,编号为2。字段编号表示为:在Protobuf数据包的序列化、反序列化时,该字段的具体排序。
在每一个“.proto”文件中,可以声明多个“message”。大部分情况下会把存在依赖关系或者包含关系的message消息结构体写入一个.proto文件。将那些没有关系、相互独立的message消息结构体,分别写入不同的文件,这样便于管理。
通过控制台命令生成POJO和Builder
完成“.proto”文件定义后,下一步就是生成消息的POJO类和Builder(构造者)类。有两种方式生成Java类:
- 一种是通过控制台命令的方式;
- 另一种是使用Maven插件的方式。
先看第一种方式:通过控制台命令生成消息的POJO类和Builder构造者。
首先从“https://github.com/protocolbuffers/protobuf/releases” 下载Protobuf的安装包,可以选择不同的版本,这里下载的是3.6.1的Java版本。在Windows下解压后执行安装。备注:这里以Windows平台为例子,对于在Linux或者Mac平台下,大家可自行尝试。
生成构造者代码,需要用到安装文件中的protoc.exe可执行文件。安装完成后,设置一下path环境变量,将proto的安装目录加入到path环境变量中。
下面开始使用protoc.exe文件生成Java的Builder(构造者)。生成的命令如下:
protoc.exe --java_out=./src/main/java/ ./Msg.proto
上面的命令中,使用的“proto”文件的名称为:./Msg.proto,所生产的POJO类和构造者类的输出文件夹为 ./src/main/java/。
使用命令行生成Java类的操作比较烦琐,另一种更加方便的方式是:使用protobufmaven-plugin插件生成Java类。
通过Maven插件生成POJO和Builder
使用protobuf-maven-plugin插件,可以非常方便地生成消息的POJO类和Builder(构造者)类的Java代码。在Maven的pom文件中增加此plugin插件的配置项,具体如下:
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.5.0</version>
<extensions>true</extensions>
<configuration>
<!--proto 文件路径-->
<protoSourceRoot>
${project.basedir}/protobuf</protoSourceRoot>
<!--目标路径-->
<outputDirectory>${project.build.sourceDirectory}</outputDirectory>
<!--设置是否在生成 Java 文件之前清空 outputDirectory 的文件-->
<clearOutputDirectory>false</clearOutputDirectory>
<!--临时目录-->
<temporaryProtoFileDirectory>
${project.build.directory}/protoc-temp
</temporaryProtoFileDirectory>
<!--protoc 可执行文件路径-->
<protocExecutable>
${project.basedir}/protobuf/protoc3.6.1.exe
</protocExecutable>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
</plugin>
protobuf-maven-plugin插件的配置项,具体介绍如下
- protoSourceRoot:“proto”消息结构体所在文件的路径;
- outputDirectory:生成的POJO类和Builder类的目标路径;
- protocExecutable:protobuf的Java代码生成工具的protoc3.6.1.exe可执行文件的路径
配置好之后,执行插件的compile命令,Java代码就利索生成了。或者在Maven的项目编译时,POJO类和Builder类也会自动生成。
实战:Protobuf序列化与反序列化演示案例
在Maven的pom.xml文件中加上protobuf的Java运行包的依赖,代码如下
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>${protobuf.version}</version>
</dependency>
这里的protobuf.version版本号的值为3.6.1。需要注意的是:Java运行时的potobuf依赖坐标的版本,“.proto”消息结构体文件中的syntax配置项值(protobuf协议的版本号),以及通过“.proto”文件生成POJO和Builder类的“protoc3.6.1.exe”可执行文件的版本,这三个版本需要配套一致.
1、使用Builder构造者,构造POJO消息对象
package com.talkweb.netty.protocol;
//...
public class ProtobufDemo {
public static MsgProtos.Msg buildMsg() {
MsgProtos.Msg.Builder personBuilder = MsgProtos.Msg.newBuilder();
personBuilder.setId(1000);
personBuilder.setContent("拓维信息,技术创新");
MsgProtos.Msg message = personBuilder.build();
return message;
}
//…..
}
Protobuf为每个message消息结构体生成的Java类中,包含了一个POJO类、一个Builder类。构造POJO消息,首先使用POJO类的newBuilder静态方法获得一个Builder构造者,其次POJO每一个字段的值,需要通过Builder构造者的setter方法去设置。字段值设置完成之后,使用构造者的build()方法构造出POJO消息对象。
2.序列化serialization与反序列化Deserialization的方式一
获得消息POJO的实例之后,可以通过多种方法将POJO对象序列化成二进制字节,或者反序列化。方式一为调用Protobuf POJO对象的toByteArray()方法将POJO对象序列化成字节数组,具体的代码如下:
package com.talkweb.netty.protocol;
//...
public class ProtobufDemo {
//第 1 种方式:序列化 serialization &反序列化 Deserialization
@Test
public void serAndDesr1() throws IOException {
MsgProtos.Msg message = buildMsg();
//将 Protobuf 对象序列化成二进制字节数组
byte[] data = message.toByteArray();
//可以用于网络传输,保存到内存或外存
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
outputStream.write(data);
data = outputStream.toByteArray();
//二进制字节数组反序列化成 Protobuf 对象
MsgProtos.Msg inMsg = MsgProtos.Msg.parseFrom(data);
Logger.info("id:=" + inMsg.getId());
Logger.info("content:=" + inMsg.getContent());
}
//…
}
这种方式,首先通过调用Protobuf POJO对象的toByteArray()方法将POJO对象序列化成字节数组,然后通过调用Protobuf POJO类的parseFrom(byte[] data)静态方法,可以从字节数组中重新反序列化得到POJO新的实例。
这种方式类似于普通Java对象的序列化,适用于很多将Protobuf的POJO序列化到内存或者外存(如物理硬盘)的应用场景。
3、序列化serialization与反序列化Deserialization的方式二
这种方式通过调用Protobuf生成的POJO对象的writeTo(OutputStream)方法将POJO对象的二进制字节写出到输出流。通过调用Protobuf生成的POJO对象的parseFrom(InputStream)方法,Protobuf从输入流中读取二进制码然后反序列化,得到POJO新的实例。具体的代码如下:
package com.talkweb.netty.protocol;
//...
public class ProtobufDemo {
//…
//第 2 种方式:序列化 serialization &反序列化 Deserialization
@Test
public void serAndDesr2() throws IOException {
MsgProtos.Msg message = buildMsg();
//序列化到二进制码流
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
message.writeTo(outputStream);
ByteArrayInputStream inputStream =
new ByteArrayInputStream(outputStream.toByteArray());
//从二进码流反序列化成 Protobuf 对象
MsgProtos.Msg inMsg = MsgProtos.Msg.parseFrom(inputStream);
Logger.info("id:=" + inMsg.getId());
Logger.info("content:=" + inMsg.getContent());
}
}
以上代码调用了POJO对象的writeTo(OutputStream)方法将自己的二进制字节写出到输出流,然后通过调用静态类的parseFrom(InputStream)方法,Protobuf从输入流中读取二进制码重新反序列化,得到POJO新的实例。
在阻塞式的二进制码流传输应用场景中,这种序列化和反序列化的方式是没有问题
的。例如,可以将二进制码流写入阻塞式的Java OIO套接字或者输出到文件。但是,这种方式在异步操作的NIO应用场景中,存在粘包/半包的问题。
4. 序列化serialization和反序列化Deserialization的方式三
这种方式通过调用Protobuf生成的POJO对象的writeDelimitedTo(OutputStream)方法在序列化的字节码之前添加了字节数组的长度。这一点类似于前面介绍的Head-Content协议,只不过Protobuf做了优化,长度的类型不是固定长度的int类型,而是可变长度varint32类型。具体实例如下:
package com.talkweb.netty.protocol;
//...
public class ProtobufDemo {
//…
//第 3 种方式:序列化 serialization &反序列化 Deserialization
//带字节长度:[字节长度][字节数据],用于解决粘包/半包问题
@Test
public void serAndDesr3() throws IOException {
MsgProtos.Msg message = buildMsg();
//序列化到二进制码流
ByteArrayOutputStream outputStream =new ByteArrayOutputStream();
message.writeDelimitedTo(outputStream);
ByteArrayInputStream inputStream = new
ByteArrayInputStream(outputStream.toByteArray());
//从二进码字节流反序列化成 Protobuf 对象
MsgProtos.Msg inMsg =
MsgProtos.Msg.parseDelimitedFrom(inputStream);
Logger.info("id:=" + inMsg.getId());
Logger.info("content:=" + inMsg.getContent());
}
}
反序列化时,调用Protobuf生成的POJO类的parseDelimitedFrom(InputStream)静态方法,从输入流中先读取varint32类型的长度值,然后根据长度值读取此消息的二进制字节,再反序列化得到POJO新的实例。
这种方式可以用于异步操作的NIO应用场景中,解决了粘包/半包的问题。
牛逼