目录:
- 需求:
- 引⼊ProtoBuf包
- 创建.proto⽂件
- 编译contacts.proto⽂件,⽣成JAVA⽂件
- 编译contacts.proto⽂件后会⽣成什么
- 序列化与反序列化的使⽤
- ⼩结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 Type | Notes | Java Type |
double | double | |
float | float | |
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 |
bool | boolean | |
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代码来使⽤的。有了这种代码⽣成机制,开发⼈员再也不⽤吭哧吭哧地编写那些协议解析的代码了(⼲这种活是典型的吃⼒不讨好)。