Google Protobuf 编解码
阅读原文时间:2023年07月08日阅读:2

更多内容,前往个人博客

Protobuf 全称:Google Protocol Buffers,由谷歌开源而来,经谷歌内部测试使用。它将数据结构以 .proto 文件进行描述,通过代码生成工具可以生成对应数据结构的 POJO 对象和 Protobuf 相关的方法和属性。


【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 做编解码框架的原因。


【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 2 com.google.protobuf 3 protobuf-java 4 3.9.1 5

到此为止,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 }


【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 }


【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*/

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器

你可能感兴趣的文章