Google Protobuf 编解码

更多内容,前往个人博客

Protobuf 全称:Google 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 版本:网站地址如下:https://github.com/protocolbuffers/protobuf/releases/tag/v3.9.1
 
  下载后对其解压:进入包含 protoc.exe 的文件目录,配置其环境变量;protoc.exe 工具主要根据 .proto 文件生成代码。

官网对 java 编写 .proto 文件,详细说明地址:https://developers.google.cn/protocol-buffers/docs/javatutorial

  下面我们定义一个 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】通过 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 包,如下:

1 <dependency>
2     <groupId>com.google.protobuf</groupId>
3     <artifactId>protobuf-java</artifactId>
4     <version>3.9.1</version>
5 </dependency>

到此为止,Protobuf 开发环境已经搭建完毕,接下来进行示例展示。

三、Protobuf 编解码开发


Protobuf 的类库使用比较简单,下面通过对 AddressBookProtos 编解码来介绍 Protobuf 的使用:由于 Protobuf 支持复杂 POJO 对象编解码,所以代码都是通过工具自动生成,相比于传统的 POJO 对象的赋值操作,其使用略微复杂一些。Protobuf 的编解码接口非常简单和实用,但是功能和性能却非常强大,这也是它流行的一个重要原因。

 1 public class TestAddressBookProtos {
 2     public static void main(String[] args) throws InvalidProtocolBufferException {
 3         AddressBookProtos.Person person = createSubscribeReq();
 4         /*
 5          * After decode:name: "ZhengZhaoXiang"
 6          * id: 1
 7          * email: "1179278531@qq.com"
 8          */
 9         System.out.printf("Before encode :"+person.toString());
10         AddressBookProtos.Person personObj = decode(encode(person));
11         /*
12          * After decode:name: "ZhengZhaoXiang"
13          * id: 1
14          * email: "1179278531@qq.com"
15          */
16         System.out.printf("After decode:"+person.toString());
17         //输出: Assert equal:true
18         System.out.printf("Assert equal:"+person.equals(personObj));
19     }
20 
21     //编码 通过调用 AddressBookProtos.Person 实例的 toByteArray 即可将 Person 编码为 byte 数组。
22     private static byte[] encode(AddressBookProtos.Person person){
23         return person.toByteArray();
24     }
25 
26     //解码  还可以解码流数据  parseFrom(InputStream i);
27     private static AddressBookProtos.Person decode(byte[] body) throws InvalidProtocolBufferException {
28         return AddressBookProtos.Person.parseFrom(body);
29     }
30 
31     //创建一个 person 对象
32     private static AddressBookProtos.Person createSubscribeReq(){
33         // 通过 AddressBookProtos.Person 的 newBuilder() 静态方法创建 Builder 实例
34         // 通过 Builder 构建器对 Person 的属性进行设置,对于集合类型,通过addAllXXX()方法将值设置到属性中。
35         return AddressBookProtos.Person.newBuilder()
36                 .setId(1).setName("ZhengZhaoXiang").setEmail("1179278531@qq.com").build();
37     }
38 }

四、Netty 的 Protobuf 服务端开发


【1】标准的服务端:主要区别在于 childHandler 方法中的 PersonChannelInitializer 类的内容。

 1 public class PersonServer {
 2     public static void main(String[] args) throws Exception{
 3         EventLoopGroup bossGroup = new NioEventLoopGroup();
 4         EventLoopGroup workerGroup = new NioEventLoopGroup();
 5         try {
 6             ServerBootstrap bootstrap = new ServerBootstrap();
 7             bootstrap.group(bossGroup,workerGroup).channel(NioServerSocketChannel.class)
 8                     .handler(new LoggingHandler(LogLevel.INFO))
 9                     //主要查看 PersonChannelInitializer 内容
10                     .childHandler(new PersonChannelInitializer());
11             ChannelFuture future = bootstrap.bind(8899).sync();
12             future.channel().closeFuture().sync();
13         }finally {
14             bossGroup.shutdownGracefully();
15             workerGroup.shutdownGracefully();
16         }
17     }
18 }

