ProtoBuf 快速上手

关于 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;}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/5370.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

apt镜像源制作-ubuntu22.04

# 安装必要的软件 sudo apt-get install -y apt-mirror # 编辑/etc/apt/mirror.list,添加以下内容 set base_path /var/spool/apt-mirror # 指定要镜像的Ubuntu发布和组件-null dir jammy-updates main restricted universe multiverse # 镜像的Ubuntu发布和组件的URL-n…

TLU - Net:一种用于钢材表面缺陷自动检测的深度学习方法

摘要&#xff1a; 钢铁表面缺陷检测是钢铁板制造过程中的一个关键步骤。近年来&#xff0c;已经研究了许多基于机器学习的自动化视觉检测 (AVI) 方法。然而&#xff0c;由于 AVI 方法的训练时间和准确性问题&#xff0c;大多数钢铁制造行业仍然使用人工视觉检测。自动钢铁缺陷检…

设计模式讲解02—责任链模式(Chain)

1. 概述 定义&#xff1a;责任链模式是一种行为型模式&#xff0c;在这个模式中&#xff0c;通常创建了一个接收者对象的链来处理请求&#xff0c;该请求沿着链的顺序传递。直到有对象处理该请求为止&#xff0c;从而达到解耦请求发送者和请求处理者的目的。 解释&#xff1a;责…

Java | Leetcode Java题解之第542题01矩阵

题目&#xff1a; 题解&#xff1a; class Solution {static int[][] dirs {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};public int[][] updateMatrix(int[][] matrix) {int m matrix.length, n matrix[0].length;// 初始化动态规划的数组&#xff0c;所有的距离值都设置为一个很大…

ServletContext介绍

文章目录 1、ServletContext对象介绍1_方法介绍2_用例分析 2、ServletContainerInitializer1_整体结构2_工作原理3_使用案例 3、Spring案例源码分析1_注册DispatcherServlet2_注册配置类3_SpringServletContainerInitializer 4_总结 ServletContext 表示上下文对象&#xff0c;…

virtualBox部署minikube+istio

环境准备 virtualBox安装 直接官网下载后安装即可&#xff0c;网上也有详细教程。镜像使用的centos7。 链接&#xff08;不保证还可用&#xff09;&#xff1a;http://big.dxiazaicc.com/bigfile/100/virtualbox_v6.1.26_downcc.com.zip?auth_key1730185635-pWBtV8LynsxPD0-0-…

QT 实现绘制汽车仪表盘

1.界面实现效果 以下是具体的项目需要用到的效果展示,通常需要使用QPainter类来绘制各种图形和文本,包括一个圆形的仪表盘、刻度、指针和数字。 2.简介 分为以下几个部分,首先设置抗锯齿 painter.setRenderHint(QPainter::Antialiasing)。 QPainter p(this);p.setRender…

Node.js——fs模块-文件读取

1、文件读取&#xff1a;通过程序从文件中去除其中的数据 2、方法 方法 说明 readFile 异步读取 readFileSync 同步读取 createReadStrean 流式读取 3、readFile 异步读取 语法&#xff1a; 本文的分享到此结束&#xff0c;欢迎大家评论区一同讨论学习&#xff0c;下一…

cv2.threshold利用OSTU方法分割图像的前景和背景

OSTU方法&#xff0c;又称大津法或最大类间方差法&#xff0c;是一种在图像处理中广泛应用的自动阈值选择方法。该方法由日本学者大津&#xff08;Nobuyuki Otsu&#xff09;于1979年提出&#xff0c;旨在通过最大化前景与背景之间的类间方差来自动确定一个最佳阈值&#xff0c…

Perforce《2024游戏技术现状报告》Part2:游戏引擎、版本控制、IDE及项目管理等多种开发工具的应用分析

游戏开发者一直处于创新前沿。他们的实践、工具和技术受到各行各业的广泛关注&#xff0c;正在改变着组织进行数字创作的方式。 近期&#xff0c;Perforce发布了《2024游戏技术现状报告》&#xff0c;通过收集来自游戏、媒体与娱乐、汽车和制造业等高增长行业的从业者、管理人…

编写虚拟的GPIO控制器的驱动程序:和pinctrl的交互使用

