内容摘抄自书籍《Netty redis zookeeper高并发实战》
proto文件来预先定义的消息格式。数据包是按照proto文件所定义的消息格式完成二进制码流的编码和解码。proto文件,简单地说,就是一个消息的协议文件,这个协议文件的后缀文件名为“.proto”。 作为演示,下面介绍一个非常简单的proto文件:仅仅定义一个消息结构体,并且该消息结构体也非常简单,仅包含两个字段。实例如下:
// [开始头部声明]
syntax = "proto3";
packagecom.crazymakercircle.netty.protocol;
// [结束头部声明]
// [开始 java选项配置]
option java_package = "com.crazymakercircle.netty.protocol";
option java_outer_classname = "MsgProtos";
// [结束 java选项配置]
// [开始消息定义]
message Msg {
uint32 id = 1; //消息ID
string content = 2;//消息内容
}
// [结束消息定义]
例子:
syntax = "proto3";
message Model {
int64 id = 1;
string action = 2;
string content = 3;
string sender = 4;
string receiver = 5;
string extra = 6;
string title = 7;
string format = 8;
int64 timestamp = 9;
}
注:在idea中下载protobuf插件即可以高亮proto文件
在“.proto”文件的头部声明中,需要声明“.proto”所使用的Protobuf协议版本,这里使用的是"proto3"。也可以使用旧一点的版本"proto2",两个版本的消息格式有一些细微的不同。
默认的协议版本为"proto2"。
Protobuf支持很多语言,所以它为不同的语言提供了一些可选的声明选项,选项的前面有option关键字。
“java_package”选项的作用为:在生成“proto”文件中消息的POJO类和Builder(构造者)的Java代码时,将Java代码放入指定的package中。
“java_outer_classname”选项的作用为:在生成“proto”文件所对应Java代码时,所生产的Java外部类的名称。 在“proto”文件中,使用message这个关键字来定义消息的结构体。在生成“proto”对应的Java代码时,
每个具体的消息结构体都对应于一个最终的Java POJO类。消息结构体的字段对应到POJO类的属性。也就是说,每定义一个“message”结构体相当于声明一个Java中的类。并且message中可以内嵌message,就像java的内部类一样。 每一个消息结构体可以有多个字段。定义一个字段的格式,简单来说就是“类型名称=编号”。
例如“string content=2;”,表示该字段是string类型,名为content,序号为2。字段序号表示为:在Protobuf数据包的序列化、反序列化时,该字段的具体排序。 在每一个“.proto”文件中,可以声明多个“message”。大部分情况下,会把有依赖关系或者包含关系的message消息结构体写入一个.proto文件。将那些没有关联关系的message消息结构体,分别写入不同的文件,这样便于管理。
使用命令行生成Java类的操作比较烦琐。另一种更加方便的方式是:使用protobuf-maven-plugin插件,它可非常方便地生成消息的POJO类和Builder(构造者)类的Java代码。在Maven的pom文件中增加此plugin插件的配置项,具体如下:
<plugin>
<groupId>org.xolstice.maven.pluginsgroupId>
<artifactId>protobuf-maven-pluginartifactId>
<version>0.5.0version>
<extensions>trueextensions>
<configuration>
<protoSourceRoot>${project.basedir}/protobufprotoSourceRoot>
<outputDirectory>${project.build.sourceDirectory}outputDirectory>
<clearOutputDirectory>falseclearOutputDirectory>
<temporaryProtoFileDirectory>
${project.build.directory}/protoc-temp
temporaryProtoFileDirectory>
<protocExecutable>
${project.basedir}/protobuf/protoc3.6.1.exe
protocExecutable>
configuration>
<executions>
<execution>
<goals>
<goal>compilegoal>
<goal>test-compilegoal>
goals>
execution>
executions>
plugin>
protobuf-maven-plugin插件的配置项,具体介绍如下:
·protoSourceRoot:“proto”消息结构体文件的路径。
·outputDirectory:生成的POJO类和Builder类的目标路径。
·protocExecutable:Java代码生成工具的protoc3.6.1.exe可执行文件的路径。
配置好之后,执行插件的compile命令,Java代码就利索生成了。或者在Maven的项目编译时,POJO类和Builder类也会自动生成。
package com.crazymakercircle.netty.protocol;
//...
public class ProtobufDemo {
public static MsgProtos.MsgbuildMsg() {
MsgProtos.Msg.BuilderpersonBuilder = 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方法去设置。注意,消息POJO对象并没有setter方法。字段值设置完成之后,使用构造者的build()方法构造出POJO消息对象。
一 获得消息POJO的实例之后,可以通过多种方法将POJO对象序列化成二进制字节,或者反序列化。下面是方式一:
package com.crazymakercircle.netty.protocol;
//...
public class ProtobufDemo {
//第1种方式:序列化 serialization &反序列化 Deserialization
@Test
public void serAndDesr1() throws IOException {
MsgProtos.Msg message = buildMsg();
//将Protobuf对象序列化成二进制字节数组
byte[] data = message.toByteArray();
//可以用于网络传输,保存到内存或外存
ByteArrayOutputStreamoutputStream = new ByteArrayOutputStream();
outputStream.write(data);
data = outputStream.toByteArray();
//二进制字节数组反序列化成Protobuf对象
MsgProtos.MsginMsg = MsgProtos.Msg.parseFrom(data);
Logger.info("id:=" + inMsg.getId());
Logger.info("content:=" + inMsg.getContent());
}
//….
}
这种方式通过调用POJO对象的toByteArray()方法将POJO对象序列化成字节数组。通过调用parseFrom(byte[] data)方法,Protobuf也可以从字节数组中重新反序列化得到POJO新的实例。
package com.crazymakercircle.netty.protocol;
//...
public class ProtobufDemo {
//…
//第2种方式:序列化 serialization &反序列化 Deserialization
@Test
public void serAndDesr2() throws IOException {
MsgProtos.Msg message = buildMsg();
//序列化到二进制码流
ByteArrayOutputStreamoutputStream = new ByteArrayOutputStream();
message.writeTo(outputStream);
ByteArrayInputStreaminputStream =
new ByteArrayInputStream(outputStream.toByteArray());
//从二进码流反序列化成Protobuf对象
MsgProtos.MsginMsg = MsgProtos.Msg.parseFrom(inputStream);
Logger.info("id:=" + inMsg.getId());
Logger.info("content:=" + inMsg.getContent());
}
//….
}
这种方式通过调用POJO对象的writeTo(OutputStream)方法将POJO对象的二进制字节写出到输出流。通过调用parseFrom(InputStream)方法,Protobuf从输入流中读取二进制码流重新反序列化,得到POJO新的实例。 在阻塞式的二进制码流传输应用场景中,这种序列化和反序列化的方式是没有问题的。例如,可以将二进制码流写入阻塞式的Java OIO套接字或者输出到文件。但是,这种方式在异步操作的NIO应用场景中,存在着粘包/半包的问题。
package com.crazymakercircle.netty.protocol;
//...
public class ProtobufDemo {
//…
//第3种方式:序列化 serialization &反序列化 Deserialization
//带字节长度:[字节长度][字节数据],解决粘包/半包问题
@Test
public void serAndDesr3() throws IOException {
MsgProtos.Msg message = buildMsg();
//序列化到二进制码流
ByteArrayOutputStreamoutputStream = new ByteArrayOutputStream();
message.writeDelimitedTo(outputStream);
ByteArrayInputStreaminputStream
= new ByteArrayInputStream(outputStream.toByteArray());
//从二进码流反序列化成Protobuf对象
MsgProtos.MsginMsg = MsgProtos.Msg.parseDelimitedFrom(inputStream);
Logger.info("id:=" + inMsg.getId());
Logger.info("content:=" + inMsg.getContent());
}
}
这种方式通过调用POJO对象的writeDelimitedTo(OutputStream)方法在序列化的字节码之前添加了字节数组的长度。这一点类似于前面介绍的Head-Content协议,只不过Protobuf做了优化,长度的类型不是固定长度的int类型,而是可变长度varint32类型。 反序列化时,调用parseDelimitedFrom(InputStream)方法。Protobuf从输入流中先读取varint32类型的长度值,然后根据长度值读取此消息的二进制字节,再反序列化得到POJO新的实例。 这种方式可以用于异步操作的NIO应用场景中,解决了粘包/半包的问题。