【2】PersonChannelInitializer 内容展示:重点关注自定义 handler(PersonHandler)

 1 public class PersonChannelInitializer extends ChannelInitializer{
 2     @Override
 3     protected void initChannel(Channel channel) throws Exception {
 4         ChannelPipeline pipeline = channel.pipeline();
 5         //主要用于半包处理
 6         pipeline.addLast(new ProtobufVarint32FrameDecoder());
 7         //解码器,参数 com.google.protobuf.MessageLite 实际上是告诉 ProtobufDecoder 解码的目标类
 8         pipeline.addLast(new ProtobufDecoder(AddressBookProtos.Person.getDefaultInstance()));
 9         pipeline.addLast(new ProtobufVarint32LengthFieldPrepender());
10         pipeline.addLast(new StringEncoder());
11         //自定义handler
12         pipeline.addLast(new PersonHandler());
13     }
14 }

【3】自定义 PersonHandler 的内容如下:由于 ProtobufDecoder  已经对消息进行了自动解码,因此接收到的 Person 消息可以直接使用。对用户进行校验,校验通过后构造应答消息返回给客户端,由于使用了 StringEncoder 因此不需要手工编码。

 1 public class PersonHandler extends SimpleChannelInboundHandler {
 2 
 3     @Override
 4     protected void channelRead0(ChannelHandlerContext channelHandlerContext, Object msg) throws Exception {
 5         AddressBookProtos.Person person = (AddressBookProtos.Person)msg;
 6         System.out.printf(String.valueOf(channelHandlerContext.channel().remoteAddress()));
 7         System.out.printf("服务端收到的消息    "+person);
 8         channelHandlerContext.writeAndFlush("from client"+ LocalDateTime.now());
 9     }
10 
11     @Override
12     public void channelActive(ChannelHandlerContext channelHandlerContext){
13         channelHandlerContext.writeAndFlush("来着服务端的问候:Active"+"\r\n");
14     }
15 
16     @Override
17     public void exceptionCaught(ChannelHandlerContext channelHandlerContext,Throwable e){
18         e.printStackTrace();
19         channelHandlerContext.close();
20     }
21 }

五、Netty 的 Protobuf 客户端开发


【1】客户端:主要区别在于 childHandler 方法中的 PersonClientInitializer 类的内容。

 1 public class PersonClient {
 2     public static void main(String[] args) throws Exception{
 3         EventLoopGroup workerGroup = new NioEventLoopGroup();
 4         try {
 5             Bootstrap bootstrap = new Bootstrap();
 6             bootstrap.group(workerGroup).channel(NioSocketChannel.class)
 7                     .handler(new PersonClientInitializer());
 8             ChannelFuture future = bootstrap.connect("127.0.0.1",8899).sync();
 9             future.channel().closeFuture().sync();
10         }finally {
11             workerGroup.shutdownGracefully();
12         }
13     }
14 }

【2】PersonClientInitializer 内容展示:重点关注自定义 handler(PersonClientHandler)

 1 public class PersonClientInitializer extends ChannelInitializer{
 2     @Override
 3     protected void initChannel(Channel channel) throws Exception {
 4         ChannelPipeline pipeline = channel.pipeline();
 5         pipeline.addLast(new ProtobufVarint32FrameDecoder());
 6         pipeline.addLast(new StringDecoder());
 7         pipeline.addLast(new ProtobufVarint32LengthFieldPrepender());
 8         pipeline.addLast(new ProtobufEncoder());
 9         pipeline.addLast(new PersonClientHandler());
10     }
11 }

【3】自定义 PersonClientHandler 的内容如下:

 1 public class PersonClientHandler extends SimpleChannelInboundHandler {
 2 
 3     @Override
 4     protected void channelRead0(ChannelHandlerContext channelHandlerContext, Object msg) throws Exception {
 5         System.out.printf(String.valueOf(channelHandlerContext.channel().remoteAddress()));
 6         System.out.printf("客户端收到的消息:   "+"\r\n" + msg);
 7     }
 8 
 9     @Override
10     public void channelActive(ChannelHandlerContext channelHandlerContext){
11         AddressBookProtos.Person person = AddressBookProtos.Person.newBuilder().setId(1)
12                 .setName("zhengzhaoxiang")
13                 .setEmail("1179278531@qq.com").build();
14         channelHandlerContext.channel().writeAndFlush(person);
15     }
16 
17     @Override
18     public void exceptionCaught(ChannelHandlerContext channelHandlerContext,Throwable e){
19         e.printStackTrace();
20         channelHandlerContext.close();
21     }
22 }