往期内容 本专栏往期内容&#xff1a; Pinctrl子系统和其主要结构体引入Pinctrl子系统pinctrl_desc结构体进一步介绍Pinctrl子系统中client端设备树相关数据结构介绍和解析inctrl子系统中Pincontroller构造过程驱动分析&#xff1a;imx_pinctrl_soc_info结构体Pinctrl子系统中c…

数据结构代码题--排序算法(快排),二叉树的基本知识,链表的基本操作引申

排序算法&#xff1a; 完成比完美更重要&#xff01; 题目中常考的是平均时间复杂度&#xff1a;但是具体计算时&#xff0c;能用最坏就用最坏 插入&#xff1a;直接插&#xff0c;希尔 交换&#xff1a;冒泡&#xff0c;快排 选择&#xff1a;简单选择&#xff0c;堆排 归…

外包干了4年,技术退步太明显了。。。。。

先说一下自己的情况&#xff0c;本科生生&#xff0c;20年通过校招进入武汉某软件公司&#xff0c;干了差不多4年的功能测试&#xff0c;今年国庆&#xff0c;感觉自己不能够在这样下去了&#xff0c;长时间呆在一个舒适的环境会让一个人堕落!而我已经在一个企业干了四年的功能…

VS2013安装报错“windows程序兼容性模式已打开,请将其关闭 ”解决方案

windows程序兼容性模式已打开,请将其关闭 在安装VS2013语言包的时候报错&#xff1a;windows程序兼容性模式已打开,请将其关闭 还会经常遇到这个错误&#xff1a;有一个安装程序已经运行 第一个问题解决办法&#xff1a; 按winr&#xff0c;输入cmd 输入 安装包路径 /Uninstal…

fastapi_socketio连接vue的socktio.client

环境 windows 11 python 3.11 fastapi 0.108.0 fastapi-socketio 0.0.10 vue2 “socket.io-client”: “^4.6.1”, 提示&#xff1a;如果遇到跨域问题自行解决 fastapi 使用fastapi-scoketio下的SocketManager, 可以看到接口解释如下&#xff1a; 所以默认配置是客户端连接时…

使用 GitHub Actions 部署到开发服务器的详细指南

使用 GitHub Actions 部署到开发服务器的详细指南 在本篇博客中&#xff0c;我们将介绍如何使用 GitHub Actions 实现自动化部署&#xff0c;将代码从 GitHub 仓库的 dev 分支自动部署到开发服务器。通过这种方式&#xff0c;可以确保每次在 dev 分支推送代码时&#xff0c;服…

常见 HTTP 状态码分类和解释及服务端向前端返回响应时的最完整格式

目前的开发项目&#xff0c;准备明年的国产化&#xff0c;用了十年的自研系统借这个机会全部重写&#xff0c;订立更严格的规范&#xff0c;这里把返回格式及对应状态码记录一下。 常见 HTTP 状态码及解释 HTTP 状态码用于表示客户端请求的响应状态&#xff0c;它们分为五类&a…

使用PyCharm连接虚拟机运行spark任务,本地开发:远程提交测试

在本地写代码&#xff0c;右键运行&#xff0c;将代码自动提交到集群上 spark是Standalone集群 1) 集群环境准备好 #启动集群&#xff1a;第一台机器 start-dfs.sh cd /opt/installs/spark sbin/start-master.sh sbin/start-workers.sh sbin/start-history-server.sh 2) Wi…

XHCI 1.2b 规范摘要(12)

系列文章目录 XHCI 1.2b 规范摘要&#xff08;一&#xff09; XHCI 1.2b 规范摘要&#xff08;二&#xff09; XHCI 1.2b 规范摘要&#xff08;三&#xff09; XHCI 1.2b 规范摘要&#xff08;四&#xff09; XHCI 1.2b 规范摘要&#xff08;五&#xff09; XHCI 1.2b 规范摘要…

多分类logistic回归分析案例教程

因变量为无序多分类变量&#xff0c;比如研究成人早餐选择的相关因素&#xff0c;早餐种类包括谷物类、燕麦类、复合类&#xff0c;此时因变量有三种结局&#xff0c;而且三种早餐是平等的没有顺序或等级属性&#xff0c;此类回归问题&#xff0c;可以使用多分类Logistic回归进…