proto的介绍和基础使用

2年前 (2022) 程序员胖胖胖虎阿
301 0 0

内容摘抄自书籍《Netty redis zookeeper高并发实战》

Protobuf使用

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消息结构体,分别写入不同的文件,这样便于管理。

Maven插件生成POJO和Builder

使用命令行生成Java类的操作比较烦琐。另一种更加方便的方式是:使用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:Java代码生成工具的protoc3.6.1.exe可执行文件的路径。
配置好之后,执行插件的compile命令,Java代码就利索生成了。或者在Maven的项目编译时,POJO类和Builder类也会自动生成。
proto的介绍和基础使用

1.使用Builder构造者,构造POJO消息对象

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消息对象。

2.序列化serialization & 反序列化Deserialization的方式

一 获得消息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新的实例。

3.序列化serialization & 反序列化Deserialization的方式二

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应用场景中,存在着粘包/半包的问题。

4.序列化serialization &反序列化Deserialization的方式三

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应用场景中,解决了粘包/半包的问题。

版权声明:程序员胖胖胖虎阿 发表于 2022年10月1日 下午11:00。
转载请注明:proto的介绍和基础使用 | 胖虎的工具箱-编程导航

相关文章

暂无评论

暂无评论...