快速上手ProtoBuf

目录:

  1. 需求:
  2. 引⼊ProtoBuf包
  3. 创建.proto⽂件
  4. 编译contacts.proto⽂件,⽣成JAVA⽂件
  5. 编译contacts.proto⽂件后会⽣成什么
  6. 序列化与反序列化的使⽤
  7. ⼩结ProtoBuf使⽤流程

1.需求:

在快速上手中,会编写第一版本的通讯录1.0。在通讯录1.0版本中,将实现:·

  • 对一个联系人的信息使用PB进行序列化,并将结果打印出来。
  • 对序列化后的内容使用PB进行反序列,解析出联系人信息并打印出来。·
  • 联系人包含以下信息:姓名、年龄。

通过通讯录1.0,我们便能了解使用ProtoBuf初步要掌握的内容,以及体验到ProtoBuf的完整使用流程。(注意需要创建maven项目)

2.引⼊ProtoBuf包

<!-- protobuf ⽀持 Java 核⼼包 -->
        <dependency>
            <groupId>com.google.protobuf</groupId>
            <artifactId>protobuf-java</artifactId>
            <version>3.21.11</version>
        </dependency>

3.创建.proto⽂件

将新建的.proto⽂件统⼀放在项⽬中的 /src/main/proto ⽬录下。

⽂件规范:

  • 创建.proto⽂件时,⽂件命名应该使⽤全⼩写字⺟命名,多个字⺟之间⽤ _ 连接。例如:lower_snake_case.proto 。
  • 书写.proto⽂件代码时,应使⽤2个空格的缩进。
  • 我们为通讯录1.0在 /src/main/proto/start ⽬录下新建⽂件: contacts.proto

添加注释:

  • 向⽂件添加注释,可使⽤ // 或者 /* ... */
     

指定proto3语法:

ProtocolBuffers语⾔版本3,简称proto3,是.proto⽂件最新的语法版本。proto3简化了Protocol
Buffers语⾔,既易于使⽤,⼜可以在更⼴泛的编程语⾔中使⽤。它允许你使⽤Java,C++,Python等多种语⾔⽣成protocolbuffer代码。在.proto⽂件中,要使⽤syntax = "proto3"; 来指定⽂件语法为proto3,并且必须写在除去注释内容的第⼀⾏。如果没有指定,编译器会使⽤proto2语法。
在通讯录1.0的contacts.proto⽂件中,可以为⽂件指定proto3语法,内容如下:
 

syntax = "proto3";

package声明符:
package是⼀个可选的声明符,能表⽰.proto⽂件的命名空间,在项⽬中要有唯⼀性。它的作⽤是为了避免我们定义的消息出现冲突。在通讯录1.0的contacts.proto⽂件中,可以声明其命名空间,内容如下:

syntax = "proto3";
package start;

添加JAVA选项:
.proto⽂件中可以声明许多选项,使⽤ option 标注。选项能影响proto编译器的某些处理⽅式。
这这个部分,对于选项能⼒不进⾏展开,后⾯学习proto3语法详解部分,会再介绍。
在通讯录1.0的contacts.proto⽂件中,新增JAVA选项,内容如下:

syntax = "proto3";
package start;

option java_multiple_files = true; // 编译后⽣成的⽂件是否分为多个⽂件
option java_package = "com.example.start"; // 编译后⽣成⽂件所在的包路径
option java_outer_classname="ContactsProtos"; // 编译后⽣成的proto包装类的类名

定义消息(message):

  • 消息(message):要定义的结构化对象,我们可以给这个结构化对象中定义其对应的属性内容。这⾥再提⼀下为什么要定义消息?
  • 在⽹络传输中,我们需要为传输双⽅定制协议。定制协议说⽩了就是定义结构体或者结构化数据,⽐如,tcp,udp报⽂就是结构化的。
  • 再⽐如将数据持久化存储到数据库时,会将⼀系列元数据统⼀⽤对象组织起来,再进⾏存储。
  • 所以ProtoBuf就是以message的⽅式来⽀持我们定制协议字段,后期帮助我们形成类和⽅法来使⽤。在通讯录1.0中我们就需要为联系⼈定义⼀个message。

.proto⽂件中定义⼀个消息类型的格式为:

message 消息类型名{
    
}
//    消息类型命名规范:使⽤驼峰命名法,⾸字⺟⼤写。

 为contacts.proto(通讯录1.0)新增联系⼈message,内容如下:

syntax = "proto3";
package start;

option java_multiple_files = true; // 编译后⽣成的⽂件是否分为多个⽂件
option java_package = "com.example.start"; // 编译后⽣成⽂件所在的包路径
option java_outer_classname = "ContactsProtos"; // 编译后⽣成的proto包装类的类名

message PeopleInfo{

}

