Protobuf
全称:Protocol
Buffers
,由谷歌开源而来,经谷歌内部测试使用。它将数据结构以.proto
文件进行描述,通过代码生成工具可以生成对应数据结构的POJO
对象和Protobuf
相关的方法和属性。
# 一、 Protocol
的特点
【1】在谷歌内部长期使用,产品成熟度高;
【2】高效的编解码性能,编码后的消息更小,有利于存储和传输;
【3】语言无关、平台无关、扩展性好
【4】官方支持 Java
、C++
、C#
、 Python
、Go
和 Dart
Protobuf
使用二进制编码,在空间和性能上相对于 XML
具有很大的优势。尽量 XML
的可读性和可扩展性非常好,也非常适合描述数据结构,但是 XML
解析的时间开销和 XML
为了可读性而牺牲的空间开销都非常大,因此不适合做高性能的通信协议。
Protobuf
的数据描述文件和代码生成机制(跨语言的编解码框架,都具有此功能),优点如下:
■ 文本化的数据结构描述语言,可以实现语言和平台无关,特别适合异构系统间的集成;
■ 通过标识字段的顺序,可以实现协议的前向兼容;
■ 自动代码生成,不需要手工编写同样数据结构的 C++
和 Java
版本;
■ 方便后续的管理和维护。相当于代码,结构化的文档更容易管理和维护。
Protobuf
的编解码性能远远高于JSON<Serializable<hession2<hession1<XStream<hession2
压缩(性能有高到底)等序列化框架的序列化和反序列化,这也是很多 RPC 框架选用protobuf
做编解码框架的原因。
# 二、Protobuf 开发环境搭建
【1】首先下载 Protobuf
的最新 Windown
版本:网站地址如下:链接 (opens new window)
protoc-3.9.1-win32.zip
protoc-3.9.1-win64.zip
2
下载后对其解压:进入包含 protoc.exe
的文件目录,配置其环境变量;protoc.exe
工具主要根据 .proto
文件生成代码。
官网对java
编写 .proto
文件,详细说明地址:链接 (opens new window)
下面我们定义一个 person.proto
数据文件。如下: 注释写在#号后,实际不能这么操作。此处为方便理解:
#类似于c++或java。检查一下文件的每一部分,看看它的作用。
syntax = "proto2";
#以包声明开始,这有助于防止不同项目之间的命名冲突
package tutorial;
#在java中,包名用作java包,除非您已经显式地指定了java_包,如我们这里所述。
#即使您确实提供了一个java_包,您也应该定义一个普通包,以避免在协议缓冲区名称空间和非java语言中发生名称冲突。
#如果不提供此属性,以package 为准
#java_package指定生成的类的java包名。
#如果您没有显式地指定它,那么它只匹配包声明给出的包名,但是这些名称通常不适合Java包名(因为它们通常不以域名开头)
option java_package = "com.example.tutorial";
#java_outer_class name选项定义类名,该类名应包含此文件中的所有类。
#如果没有显式地给出java_outer_类名,则将通过将文件名转换为camel case来生成它。
#例如,“my_proto.proto”在默认情况下将使用“myProto”作为外部类名。利用驼峰命名法。
option java_outer_classname = "AddressBookProtos";
#开始定义消息,相当于内部类 Person
message Person {
# required 表示必须字段,1是序号不是赋值的意思,表示唯一的标记。
# 建议不要使用 required 而使用optional 因为当后期将 required 修改为 optional 会有问题。
required string name = 1;
required int32 id = 2;
optional string email = 3;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
【2】通过 protoc.exe
命令行生成 Java
代码,命令如下:[ --java_out
=生成 *.java
文件的存放路径,我所在的目录正是存放person.proto
文件的目录 ]没有任何错误就说明生成成功。
E:\learnWorkspacesDesign\netty_learn\src\protobuf>protoc.exe --java_out=..\main\java person.proto
【3】查看生成的目标文件:或者在外面生成好,拷贝进来也行。建议不要对生成的文件做任何修改。我们发现代码编译出错,原因是因为少了 protobuf
的 jar
包:
引入 protobuf-java
相关的 jar
包,如下:
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.9.1</version>
</dependency>
2
3
4
5
到此为止,Protobuf
开发环境已经搭建完毕,接下来进行示例展示。
# 三、Protobuf
编解码开发
Protobuf
的类库使用比较简单,下面通过对 AddressBookProtos
编解码来介绍 Protobuf
的使用:由于 Protobuf
支持复杂 POJO
对象编解码,所以代码都是通过工具自动生成,相比于传统的 POJO
对象的赋值操作,其使用略微复杂一些。Protobuf
的编解码接口非常简单和实用,但是功能和性能却非常强大,这也是它流行的一个重要原因。
public class TestAddressBookProtos {
public static void main(String[] args) throws InvalidProtocolBufferException {
AddressBookProtos.Person person = createSubscribeReq();
/*
* After decode:name: "ZhengZhaoXiang"
* id: 1
* email: "1179278531@qq.com"
*/
System.out.printf("Before encode :"+person.toString());
AddressBookProtos.Person personObj = decode(encode(person));
/*
* After decode:name: "ZhengZhaoXiang"
* id: 1
* email: "1179278531@qq.com"
*/
System.out.printf("After decode:"+person.toString());
//输出: Assert equal:true
System.out.printf("Assert equal:"+person.equals(personObj));
}
//编码 通过调用 AddressBookProtos.Person 实例的 toByteArray 即可将 Person 编码为 byte 数组。
private static byte[] encode(AddressBookProtos.Person person){
return person.toByteArray();
}
//解码 还可以解码流数据 parseFrom(InputStream i);
private static AddressBookProtos.Person decode(byte[] body) throws InvalidProtocolBufferException {
return AddressBookProtos.Person.parseFrom(body);
}
//创建一个 person 对象
private static AddressBookProtos.Person createSubscribeReq(){
// 通过 AddressBookProtos.Person 的 newBuilder() 静态方法创建 Builder 实例
// 通过 Builder 构建器对 Person 的属性进行设置,对于集合类型,通过addAllXXX()方法将值设置到属性中。
return AddressBookProtos.Person.newBuilder()
.setId(1).setName("ZhengZhaoXiang").setEmail("1179278531@qq.com").build();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 四、Netty
的 Protobuf
服务端开发
【1】标准的服务端:主要区别在于 childHandler
方法中的 PersonChannelInitializer
类的内容。
public class PersonServer {
public static void main(String[] args) throws Exception{
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup,workerGroup).channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
//主要查看 PersonChannelInitializer 内容
.childHandler(new PersonChannelInitializer());
ChannelFuture future = bootstrap.bind(8899).sync();
future.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
【2】PersonChannelInitializer
内容展示:重点关注自定义 handler
(PersonHandler
)
public class PersonChannelInitializer extends ChannelInitializer{
@Override
protected void initChannel(Channel channel) throws Exception {
ChannelPipeline pipeline = channel.pipeline();
//主要用于半包处理
pipeline.addLast(new ProtobufVarint32FrameDecoder());
//解码器,参数 com.google.protobuf.MessageLite 实际上是告诉 ProtobufDecoder 解码的目标类
pipeline.addLast(new ProtobufDecoder(AddressBookProtos.Person.getDefaultInstance()));
pipeline.addLast(new ProtobufVarint32LengthFieldPrepender());
pipeline.addLast(new StringEncoder());
//自定义handler
pipeline.addLast(new PersonHandler());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
【3】自定义 PersonHandler
的内容如下:由于 ProtobufDecoder
已经对消息进行了自动解码,因此接收到的 Person
消息可以直接使用。对用户进行校验,校验通过后构造应答消息返回给客户端,由于使用了 StringEncoder
因此不需要手工编码。
public class PersonHandler extends SimpleChannelInboundHandler {
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, Object msg) throws Exception {
AddressBookProtos.Person person = (AddressBookProtos.Person)msg;
System.out.printf(String.valueOf(channelHandlerContext.channel().remoteAddress()));
System.out.printf("服务端收到的消息 "+person);
channelHandlerContext.writeAndFlush("from client"+ LocalDateTime.now());
}
@Override
public void channelActive(ChannelHandlerContext channelHandlerContext){
channelHandlerContext.writeAndFlush("来着服务端的问候:Active"+"\r\n");
}
@Override
public void exceptionCaught(ChannelHandlerContext channelHandlerContext,Throwable e){
e.printStackTrace();
channelHandlerContext.close();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 五、Netty 的 Protobuf 客户端开发
【1】客户端:主要区别在于 childHandler
方法中的 PersonClientInitializer
类的内容。
public class PersonClient {
public static void main(String[] args) throws Exception{
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(workerGroup).channel(NioSocketChannel.class)
.handler(new PersonClientInitializer());
ChannelFuture future = bootstrap.connect("127.0.0.1",8899).sync();
future.channel().closeFuture().sync();
}finally {
workerGroup.shutdownGracefully();
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
【2】PersonClientInitializer
内容展示:重点关注自定义 handler
(PersonClientHandler
)
public class PersonClientInitializer extends ChannelInitializer{
@Override
protected void initChannel(Channel channel) throws Exception {
ChannelPipeline pipeline = channel.pipeline();
pipeline.addLast(new ProtobufVarint32FrameDecoder());
pipeline.addLast(new StringDecoder());
pipeline.addLast(new ProtobufVarint32LengthFieldPrepender());
pipeline.addLast(new ProtobufEncoder());
pipeline.addLast(new PersonClientHandler());
}
}
2
3
4
5
6
7
8
9
10
11
【3】自定义 PersonClientHandler
的内容如下:
public class PersonClientHandler extends SimpleChannelInboundHandler {
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, Object msg) throws Exception {
System.out.printf(String.valueOf(channelHandlerContext.channel().remoteAddress()));
System.out.printf("客户端收到的消息: "+"\r\n" + msg);
}
@Override
public void channelActive(ChannelHandlerContext channelHandlerContext){
AddressBookProtos.Person person = AddressBookProtos.Person.newBuilder().setId(1)
.setName("zhengzhaoxiang")
.setEmail("1179278531@qq.com").build();
channelHandlerContext.channel().writeAndFlush(person);
}
@Override
public void exceptionCaught(ChannelHandlerContext channelHandlerContext,Throwable e){
e.printStackTrace();
channelHandlerContext.close();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 六、测试
启动服务端——>启动客户端,运行结果如下:
【1】服务端结果展示:
/127.0.0.1:57595服务端收到的消息 name: "zhengzhaoxiang"
id: 1
email: "1179278531qq.com"
2
3
【2】客户端结果展示:
Connected to the target VM, address: '127.0.0.1:57572', transport: 'socket'
/127.0.0.1:8899客户端收到的消息:
来着服务端的问候:Active
/127.0.0.1:8899客户端收到的消息:
from client2019-09-21T19:20:14.977
2
3
4
5
# 七、问题
当
.proto
中存在多个message
时,在解码ProtobufDecode
(目标对象)中,添加的目标对象不唯一,会根据情况进行变化的问题及解决方案。
【1】.proto
文件内容如下:包含多个 message
对象。oneof
关键字表示:多个可选项,但允许选择一个。设置的新值会替换掉旧值。
syntax = "proto2";
package tutorial;
option java_package = "com.protobuf";
option java_outer_classname = "AddressBookProtos";
message myMessage {
enum data {
personType = 1;
dogType = 2;
pigType = 3;
}
required string type = 1;
oneof zoo {
Person person = 2;
Dog dog = 3;
Pig pig =4;
}
}
message Person {
optional string name = 1;
optional int32 id = 2;
optional string email = 3;
}
message Dog {
optional string name = 1;
}
message Pig {
optional string name = 1;
optional int32 price = 2;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
【2】编辑码出的问题,便可以修改为最外层的 myMessage
对象,服务端解码设置如下:
pipeline.addLast(new ProtobufDecoder(AddressBookProtos.myMessage.getDefaultInstance()));
【3】客户端发送发送消息,内容如下:需要什么对象,就往 oneof
中传入目标对象即可。
@Override
public void channelActive(ChannelHandlerContext channelHandlerContext){
int random = new Random().nextInt(3);
AddressBookProtos.myMessage message = null;
if(random == AddressBookProtos.myMessage.data.personType_VALUE){
message = AddressBookProtos.myMessage.newBuilder()
.setType("1").setPerson(AddressBookProtos.Person.newBuilder()
.setId(1).setName("zheng").setEmail("117278531@qq.com").build()).build();
}else if(random == AddressBookProtos.myMessage.data.dogType_VALUE){
message = AddressBookProtos.myMessage.newBuilder()
.setType("2").setDog(AddressBookProtos.Dog.newBuilder()
.setName("一条狗").build()).build();
}else{
message = AddressBookProtos.myMessage.newBuilder()
.setType("3").setPig(AddressBookProtos.Pig.newBuilder()
.setName("一只猪").setPrice(20).build()).build();
}
channelHandlerContext.channel().writeAndFlush(message);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
【4】服务端接受客户端的消息,根据 type
的值判断需要解析的数据信息,具体内容如下:
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, Object msg) throws Exception {
AddressBookProtos.myMessage message = (AddressBookProtos.myMessage)msg;
if(Integer.valueOf(message.getType()) == (AddressBookProtos.myMessage.data.personType_VALUE)){
System.out.printf("服务端收到的消息 "+message.getPerson().toString());
}else if(Integer.valueOf(message.getType()) == (AddressBookProtos.myMessage.data.dogType_VALUE)){
System.out.printf("服务端收到的消息 "+message.getDog().getName());
}else{
System.out.printf("服务端收到的消息 "+message.getPig().getName()+"\r\n"+message.getPig().getPrice());
}
}
2
3
4
5
6
7
8
9
10
11
【5】不断重启客户端,会根据随机数得到不同的结果,如下:
//第一次输入结果展示:
/*服务端收到的消息 name: "zheng"
id: 1
email: "117278531@qq.com"*/
//第三次输入结果展示:
/*服务端收到的消息 一条狗*/
//第四次输入结果展示:
/*服务端收到的消息 一只猪
20*/
2
3
4
5
6
7
8
9
10
11