关于 ProtoBuf 的含义和安装推荐看:ProtoBuf 的含义和安装
步骤0:引⼊ ProtoBuf 包
<!-- protobuf ⽀持 Java 核⼼包 -->
<dependency><groupId>com.google.protobuf</groupId><artifactId>protobuf-java</artifactId><version>${protobuf_version}</version>
</dependency>
对于${protobuf_version} ,和我们在安装 PB 时的版本保持⼀致,例如 3.21.11 。可以通过下面的命令查看 ProtoBuf 的版本
protoc --version
步骤1:创建 .proto ⽂件
将新建的 .proto ⽂件统⼀放在项⽬中的 /src/main/proto ⽬录下。
⽂件规范
contacts.proto 文件示例
// 首行: 语法指定行
syntax = "proto3";
package start;option java_multiple_files = true;
option java_package = "com.example.start";
option java_outer_classname = "ContactsProtos";// 定义联系人 message
message PeopleInfo {// 字段类型 字段名 = 字段唯一编号;string name = 1;int32 age = 2;
}
• 创建 .proto ⽂件时,⽂件命名应该使⽤全⼩写字⺟命名,多个字⺟之间⽤ _ 连接。例如: lower_snake_case.proto 。
• 书写 .proto ⽂件代码时,应使⽤ 2 个空格的缩进。
添加注释 :向⽂件添加注释,可使⽤ // 或者 /* ... */
指定 proto3 语法
Protocol Buffers 语⾔版本3,简称 proto3,是 .proto ⽂件最新的语法版本。proto3 简化了 Protocol Buffers 语⾔,既易于使⽤,⼜可以在更⼴泛的编程语⾔中使⽤。它允许你使⽤ Java,C++,Python 等多种语⾔⽣成 protocol buffer 代码。 在 .proto ⽂件中,要使⽤ syntax = "proto3"; 来指定⽂件语法为\ \proto3,并且必须写在除去注释内容的第⼀⾏。如果没有指定,编译器会使⽤ proto2 语法。
syntax = "proto3";
package 声明符
package 是⼀个可选的声明符,能表⽰ .proto ⽂件的命名空间,在项⽬中要有唯⼀性。它的作⽤是为 了避免我们定义的消息出现冲突。
package start;
添加 JAVA 选项
.proto ⽂件中可以声明许多选项,使⽤ option 标注。选项能影响 proto 编译器的某些处理⽅式:
option java_multiple_files = true; // 编译后⽣成的⽂件是否分为多个⽂件
option java_package = "com.example.start"; // 编译后⽣成⽂件所在的包路径
option java_outer_classname="ContactsProtos"; // 编译后⽣成的proto包装类的类名
定义消息(message)
消息(message):要定义的结构化对象,我们可以给这个结构化对象中定义其对应的属性内容。这⾥再提⼀下为什么要定义消息? 在⽹络传输中,我们需要为传输双⽅定制协议。定制协议说⽩了就是定义结构体或者结构化数据, ⽐如,tcp,udp 报⽂就是结构化的。再⽐如将数据持久化存储到数据库时,会将⼀系列元数据统⼀⽤对象组织起来,再进⾏存储。
所以 ProtoBuf 就是以 message 的⽅式来⽀持我们定制协议字段,后期帮助我们形成类和⽅法来使⽤。
.proto ⽂件中定义⼀个消息类型的格式为:
message 消息类型名{}
消息类型命名规范:使⽤驼峰命名法,⾸字⺟⼤写。
定义消息字段
在 message 中我们可以定义其属性字段,字段定义格式为:字段类型 字段名 = 字段唯⼀编号;
• 字段名称命名规范:全⼩写字⺟,多个字⺟之间⽤ _ 连接。
• 字段类型分为:标量数据类型 和 特殊类型(包括枚举、其他消息类型等)。
• 字段唯⼀编号:⽤来标识字段,⼀旦开始使⽤就不能够再改变。 该表格展⽰了定义于消息体中的标量数据类型,以及编译 .proto ⽂件之后⾃动⽣成的类中与之对应的字段类型。在这⾥展⽰了与 JAVA 语⾔对应的类型。
[1]变⻓编码是指:经过 protobuf 编码后,原本 4 字节或 8 字节的数可能会被变为其他字节数。
[2] 在 Java 中,⽆符号 32 位和⽆符号 64 位整数使⽤它们对应的有符号整数来表示,这时第⼀个 bit 位仅是简单地存储在符号位中。
在这⾥还要特别讲解⼀下字段唯⼀编号的范围: 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 要⽤来标记出现⾮常频繁的字段,要为将来有可能添加的、频繁出现的字段预留⼀些出来。
步骤2:编译 contacts.proto ⽂件
⽣成 JAVA ⽂件编译的⽅式有两种:
1.使⽤命令⾏编译;
2.使⽤ 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 插件编译
这种编译⽅式⽐⼿动执⾏ protoc 命令,后⾯跟⼀堆易忘的参数要⾼效省⼼得多(每次编译都得 google 或找之前记的笔记)。 在 pom 中添加 porotbuf 编译插件:
<build><plugins><plugin><groupId>org.xolstice.maven.plugins</groupId><artifactId>protobuf-maven-plugin</artifactId><version>0.6.1</version><configuration><!-- 本地安装的protoc.exe的目录 --><protocExecutable>D:\Software\ProtoBuf\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>
根据以下图⽰步骤,⼀键即可完成编译:
编译 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 类部分代码展⽰(为了简洁,⽅法省略了具体实现):
public final class PeopleInfo extendscom.google.protobuf.GeneratedMessageV3 implementsPeopleInfoOrBuilder {// ------------------------------- get ⽅法 ----------------------------------
---@java.lang.Overridepublic java.lang.String getName() {...}@java.lang.Overridepublic com.google.protobuf.ByteString getNameBytes() {...}@java.lang.Overridepublic int getAge() {...}// -------------------------------反序列化⽅法 --------------------------------
---public static com.example.start.PeopleInfo parseFrom(byte[] data)throws com.google.protobuf.InvalidProtocolBufferException {...}public static com.example.start.PeopleInfo parseFrom(java.io.InputStream
input)throws java.io.IOException {...}// public static Builder newBuilder() {...}
}
上述的例⼦中:
• 获取字段值的 get ⽅法,⽽没有 set ⽅法。
• parseFrom() 系列静态⽅法提供了反序列化消息对象的能⼒。
• newBuilder() 静态⽅法:⽤来创建 Builder。
PeopleInfo.java 中 Builder 内部类部分代码展⽰(为了简洁,⽅法省略了具体实现):
public static final class Builder extendscom.google.protobuf.GeneratedMessageV3.Builder<Builder> implementscom.example.start.PeopleInfoOrBuilder {// --------------------------- build ⽅法 --------------------------- @java.lang.Overridepublic com.example.start.PeopleInfo build() {...}// --------------------------- 处理字段⽅法 ---------------------------- private java.lang.Object name_ = "";public java.lang.String getName() {...}public com.google.protobuf.ByteString getNameBytes() {...}public Builder setName(java.lang.String value) {...}public Builder clearName() {...}public Builder setNameBytes(com.google.protobuf.ByteString value) {...}private int age_ ;public int getAge() {...}public Builder setAge(int value) {...}public Builder clearAge() {...}}
上述的例⼦中:
• 包含⼀个build() ⽅法:主要是⽤来构造出⼀个⾃定义类对象。
• 每个字段都有set设置和get获取的⽅法。
• 每个字段都有⼀个 clear ⽅法,可以将字段重新设置回 empty 状态。
到这⾥有同学可能就有疑惑了,那之前提到的序列化⽅法在哪⾥呢?其实在⾃定义消息类继承的接⼝ MessageLite 中,提供了序列化消息实例的⽅法。
public interface MessageLite extends MessageLiteOrBuilder {// 序列化 message,并返回序列化后的字节的byte数组。 byte[] toByteArray();// 序列化message,并将其写⼊⼀个OutputStream。 void writeTo(OutputStream var1) throws IOException;
}
注意:
• 序列化的结果为⼆进制字节序列,⽽⾮⽂本格式。
• 以上两种序列化的⽅法没有本质上的区别,只是序列化后输出的格式不同,可以供不同的应⽤场景使⽤。
• 详细 message API 可以参⻅ https://protobuf.dev/reference/java/
步骤3:序列化与反序列化的使用
创建⼀个测试⽂件 FastStart.java,⽅法中我们实现:
• 对⼀个联系⼈的信息使⽤ PB 进⾏序列化,并将结果打印出来。
• 对序列化后的内容使⽤ PB 进⾏反序列,解析出联系⼈信息并打印出来。
FastStart.java
public class FastStart {public static void main(String[] args) {// 使⽤消息构造器来构造消息 PeopleInfo p1 = PeopleInfo.newBuilder().setName("张三").setAge(20).build();// 序列化消息 byte[] s = p1.toByteArray();System.out.println("序列化后数据,bytes[] = " + Arrays.toString(s));System.out.println("序列化后数据的⼤⼩: " + s.length);try {// 反序列化消息 PeopleInfo p2 = PeopleInfo.parseFrom(s);System.out.println("反序列化name: " + p2.getName());System.out.println("反序列化age: " + p2.getAge());} catch (InvalidProtocolBufferException e) {e.printStackTrace();}}
}
运⾏代码:
序列化后数据,bytes[] = [10, 6, -27, -68, -96, -28, -72, -119, 16, 20]
序列化后数据的⼤⼩: 10
反序列化name: 张三
反序列化age: 20
ProtoBuf 是把联系⼈对象序列化成了⼆进制序列,这⾥⽤ byte[] 来作为接收⼆进制序列的容器,帮助我们看到序列化后的结果。 所以相对于 xml 和 JSON 来说,因为被编码成⼆进制,破解成本增⼤,ProtoBuf 编码是相对安全的
proto 3 语法详解
字段规则
消息的字段可以⽤下⾯⼏种规则来修饰:
singular
消息中可以包含该字段零次或⼀次(不超过⼀次)。proto3 语法中,字段默认使⽤该规则。
repeated
消息中可以包含该字段任意多次(包括零次),其中重复值的顺序会被保留。可以理解为定义了⼀个数组。
我们新建 contacts.proto ⽂件,内容如下:
syntax = "proto3";
package proto3;
option java_multiple_files = true; // 编译后⽣成的⽂件是否分为多个⽂件
option java_package = "com.example.proto3"; // 编译后⽣成⽂件所在的包路径
option java_outer_classname="ContactsProtos"; // 编译后⽣成的proto包装类的类名
message PeopleInfo {string name = 1;int32 age = 2;repeated string phone_numbers = 3;
}
PeopleInfo 消息中新增 phone_numbers 字段,表⽰⼀个联系⼈有多个号码,所以将其设置为 repeated。
消息类型的定义与使用
定义
在单个 .proto ⽂件中可以定义多个消息体,且⽀持定义嵌套类型的消息(任意多层)。每个消息体中的字段编号可以重复。更新 contacts.proto,我们可以将 phone_number 提取出来,单独成为⼀个消息:
// -------------------------- 嵌套写法 -------------------------
message PeopleInfo {string name = 1; int32 age = 2; message Phone {string number = 1;}
}
// -------------------------- ⾮嵌套写法 -------------------------
message Phone {string number = 1;
}
message PeopleInfo {string name = 1; int32 age = 2;
}
使⽤
消息类型可作为字段类型使⽤
// 联系⼈
message PeopleInfo {string name = 1; int32 age = 2; message Phone {string number = 1; }repeated Phone phone = 3;
}
可导⼊其他 .proto ⽂件的消息并使用
例如 Phone 消息定义在 phone.proto ⽂件中:
syntax = "proto3";
package phone;
option java_multiple_files = true; // 编译后⽣成的⽂件是否分为多个⽂件
option java_package = "com.example.proto3"; // 编译后⽣成⽂件所在的包路径
option java_outer_classname="PhoneProtos"; // 编译后⽣成的proto包装类的类名
message Phone {string number = 1;
}
contacts.proto 中的 PeopleInfo 使⽤ Phone 消息:
syntax = "proto3";
package proto3;
option java_multiple_files = true; // 编译后⽣成的⽂件是否分为多个⽂
件
option java_package = "com.example.proto3"; // 编译后⽣成⽂件所在的包路径
option java_outer_classname="ContactsProtos"; // 编译后⽣成的proto包装类的类名
// 使⽤ import 注意点:
// 1、import 不允许使⽤相对路径
// import "phone.proto";
// 2、import 导⼊路径应该从pom中的protoSourceRoot设置的路径开始设置
import "proto3/phone.proto"; // 使⽤ import 将 phone.proto ⽂件导⼊进来 !!!
message PeopleInfo {string name = 1;int32 age = 2;repeated phone.Phone phone = 3;
}
注:在 proto3 ⽂件中可以导⼊ proto2 消息类型并使⽤它们,反之亦然。
文件的写入与读取
contacts.proto
syntax = "proto3";
package proto3;
option java_multiple_files = true; // 编译后⽣成的⽂件是否分为多个⽂件
option java_package = "com.example.proto3"; // 编译后⽣成⽂件所在的包路径
option java_outer_classname="ContactsProtos"; // 编译后⽣成的proto包装类的类名
// 联系⼈
message PeopleInfo {string name = 1; // 姓名 int32 age = 2; // 年龄 message Phone {string number = 1; // 电话号码 }repeated Phone phone = 3; // 电话
}
// 通讯录
message Contacts {repeated PeopleInfo contacts = 1;
}
接着使⽤ maven 插件进⾏⼀次编译,这次编译会多⽣成五个⽂件: Contacts.java ContactsOrBuilder.java ContactsProtos.java PeopleInfo.java PeopleInfoOrBuilder.java 。 可以看出由于我们设置了 option java_multiple_files = true; ,会给⽣成的每个⾃定义 message 类都⽣成两个对应的⽂件:。
Contacts.java 部分代码展示:
public final class Contacts extendscom.google.protobuf.GeneratedMessageV3 implementsContactsOrBuilder {// -------------------- get ⽅法 ------------------------- private java.util.List<com.example.proto3.PeopleInfo> contacts_;public java.util.List<com.example.proto3.PeopleInfo> getContactsList() {...}public java.util.List<? extends com.example.proto3.PeopleInfoOrBuilder> getContactsOrBuilderList() {...}public int getContactsCount() {...}public com.example.proto3.PeopleInfo getContacts(int index) {...}public com.example.proto3.PeopleInfoOrBuilder getContactsOrBuilder(int index){...}
// ------------------- 反序列化⽅法 -------------------------
public static com.example.proto3.Contacts parseFrom(byte[] data)throws com.google.protobuf.InvalidProtocolBufferException {...}public static com.example.proto3.Contacts parseFrom(java.io.InputStream
input)throws java.io.IOException {...}// ------------------- newBuilder ---------------------------public static Builder newBuilder() {return DEFAULT_INSTANCE.toBuilder();}// --------------------------------- Builder 内部类 --------------------------
--public static final class Builder extendscom.google.protobuf.GeneratedMessageV3.Builder<Builder> implementscom.example.proto3.ContactsOrBuilder {// --------------------- build ---------------------------public com.example.start.PeopleInfo build() {...}// -------------------- 处理字段⽅法 ----------------------- private java.util.List<com.example.proto3.PeopleInfo> contacts_ =java.util.Collections.emptyList();public java.util.List<com.example.proto3.PeopleInfo> getContactsList()
{...}public int getContactsCount() {...}public com.example.proto3.PeopleInfo getContacts(int index) {...}public Builder setContacts(int index, com.example.proto3.PeopleInfo value)
{...}public Builder setContacts(int index, com.example.proto3.PeopleInfo.Builder builderForValue) {...}public Builder addContacts(com.example.proto3.PeopleInfo value) {...}public Builder addContacts(int index, com.example.proto3.PeopleInfo value)
{...}public Builder addContacts(com.example.proto3.PeopleInfo.Builder
builderForValue) {...}public Builder addContacts(int index, com.example.proto3.PeopleInfo.Builder builderForValue) {...}public Builder addAllContacts(java.lang.Iterable<? extends com.example.proto3.PeopleInfo> values)
{...}public Builder clearContacts() {...}public Builder removeContacts(int index) {...}public com.example.proto3.PeopleInfo.Builder getContactsBuilder(int index)
{...}public java.util.List<? extends com.example.proto3.PeopleInfoOrBuilder> getContactsOrBuilderList() {...}
public com.example.proto3.PeopleInfo.Builder addContactsBuilder() {...}public com.example.proto3.PeopleInfo.Builder addContactsBuilder(int index)
{...}public java.util.List<com.example.proto3.PeopleInfo.Builder> getContactsBuilderList() {...}}
}
PeopleInfo.java 部分代码展⽰:
public final class PeopleInfo extendscom.google.protobuf.GeneratedMessageV3 implementsPeopleInfoOrBuilder {// -------------------------- Phone 嵌套类型 ------------------------------- public static final class Phone extendscom.google.protobuf.GeneratedMessageV3 implementsPhoneOrBuilder {// -------------------------- get ---------------------public java.lang.String getNumber() {...}public com.google.protobuf.ByteString getNumberBytes() {...}// ------------------------ 反序列化⽅法 --------------- public static com.example.proto3.PeopleInfo.Phone parseFrom(byte[] data)throws com.google.protobuf.InvalidProtocolBufferException {...}public static com.example.proto3.PeopleInfo.Phone
parseFrom(java.io.InputStream input)throws java.io.IOException {...}// ------------------------ newBuilder ---------------public static Builder newBuilder() {...}// -------------------- Phone Builder 类 ------------------ public static final class Builder extendscom.google.protobuf.GeneratedMessageV3.Builder<Builder> implementscom.example.proto3.PeopleInfo.PhoneOrBuilder {// ------------------ build ------------------public com.example.proto3.PeopleInfo.Phone build() {...}// ---------------- 处理字段⽅法 --------------- private java.lang.Object number_ = "";public java.lang.String getNumber() {...}public com.google.protobuf.ByteString getNumberBytes() {...}public Builder setNumber(java.lang.String value) {...}public Builder clearNumber() {...}public Builder setNumberBytes(com.google.protobuf.ByteString value) {...}}
// -------------------------- Phone 嵌套类型结束 ------------------------------- // -------------------------- PeopleInfo 类 ----------------------------------- // 此处省略了姓名,年龄的 get ⽅法,在快速上⼿部分已经展⽰,只展⽰新增的电话字段相关⽅法 private java.util.List<com.example.proto3.PeopleInfo.Phone> phone_;public java.util.List<com.example.proto3.PeopleInfo.Phone> getPhoneList()
{...}public java.util.List<? extends
com.example.proto3.PeopleInfo.PhoneOrBuilder> getPhoneOrBuilderList() {...}public int getPhoneCount() {...}public com.example.proto3.PeopleInfo.Phone getPhone(int index) {...}public com.example.proto3.PeopleInfo.PhoneOrBuilder getPhoneOrBuilder(int
index) {...}// 此处省略反序列化⽅法 和 newBuilder() ⽅法 // ------------------------ PeopleInfo Builder 类 -------------------------- public static final class Builder extendscom.google.protobuf.GeneratedMessageV3.Builder<Builder> implementscom.example.proto3.PeopleInfoOrBuilder {// -------------------- build --------------------public com.example.proto3.PeopleInfo build() {...}// ------------------- mergefrom ----------------public Builder mergeFrom(com.google.protobuf.Message other) {...}public Builder mergeFrom(com.example.proto3.Contacts other) {...}// ------------------ 处理字段⽅法 ------------------ private com.google.protobuf.RepeatedFieldBuilderV3<com.example.proto3.PeopleInfo.Phone,
com.example.proto3.PeopleInfo.Phone.Builder,
com.example.proto3.PeopleInfo.PhoneOrBuilder> phoneBuilder_;public java.util.List<com.example.proto3.PeopleInfo.Phone> getPhoneList()
{...}public int getPhoneCount() {...}public com.example.proto3.PeopleInfo.Phone getPhone(int index) {...}public Builder setPhone(int index, com.example.proto3.PeopleInfo.Phone
value) {...}public Builder setPhone(int index, com.example.proto3.PeopleInfo.Phone.Builder builderForValue){...}public Builder addPhone(com.example.proto3.PeopleInfo.Phone value) {...}public Builder addPhone(int index, com.example.proto3.PeopleInfo.Phone
value) {...}public Builder addPhone(com.example.proto3.PeopleInfo.Phone.Builder builderForValue) {...}public Builder addPhone(int index, com.example.proto3.PeopleInfo.Phone.Builder builderForValue){...}public Builder addAllPhone(java.lang.Iterable<? extends com.example.proto3.PeopleInfo.Phone>
values) {...}public Builder clearPhone() {...}public Builder removePhone(int index) {...}public com.example.proto3.PeopleInfo.Phone.Builder getPhoneBuilder(int
index) {...}public com.example.proto3.PeopleInfo.PhoneOrBuilder getPhoneOrBuilder(int
index) {...}public java.util.List<? extends
com.example.proto3.PeopleInfo.PhoneOrBuilder> getPhoneOrBuilderList() {...}public com.example.proto3.PeopleInfo.Phone.Builder addPhoneBuilder() {...}public com.example.proto3.PeopleInfo.Phone.Builder addPhoneBuilder(int
index) {...}public java.util.List<com.example.proto3.PeopleInfo.Phone.Builder> getPhoneBuilderList() {...}}
}
再次印证了:
• 在 message 类中,主要包含:◦ 获取字段值的 get ⽅法,⽽没有 set ⽅法。
◦ 序列化(在MessageLite中定义)和反序列化⽅法。
◦ newBuilder() 静态⽅法:⽤来创建 Builder。
• 在 Builder 类中,主要包含:
◦ 包含⼀个build() ⽅法:主要是⽤来构造出⼀个⾃定义类对象。
◦ 编译器为每个字段提供了获取和设置⽅法,以及能够操作字段的⼀些⽅法。
且在上述的例⼦中:
• 对于builder,每个字段都有⼀个 clear_ ⽅法,可以将字段重新设置回 empty 状态。
• mergeFrom(Message other):合并other的内容到这个 message 中,如果是单数域则覆盖,如果是重复值则追加连接。
• 对于使⽤ repeated 修饰的字段,也就是数组类型,pb 为我们提供了⼀系列 add ⽅法来新增⼀个 值或⼀个 builder,并且提供了 getXXXCount() ⽅法来获取数组存放元素的个数。
文件写入实现
TestWrite.java
package com.example.proto3;
import java.io.*;
import java.util.Scanner;
public class TestWrite {public static void main(String[] args) throws IOException {Contacts.Builder contactsBuilder = Contacts.newBuilder();// 读取已存在的contacts try {contactsBuilder.mergeFrom(new FileInputStream("src/main/java/com/example/proto3/contacts.bin"));} catch (FileNotFoundException e) {System.out.println("contacts.bin not found. Creating a new
file.");}// 新增⼀个联系⼈ contactsBuilder.addContacts(addPeopleInfo());// 将新的contacts写回磁盘 FileOutputStream output = new
FileOutputStream("src/main/java/com/example/proto3/contacts.bin");contactsBuilder.build().writeTo(output);output.close();}private static PeopleInfo addPeopleInfo() {Scanner scan = new Scanner(System.in);PeopleInfo.Builder peopleBuilder = PeopleInfo.newBuilder();System.out.println("-------------新增联系⼈-------------");System.out.print("请输⼊联系⼈姓名: ");String name = scan.nextLine();peopleBuilder.setName(name);System.out.print("请输⼊联系⼈年龄: ");int age = scan.nextInt();peopleBuilder.setAge(age);scan.nextLine();for(int i = 0; ; i++) {System.out.print("请输⼊联系⼈电话" + (i+1) + "(只输⼊回⻋完成电话新
增): ");String number = scan.nextLine();if (number.isEmpty()) {break;}PeopleInfo.Phone.Builder phoneBuilder =
PeopleInfo.Phone.newBuilder();phoneBuilder.setNumber(number);peopleBuilder.addPhone(phoneBuilder);}System.out.println("-------------添加联系⼈成功-------------");return peopleBuilder.build();}
}
运⾏代码:
contacts.bin not found. Creating a new file.
-------------新增联系⼈-------------
请输⼊联系⼈姓名: 张三
请输⼊联系⼈年龄: 20
请输⼊联系⼈电话1(只输⼊回⻋完成电话新增): 13111111
请输⼊联系⼈电话2(只输⼊回⻋完成电话新增): 15111111
请输⼊联系⼈电话3(只输⼊回⻋完成电话新增):
-------------添加联系⼈成功-------------
文件读取实现
TestRead.java
package com.example.proto3;
import java.io.FileInputStream;
import java.io.IOException;
public class TestRead {public static void main(String[] args) throws IOException {// 从磁盘⽂件⾥读取,并反序列化为 Message 实例 Contacts contacts = Contacts.parseFrom(
new FileInputStream("src/main/java/com/example/proto3/contacts.bin"));// 打印 printContacts(contacts);}private static void printContacts(Contacts contacts) {for (int i = 0; i < contacts.getContactsCount(); i++) {System.out.println("--------------联系⼈" + (i+1) + "-----------");PeopleInfo peopleInfo = contacts.getContacts(i);System.out.println("姓名: " + peopleInfo.getName());System.out.println("年龄: " + peopleInfo.getAge());int j = 1;for(PeopleInfo.Phone phone : peopleInfo.getPhoneList()) {System.out.println("电话" + (j++) + ": " + phone.getNumber());}}}
}
运⾏代码:
--------------联系⼈1-----------
姓名: 张三
年龄: 20
电话1: 13111111
电话2: 15111111
另⼀种验证⽅法--toString() 在⾃定义消息类的⽗抽象类 AbstractMessage 中,重写了 toString() ⽅法。该⽅法返回的内容是⼈类可读的,对于调试特别有⽤。 例如在 TestRead 类的main 函数中调⽤⼀下
public static void main(String[] args) throws IOException {// 从磁盘⽂件⾥读取,并反序列化为 Message 实例 Contacts contacts = Contacts.parseFrom(new FileInputStream("src/main/java/com/example/proto3/contacts.bin"));// 打印 System.out.println(contacts.toString());// 打印 // printContacts(contacts);
}
运⾏结果:
contacts {name: "\345\274\240\344\270\211" // 在这⾥是将utf-8汉字转为⼋进制格式输出了age: 20phone {number: "13111111"}phone {number: "15111111"}
}
enum 类型
定义规则
语法⽀持我们定义枚举类型并使⽤。在.proto ⽂件中枚举类型的书写规范为: 枚举类型名称: 使⽤驼峰命名法,⾸字⺟⼤写。例如: MyEnum 常量值名称: 全⼤写字⺟,多个字⺟之间⽤_ 连接。例如: ENUM_CONST = 0;
我们可以定义⼀个名为 PhoneType 的枚举类型,定义如下
enum PhoneType {MP = 0; // 移动电话 TEL = 1; // 固定电话
}
要注意枚举类型的定义有以下⼏种规则:
1. 0 值常量必须存在,且要作为第⼀个元素。这是为了与 proto2 的语义兼容:第⼀个元素作为默认值,且值为 0。
2. 枚举类型可以在消息外定义,也可以在消息体内定义(嵌套)。
3. 枚举的常量值在 32 位整数的范围内。但因负值⽆效因⽽不建议使⽤(与编码规则有关)。
定义时注意
将两个具有相同枚举值名称’的枚举类型放在单个 .proto ⽂件下测试时,编译后会报错:某某某常量已经被定义!所以这⾥要注意:
• 同级(同层)的枚举类型,各个枚举类型中的常量不能重名。
• 单个 .proto ⽂件下,最外层枚举类型和嵌套枚举类型,不算同级。
• 多个 .proto ⽂件下,若⼀个⽂件引⼊了其他⽂件,且每个⽂件都未声明 package,每个 proto ⽂件中的枚举类型都在最外层,算同级。
• 多个 .proto ⽂件下,若⼀个⽂件引⼊了其他⽂件,且每个⽂件都声明了 package,不算同级。
// ---------------------- 情况1:同级枚举类型包含相同枚举值名称--------------------
enum PhoneType {MP = 0; // 移动电话 TEL = 1; // 固定电话
}
enum PhoneTypeCopy {MP = 0; // 移动电话 // 编译后报错:MP 已经定义
}
// ---------------------- 情况2:不同级枚举类型包含相同枚举值名称-------------------
-
enum PhoneTypeCopy {MP = 0; // 移动电话 // ⽤法正确
}
message Phone {string number = 1; // 电话号码 enum PhoneType {MP = 0; // 移动电话 TEL = 1; // 固定电话 }
}
// ---------------------- 情况3:多⽂件下都未声明package--------------------
// phone1.proto
import "phone1.proto"
enum PhoneType {MP = 0; // 移动电话 // 编译后报错:MP 已经定义 TEL = 1; // 固定电话
}
// phone2.proto
enum PhoneTypeCopy {MP = 0; // 移动电话
}
// ---------------------- 情况4:多⽂件下都声明了package--------------------
// phone1.proto
import "phone1.proto"
package phone1;
enum PhoneType {MP = 0; // 移动电话 // ⽤法正确 TEL = 1; // 固定电话
}
// phone2.proto
package phone2;
enum PhoneTypeCopy {MP = 0; // 移动电话
}
实操
更新 contacts.proto,新增枚举字段并使⽤,更新内容如下:
syntax = "proto3";
package proto3;
option java_multiple_files = true; // 编译后⽣成的⽂件是否分为多个⽂件
option java_package = "com.example.proto3"; // 编译后⽣成⽂件所在的包路径
option java_outer_classname="ContactsProtos"; // 编译后⽣成的proto包装类的类名
// 联系⼈
message PeopleInfo {string name = 1; // 姓名 int32 age = 2; // 年龄 message Phone {string number = 1; // 电话号码 enum PhoneType {MP = 0; // 移动电话 TEL = 1; // 固定电话 }PhoneType type = 2; // 类型 }repeated Phone phone = 3; // 电话
}
// 通讯录
message Contacts {repeated PeopleInfo contacts = 1;
}
接着使⽤ maven 插件进⾏⼀次编译。PeopleInfo.java 更新的部分代码展⽰:
public final class PeopleInfo extendscom.google.protobuf.GeneratedMessageV3 implementsPeopleInfoOrBuilder {public static final class Phone extendscom.google.protobuf.GeneratedMessageV3 implementsPhoneOrBuilder {// ---------------- 新增的枚举类型 -------------------------- public enum PhoneType implements com.google.protobuf.ProtocolMessageEnum {MP(0),TEL(1),UNRECOGNIZED(-1),;public static final int MP_VALUE = 0;public static final int TEL_VALUE = 1;public static PhoneType valueOf(int value) {...}}// ------------------ get ------------------------private int type_ = 0;public int getTypeValue() {...}public com.example.proto3.PeopleInfo.Phone.PhoneType getType() {...}// ----------------------- phone builder -----------------------public static final class Builder extendscom.google.protobuf.GeneratedMessageV3.Builder<Builder> implementscom.example.proto3.PeopleInfo.PhoneOrBuilder {// --------------- 新增处理type字段⽅法 ---------------------- private int type_ = 0;public int getTypeValue() {...}public Builder setTypeValue(int value) {...}public com.example.proto3.PeopleInfo.Phone.PhoneType getType() {...}public Builder setType(com.example.proto3.PeopleInfo.Phone.PhoneType
value) {...}public Builder clearType() {...}}}
}
上述的代码中:
• 对于在.proto⽂件中定义的枚举类型,编译⽣成的代码中会含有与之对应的枚举类型。
• 对于使⽤了枚举类型的字段,在 Builder 中包含设置和获取字段的⽅法,以及清空字段的⽅法clear_。
设置
System.out.print("选择此电话类型 (1、移动电话 2、固定电话) : ");int type = scan.nextInt();scan.nextLine();switch (type) {case 1:phoneBuilder.setType(PeopleInfo.Phone.PhoneType.MP);break;case 2:phoneBuilder.setType(PeopleInfo.Phone.PhoneType.TEL);break;default:System.out.println("⾮法选择,使⽤默认值!");break;}
读取
System.out.println("电话" + (j++) + ": " + phone.getNumber()+ " (" + phone.getType().name() + ")");
Any 类型
字段还可以声明为 Any 类型,可以理解为泛型类型。使⽤时可以在 Any 中存储任意消息类型。Any 类型的字段也⽤ repeated 来修饰。 Any 类型是 google 已经帮我们定义好的类型,在安装 ProtoBuf 时,其中的 include ⽬录下查找所有 google 已经定义好的 .proto ⽂件。
实操
更新 contacts.proto,使⽤ any 类型的字段来存储地址信息,更新内容如下:
syntax = "proto3";
package proto3;
option java_multiple_files = true; // 编译后⽣成的⽂件是否分为多个⽂件
option java_package = "com.example.proto3"; // 编译后⽣成⽂件所在的包路径
option java_outer_classname="ContactsProtos"; // 编译后⽣成的proto包装类的类名
import "google/protobuf/any.proto"; // 引⼊ any.proto ⽂件
// 地址
message Address{string home_address = 1; // 家庭地址 string unit_address = 2; // 单位地址
}
// 联系⼈
message PeopleInfo {string name = 1; // 姓名 int32 age = 2; // 年龄 message Phone {string number = 1; // 电话号码 enum PhoneType {MP = 0; // 移动电话 TEL = 1; // 固定电话 }PhoneType type = 2; // 类型 }repeated Phone phone = 3; // 电话 google.protobuf.Any data = 4;
}
// 通讯录
message Contacts {repeated PeopleInfo contacts = 1;
}
使⽤ maven 插件编译后,新⽣成了两个⽂件:Address.java AddressOrBuilder.java 。 并且更新了 PeopleInfo.java 。学习到这⾥,对于 Address.java ,就不展⽰其代码了。但我们肯定能知道在 Address 类中,主 要包含了获取地址信息字段的 get ⽅法、反序列化⽅法,以及⽤来创建 Builder 的 newBuilder() 静态⽅法。在 Builder 内部类中,主要包含了构造 Address 类对象的 build() ⽅法,以及能够操作字段的⼀些⽅法,例如 get/set/clear... 。 对于更新的 PeopleInfo.java ,我们来看看其对于 Any 字段更新的内容:
public final class PeopleInfo extendscom.google.protobuf.GeneratedMessageV3 implementsPeopleInfoOrBuilder { // ----------------------- get/has -------------------------private com.google.protobuf.Any data_;public boolean hasData() {...}public com.google.protobuf.Any getData() {...}public static final class Builder extendscom.google.protobuf.GeneratedMessageV3.Builder<Builder> implementscom.example.proto3.PeopleInfoOrBuilder {// ------------------------ 处理 any 字段⽅法 ------------------- private com.google.protobuf.Any data_;public boolean hasData() {...}public com.google.protobuf.Any getData() {...}public Builder setData(com.google.protobuf.Any value) {...}public Builder setData(com.google.protobuf.Any.Builder builderForValue)
{...}public Builder mergeData(com.google.protobuf.Any value) {...}public Builder clearData() {...}public com.google.protobuf.Any.Builder getDataBuilder() {...}public com.google.protobuf.AnyOrBuilder getDataOrBuilder() {...}}
}
• 对于 has ⽅法,⽤来检测当前字段是否被设置。
• 对于 set ⽅法,它要求传⼀个 Any 类型的对象。
• 对于 Any 类型:
◦ 使⽤ Any.pack(T message) ⽅法可以将任意消息类型转为 Any 类型。
◦ 使⽤ message.getAny().unpack(Class clazz)⽅法可以将 Any 类型转回之前设置的任意消息类型。
◦ 使⽤ message.getAny().is(Class clazz) ⽅法⽤来判断存放的消息类型。
设置
Address.Builder addressBuilder = Address.newBuilder();System.out.print("请输⼊联系⼈家庭地址: ");String homeAddress = scan.nextLine();addressBuilder.setHomeAddress(homeAddress);System.out.print("请输⼊联系⼈单位地址: ");String unitAddress = scan.nextLine();addressBuilder.setUnitAddress(unitAddress);peopleBuilder.setData(Any.pack(addressBuilder.build()));
读取
if (peopleInfo.hasData() && peopleInfo.getData().is(Address.class)) {Address address = peopleInfo.getData().unpack(Address.class);if (!address.getHomeAddress().isEmpty()) {System.out.println("家庭地址:" + address.getHomeAddress());}if (!address.getUnitAddress().isEmpty()) {System.out.println("单位地址:" + address.getUnitAddress());}}
oneof 类型
如果消息中有很多可选字段,并且将来同时只有⼀个字段会被设置,那么就可以使⽤ oneof 加强这个⾏为,也能有节约内存的效果。
实操
更新 contacts.proto,使⽤ oneof 字段来加强多选⼀这个⾏为(qq或者微信号⼆选⼀),更新内容如下:
syntax = "proto3";
package proto3;
option java_multiple_files = true; // 编译后⽣成的⽂件是否分为多个⽂件
option java_package = "com.example.proto3"; // 编译后⽣成⽂件所在的包路径
option java_outer_classname="ContactsProtos"; // 编译后⽣成的proto包装类的类名
import "google/protobuf/any.proto"; // 引⼊ any.proto ⽂件
// 地址
message Address{string home_address = 1; // 家庭地址 string unit_address = 2; // 单位地址
}
// 联系⼈
message PeopleInfo {string name = 1; // 姓名 int32 age = 2; // 年龄 message Phone {string number = 1; // 电话号码 enum PhoneType {MP = 0; // 移动电话 TEL = 1; // 固定电话 }PhoneType type = 2; // 类型 }repeated Phone phone = 3; // 电话 google.protobuf.Any data = 4;oneof other_contact { // 其他联系⽅式:多选⼀ string qq = 5;string wechat = 6;}
}
// 通讯录
message Contacts {repeated PeopleInfo contacts = 1;
}
注意:
• 可选字段中的字段编号,不能与⾮可选字段的编号冲突。
• 不能在 oneof 中使⽤ repeated 字段。
• 将来在设置 oneof 字段中值时,如果将 oneof 中的字段设置多个,那么只会保留最后⼀次设置的成员,之前设置的 oneof 成员会⾃动清除。
使⽤ maven 插件进⾏⼀次编译。展⽰更新的 PeopleInfo.java 部分代码:
public final class PeopleInfo extendscom.google.protobuf.GeneratedMessageV3 implementsPeopleInfoOrBuilder { // --------------------- 新增枚举类型 -------------------- public enum OtherContactCaseimplements com.google.protobuf.Internal.EnumLite,com.google.protobuf.AbstractMessage.InternalOneOfEnum {QQ(5),WECHAT(6),OTHERCONTACT_NOT_SET(0);};// ---------------------- get ------------------------public OtherContactCase getOtherContactCase() {...}public java.lang.String getQq() {...}public com.google.protobuf.ByteString getQqBytes() {...}public java.lang.String getWechat() {...}public com.google.protobuf.ByteString getWechatBytes() {...}// ---------------------- has -----------------------public boolean hasQq() {...}public boolean hasWechat() {...}public static final class Builder extendscom.google.protobuf.GeneratedMessageV3.Builder<Builder> implementscom.example.proto3.PeopleInfoOrBuilder {// ------------------------ 处理字段⽅法 ------------------- private java.lang.Object otherContact_;public OtherContactCase getOtherContactCase() {...}public Builder clearOtherContact() {...}public boolean hasQq() {...}public java.lang.String getQq() {...}public com.google.protobuf.ByteString getQqBytes() {...}public Builder setQq(java.lang.String value) {...}public Builder clearQq() {...}public Builder setQqBytes(com.google.protobuf.ByteString value) {...}public boolean hasWechat() {...}public java.lang.String getWechat() {...}public com.google.protobuf.ByteString getWechatBytes() {...}public Builder setWechat(java.lang.String value) {...}public Builder clearWechat() {...}public Builder setWechatBytes(com.google.protobuf.ByteString value) {...}}
}
上述的代码中,对于 oneof 字段:
• 会将 oneof 中的多个字段定义为⼀个枚举类型。
• 设置和获取注意:对 oneof 内的字段进⾏常规的设置和获取即可,但要注意只能设置⼀个。如果 设置多个,那么只会保留最后⼀次设置的成员。
• clear() ⽅法:清空 oneof 字段
• getXXXCase() ⽅法:获取当前设置了哪个字段
• hasXXX() ⽅法:检测当前字段是否被设置
设置
System.out.print("选择添加⼀个其他联系⽅式 (1、qq号 2、微信号) : ");int other_contact = scan.nextInt();scan.nextLine();if (1 == other_contact) {System.out.print("请输⼊qq号: ");String qq = scan.nextLine();peopleBuilder.setQq(qq);} else if (2 == other_contact) {System.out.print("请输⼊微信号: ");String wechat = scan.nextLine();peopleBuilder.setWechat(wechat);} else {System.out.println("⾮法选择,该项设置失败!");}
读取
switch (peopleInfo.getOtherContactCase()) {case QQ:System.out.println("qq号:" + peopleInfo.getQq());break;case WECHAT:System.out.println("微信号:" + peopleInfo.getWechat());break;case OTHERCONTACT_NOT_SET:break;}
map 类型
语法⽀持创建⼀个关联映射字段,也就是可以使⽤ map 类型去声明字段类型,格式为: map map_field = N; 要注意的是:
• key_type 是除了 float 和 bytes 类型以外的任意标量类型。value_type 可以是任意类型。
• map 字段不可以⽤ repeated 修饰
• map 中存⼊的元素是⽆序的
实操
更新 contacts.proto,使⽤ map 类型的字段来存储备注信息,更新内容如下:
syntax = "proto3";
package proto3;
option java_multiple_files = true; // 编译后⽣成的⽂件是否分为多个⽂件
option java_package = "com.example.proto3"; // 编译后⽣成⽂件所在的包路径
option java_outer_classname="ContactsProtos"; // 编译后⽣成的proto包装类的类名
import "google/protobuf/any.proto"; // 引⼊ any.proto ⽂件
// 地址
message Address{string home_address = 1; // 家庭地址 string unit_address = 2; // 单位地址
}
// 联系⼈
message PeopleInfo {string name = 1; // 姓名 int32 age = 2; // 年龄 message Phone {string number = 1; // 电话号码 enum PhoneType {MP = 0; // 移动电话 TEL = 1; // 固定电话 }PhoneType type = 2; // 类型 }repeated Phone phone = 3; // 电话 google.protobuf.Any data = 4;oneof other_contact { // 其他联系⽅式:多选⼀ string qq = 5;string wechat = 6;}map<string, string> remark = 7; // 备注
}
// 通讯录
message Contacts {repeated PeopleInfo contacts = 1;
}
使⽤ maven 插件进⾏⼀次编译。展⽰更新的 PeopleInfo.java 部分代码
public final class PeopleInfo extendscom.google.protobuf.GeneratedMessageV3 implementsPeopleInfoOrBuilder { // ---------------------- get ------------------------public int getRemarkCount() {...}public boolean containsRemark(java.lang.String key) {...}public java.util.Map<java.lang.String, java.lang.String> getRemark() {...}public java.util.Map<java.lang.String, java.lang.String> getRemarkMap() {...}public java.lang.String getRemarkOrDefault(java.lang.String key,java.lang.String defaultValue) {...}public java.lang.String getRemarkOrThrow(java.lang.String key) {...}public static final class Builder extendscom.google.protobuf.GeneratedMessageV3.Builder<Builder> implementscom.example.proto3.PeopleInfoOrBuilder {// ------------------------ 处理字段⽅法 ------------------- public int getRemarkCount() {...}public boolean containsRemark(java.lang.String key) {...}public java.util.Map<java.lang.String, java.lang.String> getRemark() {...}public java.util.Map<java.lang.String, java.lang.String> getRemarkMap()
{...}public java.lang.String getRemarkOrDefault(java.lang.String key, java.lang.String defaultValue) {...}public java.lang.String getRemarkOrThrow(java.lang.String key) {...}public Builder clearRemark() {...}public Builder removeRemark(java.lang.String key) {...}public java.util.Map<java.lang.String, java.lang.String>
getMutableRemark() {...}public Builder putRemark(java.lang.String key, java.lang.String value)
{...}public Builder putAllRemark(java.util.Map<java.lang.String, java.lang.String> values) {...}}
}
上述的代码中,对于Map类型的字段:
• clear() ⽅法:清空map
• putXXX() ⽅法:插⼊⼀个key/value 或塞⼊整个map
• getXXX()⽅法:获取元素或整个map
设置
for(int i = 0; ; i++) {System.out.print("请输⼊备注" + (i+1) + "标题 (只输⼊回⻋完成备注新增): ");String remarkKey = scan.nextLine();if (remarkKey.isEmpty()) {break;}System.out.print("请输⼊备注" + (i+1) + "内容: ");String remarkValue = scan.nextLine();peopleBuilder.putRemark(remarkKey, remarkValue);}
读取
if (0 != peopleInfo.getRemarkCount()) {System.out.println("备注信息: ");}for (Map.Entry<String, String> entry : peopleInfo.getRemarkMap().entrySet()) {System.out.println(" " + entry.getKey() + ": " + entry.getValue());}
默认值
反序列化消息时,如果被反序列化的⼆进制序列中不包含某个字段,反序列化对象中相应字段时,就会设置为该字段的默认值。不同的类型对应的默认值不同:
• 对于字符串,默认值为空字符串。
• 对于字节,默认值为空字节。
• 对于布尔值,默认值为 false。
• 对于数值类型,默认值为 0。
• 对于枚举,默认值是第⼀个定义的枚举值,必须为 0。
• 对于消息字段,未设置该字段。它的取值是依赖于语⾔。
• 对于设置了 repeated 的字段的默认值是空的(通常是相应语⾔的⼀个空列表)。
• 对于 消息字段 、 oneof字段 和 any字段 ,都有 has ⽅法来检测当前字段是否被设置。
更新消息
更新规则
如果现有的消息类型已经不再满⾜我们的需求,例如需要扩展⼀个字段,在不破坏任何现有代码的情况下更新消息类型⾮常简单。遵循如下规则即可:
• 禁⽌修改任何已有字段的字段编号。
• 若是移除⽼字段,要保证不再使⽤移除字段的字段编号。正确的做法是保留字段编号 (reserved),以确保该编号将不能被重复使⽤。不建议直接删除或注释掉字段。
• int32,uint32,int64,uint64 和 bool 是完全兼容的。可以从这些类型中的⼀个改为另⼀个, ⽽不破坏前后兼容性。若解析出来的数值与相应的类型不匹配,可能会被截断(例如,若将 64 位整数当做 32 位进⾏读取,它将被截断为 32 位)。
• sint32 和 sint64 相互兼容但不与其他的整型兼容。
• string 和 bytes 在合法 UTF-8 字节前提下也是兼容的。
• bytes 包含消息编码版本的情况下,嵌套消息与 bytes 也是兼容的。
• fixed32 与 sfixed32 兼容, fixed64 与 sfixed64 兼容。
• enum 与 int32,uint32,int64 和 uint64 兼容(注意若值不匹配会被截断)。但要注意当反序列化消息时会根据语⾔采⽤不同的处理⽅案:例如,未识别的 proto3 枚举类型会被保存在消息 中,但是当消息反序列化时如何表⽰是依赖于编程语⾔的。整型字段总是会保持其的值。
• oneof:
◦ 将⼀个单独的值更改为 新 oneof 类型成员之⼀是安全和⼆进制兼容的。
◦ 若确定没有代码⼀次性设置多个值那么将多个字段移⼊⼀个新 oneof 类型也是可⾏· 的。
◦ 将任何字段移⼊已存在的 oneof 类型是不安全的。
保留字段 reserved
如果通过删除或注释掉字段来更新消息类型,未来的⽤户在添加新字段时,有可能会使⽤以前已经存在,但已经被删除或注释掉的字段编号。将来使⽤该 .proto 的旧版本时的程序会引发很多问题:数据损坏、隐私错误等等。 确保不会发⽣这种情况的⼀种⽅法是:使⽤ reserved 将指定字段的编号或名称设置为保留项。当我们再使⽤这些编号或名称时,protocol buffer 的编译器将会警告这些编号或名称不可⽤。举个例⼦:
message Message {// 设置保留项 reserved 100, 101, 200 to 299;reserved "field3", "field4";// 注意:不要在⼀⾏ reserved 声明中同时声明字段编号和名称。 // reserved 102, "field5";// 设置保留项之后,下⾯代码会告警 int32 field1 = 100; //告警:Field 'field1' uses reserved number 100 int32 field2 = 101; //告警:Field 'field2' uses reserved number 101 int32 field3 = 102; //告警:Field name 'field3' is reserved int32 field4 = 103; //告警:Field name 'field4' is reserved
}
未知字段
• 未知字段:解析结构良好的 protocol buffer 已序列化数据中的未识别字段的表⽰⽅式。例如,当旧程序解析带有新字段的数据时,这些新字段就会成为旧程序的未知字段。
• 本来,proto3 在解析消息时总是会丢弃未知字段,但在 3.5 版本中重新引⼊了对未知字段的保留机制。所以在 3.5 或更⾼版本中,未知字段在反序列化时会被保留,同时也会包含在序列化的结果中。
未知字段从哪获取
在 PeopleInfo.java 的 PeopleInfo 类中,有个 getUnknownFields() ⽅法⽤来获取未知字段
public final com.google.protobuf.UnknownFieldSet getUnknownFields() {...}
UnknownFieldSet 类介绍
UnknownFieldSet 包含在分析消息时遇到但未将其类型定义的所有字段。
public final class UnknownFieldSet implements MessageLite {private final TreeMap<Integer, Field> fields;public Map<Integer, Field> asMap() {...}public boolean hasField(int number) {...}public Field getField(int number) {...}// ----------------- 重写了 toString ---------------- public String toString() {...}// ------------------------builder------------------public static final class Builder implements MessageLite.Builder {public Builder clear() {...}public Builder clearField(int number) {...}public boolean hasField(int number) {...}public Builder addField(int number, Field field) {...}public Map<Integer, Field> asMap() {...}}public static final class Field {private List<Long> varint;private List<Integer> fixed32;private List<Long> fixed64;private List<ByteString> lengthDelimited;private List<UnknownFieldSet> group;public List<Long> getVarintList() {...}public List<Integer> getFixed32List() {...}public List<Long> getFixed64List() {...}public List<ByteString> getLengthDelimitedList() {...}public List<UnknownFieldSet> getGroupList() {...}// 省略了 Field Builder : 是⼀些处理字段的⽅法,例如设置、获取、清理 }
}
前后兼容性
根据上述的例⼦可以得出,pb是具有向前兼容的。为了叙述⽅便,把增加了“⽣⽇”属性的 service 称为“新模块”;未做变动的 client 称为 “⽼模块”。
• 向前兼容:⽼模块能够正确识别新模块⽣成或发出的协议。这时新增加的“⽣⽇”属性会被当作未知字段( pb 3.5版本及之后)。
• 向后兼容:新模块也能够正确识别⽼模块⽣成或发出的协议。 前后兼容的作⽤:当我们维护⼀个很庞⼤的分布式系统时,由于你⽆法同时 升级所有模块,为了保证在升级过程中,整个系统能够尽可能不受影响,就需要尽量保证通讯协议的“向后兼容”或“向前兼容”。
选项 option
.proto ⽂件中可以声明许多选项,使⽤ option 标注。选项能影响 proto 编译器的某些处理⽅式。
选项分类
选项的完整列表在 google/protobuf/descriptor.proto 中定义。部分代码:
syntax = "proto2"; // descriptor.proto 使⽤ proto2 语法版本
message FileOptions { ... } // ⽂件选项 定义在 FileOptions 消息中
message MessageOptions { ... } // 消息类型选项 定义在 MessageOptions 消息中
message FieldOptions { ... } // 消息字段选项 定义在 FieldOptions 消息中
message OneofOptions { ... } // oneof字段选项 定义在 OneofOptions 消息中
message EnumOptions { ... } // 枚举类型选项 定义在 EnumOptions 消息中
message EnumValueOptions { .. } // 枚举值选项 定义在 EnumValueOptions 消息中
message ServiceOptions { ... } // 服务选项 定义在 ServiceOptions 消息中
message MethodOptions { ... } // 服务⽅法选项 定义在 MethodOptions 消息中
...
由此可⻅,选项分为 ⽂件级、消息级、字段级 等等,但并没有⼀种选项能作⽤于所有的类型。
JAVA 常用选项列举
• java_multiple_files:编译后⽣成的⽂件是否分为多个⽂件,该选项为⽂件选项。
• java_package:编译后⽣成⽂件所在的包路径,该选项为⽂件选项。
• java_outer_classname:编译后⽣成的proto包装类的类名,该选项为⽂件选项。
• allow_alias:允许将相同的常量值分配给不同的枚举常量,⽤来定义别名。该选项为枚举选项。 举个例⼦:
enum PhoneType {option allow_alias = true;MP = 0;TEL = 1;LANDLINE = 1; // 若不加 option allow_alias = true; 这⼀⾏会编译报错
}
在网络中进行数据交互、
Protobuf 还常⽤于通讯协议、服务端数据交换场景。那么在这个⽰例中,我们将实现⼀个⽹络版本的 通讯录,模拟实现客户端与服务端的交互,通过 Protobuf 来实现各端之间的协议序列化。需求如下:
• 客户端:向服务端发送联系⼈信息,并接收服务端返回的响应。
• 服务端:接收到联系⼈信息后,将结果打印出来。
• 客户端、服务端间的交互数据使⽤ Protobuf 来完成。
约定双端交互 req/resp
contacts.proto(注意:客户端服务端都私有⼀份)
message PeopleInfoRequest {string name = 1; // 姓名 int32 age = 2; // 年龄 message Phone {string number = 1; // 电话号码 enum PhoneType {MP = 0; // 移动电话 TEL = 1; // 固定电话 }PhoneType type = 2; // 类型 }repeated Phone phone = 3; // 电话 map<string, string> remark = 4; // 备注
}
message PeopleInfoResponse {string uid = 1;
}
客户端代码实现
ContactsClient.java
import com.example.internet.client.dto.PeopleInfoRequest;
import com.example.internet.client.dto.PeopleInfoResponse;
import java.io.*;
import java.net.*;
import java.util.Scanner;
public class ContactsClient {private static final SocketAddress ADDRESS = new InetSocketAddress("localhost", 8888);public static void main(String[] args) throws IOException {// 创建客⼾端 DatagramSocket DatagramSocket socket = new DatagramSocket();// 构造 request 请求数据 PeopleInfoRequest request = createRequest();// 序列化 request byte[] requestData = request.toByteArray();// 创建 request 数据报 DatagramPacket requestPacket = new DatagramPacket(requestData,requestData.length, ADDRESS);// 发送 request 数据报 socket.send(requestPacket);System.out.println("发送成功!");// 创建 response 数据报,⽤于接收服务端返回的响应 byte[] udpResponse = new byte[1024];DatagramPacket responsePacket = new DatagramPacket(udpResponse,udpResponse.length);// 接收 response 数据报 socket.receive(responsePacket);// 获取有效的 response int length = BytesUtils.getValidLength(udpResponse);byte[] reqsponseData = BytesUtils.subByte(udpResponse, 0, length);// 反序列化 response,打印结果 PeopleInfoResponse response =
PeopleInfoResponse.parseFrom(reqsponseData);System.out.printf("接收到服务端返回的响应:%s", response.toString());}private static PeopleInfoRequest createRequest() {System.out.println("------输⼊需要传输的联系⼈信息-----");Scanner scan = new Scanner(System.in);PeopleInfoRequest.Builder peopleBuilder =
PeopleInfoRequest.newBuilder();System.out.print("请输⼊联系⼈姓名: ");String name = scan.nextLine();peopleBuilder.setName(name);System.out.print("请输⼊联系⼈年龄: ");int age = scan.nextInt();peopleBuilder.setAge(age);scan.nextLine();for(int i = 0; ; i++) {System.out.print("请输⼊联系⼈电话" + (i+1) + "(只输⼊回⻋完成电话新
增): ");String number = scan.nextLine();if (number.isEmpty()) {break;}PeopleInfoRequest.Phone.Builder phoneBuilder = PeopleInfoRequest.Phone.newBuilder();phoneBuilder.setNumber(number);peopleBuilder.addPhone(phoneBuilder);}for(int i = 0; ; i++) {System.out.print("请输⼊备注" + (i+1) + "标题 (只输⼊回⻋完成备注新增):
");String remarkKey = scan.nextLine();if (remarkKey.isEmpty()) {break;}System.out.print("请输⼊备注" + (i+1) + "内容: ");String remarkValue = scan.nextLine();peopleBuilder.putRemark(remarkKey, remarkValue);}System.out.println("------------输⼊结束-----------");return peopleBuilder.build();}
}
BytesUtils.java:bytes⼯具类
public class BytesUtils { /*** 获取 bytes 有效⻓度 * @param bytes* @return*/public static int getValidLength(byte[] bytes){int i = 0;if (null == bytes || 0 == bytes.length)return i;for (; i < bytes.length; i++) {if (bytes[i] == '\0')break;}return i;}/*** 截取 bytes * @param b* @param off* @param length* @return*/public static byte[] subByte(byte[] b,int off,int length){byte[] b1 = new byte[length];System.arraycopy(b, off, b1, 0, length);return b1;}
}
服务端代码实现
import com.example.internet.service.dto.PeopleInfoRequest;
import com.example.internet.service.dto.PeopleInfoResponse;
import java.io.*;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
public class ContactsService {//服务器socket要绑定固定的端⼝ private static final int PORT = 8888;public static void main(String[] args) throws IOException {// 创建服务端DatagramSocket,指定端⼝,可以发送及接收UDP数据报 DatagramSocket socket = new DatagramSocket(PORT);// 不停接收客⼾端udp数据报 while (true){System.out.println("等待接收UDP数据报...");// 创建 request 数据报,⽤于接收客⼾端发送的数据 byte[] udpRequest = new byte[1024]; // 1m=1024kb, 1kb=1024byte,
UDP最多64k(包含UDP⾸部8byte) DatagramPacket requestPacket = new DatagramPacket(udpRequest,udpRequest.length);// 接收 request 数据报,在接收到数据报之前会⼀直阻塞, socket.receive(requestPacket);// 获取有效的 request int length = BytesUtils.getValidLength(udpRequest);byte[] requestData = BytesUtils.subByte(udpRequest, 0, length);// 反序列化 request PeopleInfoRequest request =
PeopleInfoRequest.parseFrom(requestData);System.out.println("接收到请求数据:");System.out.println(request.toString());// 构造 response PeopleInfoResponse response = PeopleInfoResponse.newBuilder().setUid("111111111").build();// 序列化 response byte[] responseData = response.toByteArray();// 构造 response 数据报,注意接收的客⼾端数据报包含IP和端⼝号,要设置到响应
的数据报中 DatagramPacket responsePacket = newDatagramPacket(responseData, responseData.length,requestPacket.getSocketAddress());// 发送 response 数据报 socket.send(responsePacket);}}
}
BytesUtils.java:bytes⼯具类
public class BytesUtils {/*** 获取 bytes 有效⻓度 * @param bytes* @return*/public static int getValidLength(byte[] bytes){int i = 0;if (null == bytes || 0 == bytes.length)return i;for (; i < bytes.length; i++) {if (bytes[i] == '\0')break;}return i;}/*** 截取 bytes * @param b* @param off* @param length* @return*/public static byte[] subByte(byte[] b,int off,int length){byte[] b1 = new byte[length];System.arraycopy(b, off, b1, 0, length);return b1;}
}