定义消息字段:

  • 在message中我们可以定义其属性字段,字段定义格式为:字段类型字段名=字段唯⼀编号;
  • 字段名称命名规范:全⼩写字⺟,多个字⺟之间⽤ _ 连接。
  • 字段类型分为:标量数据类型和特殊类型(包括枚举、其他消息类型等)。
  • 字段唯⼀编号:⽤来标识字段,⼀旦开始使⽤就不能够再改变。
  • 该表格展⽰了定义于消息体中的标量数据类型,以及编译.proto⽂件之后⾃动⽣成的类中与之对应的字段类型。在这⾥展⽰了与JAVA语⾔对应的类型。
.proto TypeNotesJava Type
doubledouble
floatfloat
int32

使用变长编码[1]。负数的编码效率较低

——若字段可能为负值,应使用sint32代替。

int
int64

使用变长编码[1]。负数的编码效率较低

——若字段可能为负值,应使用sint64代替。

long
uint32使用变长编码[1]。int[2]
uint64使用变长编码[1]。long[2]
sint32

使用变长编码[1]。符号整型。负值的

编码效率高于常规的int32类型。

int
sint64

使用变长编码[1]。符号整型。负值的

编码效率高于常规的int64类型。

long
fixed32

定长4字节。若值常大于2^28则会比

uint32更高效。

int
fixed64

定长8字节。若值常大于2N56则会比

uint64更高效。

long
sfixed32定长4字节。int
sfixed64定长8字节。long
boolboolean
string包含UTF-8和ASCII编码的字符串,长度不能超过
2^32。
String
bytes可包含任意的字节序列但长度不能超过2^32。

ByteString

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 变⻓编码是指:经过protobuf编码后,原本4字节或8字节的数可能会被变为其他字节数。
  • 在Java中,⽆符号32位和⽆符号64位整数使⽤它们对应的有符号整数来表⽰,这时第⼀个bit位仅是简单地存储在符号位中。
     

更新contacts.proto(通讯录1.0),新增姓名、年龄字段:

syntax = "proto3";
package start;

option java_multiple_files = true; // 编译后⽣成的⽂件是否分为多个⽂件
option java_package = "com.example.start"; // 编译后⽣成⽂件所在的包路径
option java_outer_classname = "ContactsProtos"; // 编译后⽣成的proto包装类的类名

message PeopleInfo{
  string name = 1;
  int32 age = 2;
}
  • 在这⾥还要特别讲解⼀下字段唯⼀编号的范围:
  • 1~536,870,911(2^29-1),其中19000~19999不可⽤。
  • 19000~19999不可⽤是因为:在Protobuf协议的实现中,对这些数进⾏了预留。如果⾮要在.proto⽂件中使⽤这些预留标识号,例如将name字段的编号设置为19000,编译时就会报警:
// 消息中定义了如下编号,代码会告警:
// Field numbers 19,000 through 19,999 are reserved for the protobuf
//    implementation
    string name = 19000;

值得⼀提的是,范围为1~15的字段编号需要⼀个字节进⾏编码,16~2047内的数字需要两个字节
进⾏编码。编码后的字节不仅只包含了编号,还包含了字段类型。所以1~15要⽤来标记出现⾮常频繁的字段,要为将来有可能添加的、频繁出现的字段预留⼀些出来。


4.编译contacts.proto⽂件,⽣成JAVA⽂件

编译的⽅式有两种:使⽤命令⾏编译;使⽤maven插件编译。

⽅式⼀:使⽤命令⾏编译,编译命令⾏格式为:

    protoc [--proto_path=IMPORT_PATH] --java_out=DST_DIR path/to/file.proto

    protoc 是 Protocol Buffer 提供的命令⾏编译⼯具。

    --proto_path 指定 被编译的.proto⽂件所在⽬录,可多次指定。可简写成 -I

    IMPORT_PATH 。如不指定该参数,则在当前⽬录进⾏搜索。当某个.proto ⽂件 import 其他
    .proto ⽂件时,或需要编译的 .proto ⽂件不在当前⽬录下,这时就要⽤-I来指定搜索⽬录。

    --java_out= 指编译后的⽂件为 JAVA ⽂件。

    OUT_DIR 编译后⽣成⽂件的⽬标路径。
    path/to/file.proto 要编译的.proto⽂件。

编译contacts.proto⽂件命令如下:

protoc -I src\main\proto\start\ --java_out=src\main\java contacts.proto

运行结果:

 

⽅式⼆:使⽤maven插件编译

<build>
        <plugins>
            <plugin>
                <groupId>org.xolstice.maven.plugins</groupId>
                <artifactId>protobuf-maven-plugin</artifactId>
                <version>0.6.1</version>
                <configuration>
                    <!-- 本地安装的protoc.exe的⽬录 -->
                    <protocExecutable>
                        E:\softwares_tl\protobuf_tl\protoc-21.11-win64\bin\protoc.exe
                    </protocExecutable>
                    <!-- proto⽂件放置的⽬录,默认为/src/main/proto -->
                    <protoSourceRoot>
                        ${project.basedir}/src/main/proto
                    </protoSourceRoot>
                    <!-- ⽣成⽂件的⽬录,默认⽣成到target/generated-sources/protobuf/ -->
                    <outputDirectory>
                        ${project.basedir}/src/main/java
                    </outputDirectory>
                    <!-- 是否清空⽬标⽬录,默认值为true。这个最好设置为false,以免误删项⽬⽂件!!! -->
                    <clearOutputDirectory>
                        false
                    </clearOutputDirectory>
                </configuration>
            </plugin>
        </plugins>
    </build>