六、测试


  启动服务端——>启动客户端,运行结果如下:
【1】服务端结果展示:
 

【2】客户端结果展示:
 

七、问题


当 .proto 中存在多个 message 时,在解码 ProtobufDecode(目标对象)中,添加的目标对象不唯一,会根据情况进行变化的问题及解决方案。

【1】.proto 文件内容如下:包含多个 message 对象。oneof 关键字表示:多个可选项,但允许选择一个。设置的新值会替换掉旧值。

 1 syntax = "proto2";
 2 
 3 package tutorial;
 4 
 5 option java_package = "com.protobuf";
 6 option java_outer_classname = "AddressBookProtos";
 7 
 8 message myMessage {
 9   enum data {
10     personType = 1;
11     dogType = 2;
12     pigType = 3;
13   }
14   
15   required string type = 1;
16   oneof zoo {
17     Person person = 2;
18     Dog dog = 3;
19     Pig pig =4;
20   }
21 }
22 
23 message Person {
24   optional string name = 1;
25   optional int32 id = 2;
26   optional string email = 3;
27 }
28 
29 message Dog {
30   optional string name = 1;
31 }
32 
33 message Pig {
34   optional string name = 1;
35   optional int32 price = 2;
36 }

【2】编辑码出的问题,便可以修改为最外层的 myMessage 对象,服务端解码设置如下:

pipeline.addLast(new ProtobufDecoder(AddressBookProtos.myMessage.getDefaultInstance()));

【3】客户端发送发送消息,内容如下:需要什么对象,就往 oneof 中传入目标对象即可。

 1 @Override
 2 public void channelActive(ChannelHandlerContext channelHandlerContext){
 3     int random = new Random().nextInt(3);
 4     AddressBookProtos.myMessage message = null;
 5     if(random == AddressBookProtos.myMessage.data.personType_VALUE){
 6         message = AddressBookProtos.myMessage.newBuilder()
 7                 .setType("1").setPerson(AddressBookProtos.Person.newBuilder()
 8                 .setId(1).setName("zheng").setEmail("117278531@qq.com").build()).build();
 9     }else if(random == AddressBookProtos.myMessage.data.dogType_VALUE){
10         message = AddressBookProtos.myMessage.newBuilder()
11                 .setType("2").setDog(AddressBookProtos.Dog.newBuilder()
12                         .setName("一条狗").build()).build();
13     }else{
14         message = AddressBookProtos.myMessage.newBuilder()
15                 .setType("3").setPig(AddressBookProtos.Pig.newBuilder()
16                         .setName("一只猪").setPrice(20).build()).build();
17     }
18     channelHandlerContext.channel().writeAndFlush(message);
19 }

【4】服务端接受客户端的消息,根据 type 的值判断需要解析的数据信息,具体内容如下:

 1 @Override
 2 protected void channelRead0(ChannelHandlerContext channelHandlerContext, Object msg) throws Exception {
 3     AddressBookProtos.myMessage message = (AddressBookProtos.myMessage)msg;
 4     if(Integer.valueOf(message.getType()) == (AddressBookProtos.myMessage.data.personType_VALUE)){
 5         System.out.printf("服务端收到的消息    "+message.getPerson().toString());
 6     }else if(Integer.valueOf(message.getType()) == (AddressBookProtos.myMessage.data.dogType_VALUE)){
 7         System.out.printf("服务端收到的消息    "+message.getDog().getName());
 8     }else{
 9         System.out.printf("服务端收到的消息    "+message.getPig().getName()+"\r\n"+message.getPig().getPrice());
10     }
11 }

【5】不断重启客户端,会根据随机数得到不同的结果,如下:

 1 //第一次输入结果展示:
 2 /*服务端收到的消息    name: "zheng"
 3 id: 1
 4 email: "117278531@qq.com"*/
 5 
 6 //第三次输入结果展示:
 7 /*服务端收到的消息    一条狗*/
 8 
 9 //第四次输入结果展示:
10 /*服务端收到的消息    一只猪
11 20*/
posted @ 2020-11-19 17:17  Java程序员进阶  阅读(1349)  评论(0编辑  收藏  举报