运行结果:

5.编译contacts.proto⽂件后会⽣成什么

  • 编译contacts.proto⽂件后,会⽣成所选择语⾔的代码,我们选择的是JAVA,编译后⽣成了三个⽂件: ContactsProtos.java PeopleInfo.java PeopleInfoOrBuilder.java 。如果在contacts.proto⽂件中不设置option java_multiple_files = true; ,或将其置为false ,则会把这三个⽂件合成为⼀个ContactsProtos.java ⽂件。

对于编译⽣成的JAVA代码,我们主要关注 PeopleInfo.java ,其内容为:

  • 在.proto⽂件中定义的每⼀个message,都会⽣成⼀个⾃⼰的⾃定义 message 类,每⼀个⾃定义 message 类还有⼀个内部 Builder 类。 Builder 类的作⽤就是可以创建 message 类的实例。
  • 在message 类中,主要包含:
    •  获取字段值的get⽅法,⽽没有set⽅法。
    • 序列化和反序列化⽅法。
    • newBuilder()静态⽅法:⽤来创建Builder。
  • 在 Builder 类中,主要包含:
    • 编译器为每个字段提供了获取和设置⽅法,以及能够操作字段的⼀些⽅法。
    • build()⽅法:主要是⽤来构造出⼀个⾃定义类对象。

PeopleInfo.java中PeopleInfo类部分代码展⽰(为了简洁,⽅法省略了具体实现):

上述的例⼦中:

  • 获取字段值的get⽅法,⽽没有set⽅法。
  • parseFrom()系列静态⽅法提供了反序列化消息对象的能⼒。
  • newBuilder()静态⽅法:⽤来创建Builder。

PeopleInfo.java中Builder内部类部分代码展⽰(为了简洁,⽅法省略了具体实现):


 

上述的例⼦中:

  • 包含⼀个build()⽅法:主要是⽤来构造出⼀个⾃定义类对象。
  • 每个字段都有set设置和get获取的⽅法。
  • 每个字段都有⼀个clear⽅法,可以将字段重新设置回empty状态。

那之前提到的序列化⽅法在哪⾥呢?其实在⾃定义消息类继承的接⼝MessageLite 中,提供了序列化消息实例的⽅法。

注意:

  • 序列化的结果为⼆进制字节序列,⽽⾮⽂本格式。
  • 以上两种序列化的⽅法没有本质上的区别,只是序列化后输出的格式不同,可以供不同的应⽤场景使⽤。

 6.序列化与反序列化的使⽤

创建⼀个测试⽂件FastStart.java,⽅法中我们实现:

  • 对⼀个联系⼈的信息使⽤PB进⾏序列化,并将结果打印出来。
  • 对序列化后的内容使⽤PB进⾏反序列,解析出联系⼈信息并打印出来。

FastStart.java

package com.example.start;

import com.google.protobuf.InvalidProtocolBufferException;

import java.util.Arrays;

public class FastStart {
    public static void main(String[] args) throws InvalidProtocolBufferException {
        PeopleInfo p1 = PeopleInfo.newBuilder().setName("张三").setAge(20).build();

        //序列化
        byte[] bytes = p1.toByteArray();
        System.out.println("序列化结果为:");
        System.out.println(Arrays.toString(bytes));

        //反序列化
        PeopleInfo p2 = PeopleInfo.parseFrom(bytes);

        System.out.println("反序列化结果为:");
        System.out.println("姓名:" + p2.getName() + " 年龄:" + p2.getAge());
    }
}

运行结果:


 

  • ProtoBuf是把联系⼈对象序列化成了⼆进制序列,这⾥⽤byte[]来作为接收⼆进制序列的容器,帮助我们看到序列化后的结果。
  • 所以相对于xml和JSON来说,因为被编码成⼆进制,破解成本增⼤,ProtoBuf编码是相对安全的。

7.⼩结ProtoBuf使⽤流程


 

  • 编写.proto⽂件,⽬的是为了定义结构对象(message)及属性内容。
  • 使⽤protoc编译器编译.proto⽂件,⽣成⼀系列接⼝代码。
  • 依赖⽣成的接⼝,实现对.proto⽂件中定义的字段进⾏设置和获取,和对message对象进⾏序列化和反序列化。
  • 总的来说:ProtoBuf是需要依赖通过编译⽣成的JAVA代码来使⽤的。有了这种代码⽣成机制,开发⼈员再也不⽤吭哧吭哧地编写那些协议解析的代码了(⼲这种活是典型的吃⼒不讨好)。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿瞒有我良计15

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值