ProtoBuf学习和使用(二):proto3语法详解【字段规则、消息类型的定义与使用】enum类型、Any类型、oneof类型、map类型、默认值、更新消息、选项option、通讯录——网络版、总结

接上次博客:ProtoBuf的学习与使用(一):初识ProtoBuf【序列化概念、介绍、使用特点】、安装 ProtoBuf【Linux-Ubuntu、Windows】、学习思路、快速上手ProtoBuf-CSDN博客

目录

proto 3 语法详解

字段规则

消息类型的定义与使用

定义

使用

创建通讯录 2.0 版本

protobuf中常见常用的方法

通讯录 2.0 的写入实现

通讯录 2.0 的读取实现

enum 类型

定义规则

定义时注意

升级通讯录至 2.1 版本

更新 TestWrite.java (通讯录 2.1)

更新 TestRead.java (通讯录 2.1)

Any 类型

升级通讯录至 2.2 版本

更新 TestWrite.java (通讯录 2.2)

更新 TestRead.java (通讯录 2.2)

oneof 类型

升级通讯录至 2.3 版本

更新 TestWrite.java (通讯录 2.3)

更新 TestRead.java (通讯录 2.3)

map 类型

升级通讯录至 2.4 版本

更新 TestWrite.java (通讯录 2.4)

更新 TestRead.java (通讯录 2.4)

默认值

更新消息

更新规则

保留字段 reserved

未知字段

未知字段从哪获取?

UnknownFieldSet 类介绍

前后兼容性

选项 option

选项分类

JAVA 常用选项列举

设置自定义选项

通讯录 4.0 实现——网络版

代码环境

约定双端交互req/resp

客户端代码实现

服务端代码实现

总结

序列化能力对比验证


首先,我们来回顾一下上一次博客提到的一些重要概念,以加深大家对于protobuf的理解:

Protocol Buffers(protobuf)是一种用于序列化结构化数据的语言无关、平台无关、可扩展的机制。它可以用于通信协议、数据存储等多种场景,其主要作用是定义数据的结构以及提供一种高效、可扩展的方式来序列化和反序列化这些结构化数据。

以下是protobuf的主要功能和作用:

  1. 定义数据结构: Protobuf允许开发者使用简单的语法定义数据结构,这些数据结构通常称为消息类型(Message Types)。消息类型由字段组成,每个字段都有一个唯一的标识符和一个数据类型。这样的数据结构定义可以包括基本数据类型(如整数、字符串等)以及复杂的嵌套结构。

  2. 序列化和反序列化: Protobuf提供了一种将结构化数据序列化为字节流的方法,以及将字节流反序列化为结构化数据的方法。这种序列化和反序列化的过程是高效的,并且可以跨不同的编程语言和平台进行数据交换。

  3. 高效性: 由于Protobuf序列化后的数据是二进制格式,相比于XML和JSON等文本格式,它更加紧凑、高效。这使得Protobuf在网络传输和数据存储方面具有更好的性能。

  4. 可扩展性: Protobuf允许向已定义的消息类型中添加新的字段,而不会影响现有的序列化数据。这意味着系统可以轻松地进行演化和扩展,而不必担心向后兼容性问题。

  5. 代码生成: 使用Protobuf定义消息类型后,可以使用Protobuf编译器生成对应的代码,例如Java、C++、Python等语言的类和方法。这些自动生成的代码可以用来序列化、反序列化消息,以及进行其他操作,使得开发更加方便。

  6. 语言无关和平台无关: 由于Protobuf使用了语言无关和平台无关的格式来表示数据,因此消息定义和序列化后的数据可以在不同的编程语言和平台之间进行交互,而无需担心兼容性问题。

我们为什么需要序列化和反序列化结构化数据?目的是什么? 

序列化和反序列化结构化数据在软件开发中具有重要的作用,其主要目的包括:

  1. 数据持久化: 序列化将内存中的数据对象转换为字节序列,可以方便地存储到文件系统、数据库或者通过网络传输到其他系统。反序列化则可以将这些字节序列重新转换为内存中的数据对象,实现数据的持久化和恢复。

  2. 数据交换: 序列化使得不同系统之间可以以统一的格式交换数据,无论这些系统使用的是不同的编程语言、操作系统或者硬件平台。通过序列化和反序列化,可以实现跨系统的数据交换和通信。

  3. 远程调用(RPC): 在分布式系统中,远程调用(Remote Procedure Call,RPC)是一种常见的通信方式。序列化和反序列化允许客户端和服务器之间通过网络传输调用参数和返回值,实现远程方法的调用和结果的返回。

  4. 缓存和消息队列: 序列化和反序列化使得数据可以在缓存中进行存储和检索,例如在内存缓存、磁盘缓存或者分布式缓存中。同时,消息队列系统也会使用序列化和反序列化来存储和传递消息。

  5. 跨语言和平台兼容性: 使用序列化和反序列化可以实现不同编程语言和平台之间的数据交换和通信,使得系统可以更加灵活地集成和扩展,同时保持向后兼容性。

为什么要使用Protobuf?主要有以下几个原因:

  • 性能: 相比于文本格式(如XML、JSON),Protobuf序列化后的数据更加紧凑,序列化和反序列化的速度更快,因此在需要高性能的场景下,使用Protobuf可以提升系统的效率。

  • 可扩展性: Protobuf支持向已定义的消息类型中添加新的字段,同时保持向后兼容性。这使得系统可以灵活地演化和扩展,而无需修改现有的代码和数据格式。

  • 跨语言和平台: Protobuf生成的代码可以在多种编程语言和平台上使用,使得不同系统之间的数据交换更加方便。

  • 清晰的数据定义: 使用Protobuf定义数据结构可以使数据的结构更加清晰和易于理解,同时也可以提供更好的文档化和可维护性。

protobuf工具提供了一种简单的语法来定义消息类型和消息结构。然后,使用protobuf编译器(protoc)来处理这些定义文件,并根据定义生成相应的代码文件。生成的代码文件包括用于序列化和反序列化消息的方法、用于构建消息的构建器(Builder)类、以及其他辅助类和方法。

这样一来,开发人员只需专注于定义数据结构和消息内容,而无需关心底层的序列化和反序列化实现细节,protobuf工具会自动处理这些工作。这大大简化了开发过程,减少了出错的可能性,同时也提高了代码的可维护性和可读性。

总的来说,Protobuf是一种强大的工具,,为开发人员提供了一种简单、高效的方式来处理结构化数据,从而提高了开发效率和代码质量。同时可以帮助开发者在不同系统之间高效、可靠地交换和存储结构化数据,提升系统的性能和可维护性。

proto 3 语法详解

在语法详解部分,我们将通过逐步升级通讯录项目来完成教学,使用2.x版本表示升级的内容。在这个部分,我们将对通讯录进行多次改进,最终实现以下内容:

  1. 不再简单打印联系人的序列化结果,而是将整个通讯录序列化后写入文件中。
  2. 从文件中读取并解析通讯录,并进行打印。
  3. 新增联系人属性,包括姓名、年龄、电话信息、地址、其他联系方式以及备注。

通过逐步升级项目,我们将学习如何使用Protocol Buffers进行对象的序列化和反序列化,并且了解如何处理更加复杂的数据结构和属性。

字段规则

当定义消息类型时,字段可以使用不同的规则进行修饰,以确定其在消息中的行为。以下是两种常见的字段修饰规则:

  1. singular(单值)

    • 这种规则表示消息中的字段最多可以包含一个值。如果未设置该字段,则其值为默认值(例如0、false、空字符串等)。
    • 在proto3语法中,字段默认使用这种规则,因此如果未显式指定规则,则字段被视为单值字段。
    • 例如,如果定义了一个姓名字段为singular规则,则该消息中每个实例只能包含一个姓名值。
  2. repeated(重复值)

    • 这种规则允许消息中的字段包含任意数量的值,包括零个。字段的值以数组形式存储,并且重复值的顺序会被保留。
    • 在消息中可以多次出现相同的字段,并且每次出现的值都会被添加到该字段的值数组中。
    • 例如,如果定义了一个电话号码字段为repeated规则,则该消息中的每个实例可以包含任意数量的电话号码。

使用这两种规则可以根据需求灵活地定义消息结构,使得协议缓冲区可以有效地表示各种类型的数据,并且具有良好的扩展性和灵活性。

我们在 src/main/proto/proto3 目录下新建 contacts.proto 文件,内容如下:

// 首行: 语法指定行
syntax = "proto3";
package proto3;

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

// 定义联系人 message
message PeopleInfo {
  // 字段类型 字段名 = 字段唯一编号;
  string name = 1;
  int32 age = 2;
  repeated string phone_numbers = 3;
}

在PeopleInfo消息中新增phone_numbers字段,表明一个联系人可以有多个号码,因此将其设置为repeated规则。

消息类型的定义与使用

定义

当我们在一个单独的 .proto 文件中编写 Protocol Buffers 消息定义时,其实可以定义多个不同的消息体,每个消息体代表一个数据结构或实体。这意味着我们可以在同一个文件中定义多个不同的数据类型,每个类型有其自己的字段和结构。

此外,Protocol Buffers 还支持消息的嵌套,这意味着我们可以在一个消息体内部定义另一个消息体,从而创建多层嵌套的数据结构。这种嵌套的结构可以让我们更清晰地组织和描述复杂的数据模型,使其更具可读性和可维护性。

另外,虽然在同一个 .proto 文件中,每个字段必须具有唯一的编号,但这个编号在不同的消息体之间是可以重复的。因为字段编号的作用范围是在消息体内部,不同消息体之间的字段编号可以相同,不会相互冲突。

因此我们可以更新 contacts.proto,将 phone_number 提取出来,单独成为⼀个消息:

// 首行: 语法指定行
syntax = "proto3";
package proto3;

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

message Phone {
  string number = 1;
}

// 定义联系人 message
message PeopleInfo {
  // 字段类型 字段名 = 字段唯一编号;
  string name = 1;
  int32 age = 2;
}

或者也可以写成嵌套的形式:

// 定义联系人 message
message PeopleInfo {
  // 字段类型 字段名 = 字段唯一编号;
  string name = 1;
  int32 age = 2;
  message Phone {
    string number = 1;//这样字段编号也不重复
  }
}

使用

在 Protocol Buffers 中,我们可以在一个消息的字段中使用其他消息类型作为字段类型。这意味着我们可以创建更复杂的数据结构,通过组合不同的消息类型来表示更多的信息。

消息类型可作为字段类型使用:

// 首行: 语法指定行
syntax = "proto3";
package proto3;

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

// 定义联系人 message
message PeopleInfo {
  // 字段类型 字段名 = 字段唯一编号;
  string name = 1;
  int32 age = 2;
  message Phone {
    string number = 1;//这样字段编号也不重复
  }
  repeated Phone phone = 3;
}

此外,我们还可以使用 import 关键字导入其他 .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 消息:

当使用插件编译Protocol Buffers(protobuf)文件时,有一些注意点需要考虑,特别是在导入其他.proto文件时:

  1. 不能使用相对路径: 当在.proto文件中导入其他.proto文件时,通常会使用相对路径,例如import "./path/to/other.proto";。然而,在使用插件编译时,应该避免相对路径,而是使用绝对路径或者相对于protoSourceRoot设置的路径来导入其他.proto文件。相对路径可能会导致在不同环境中的不一致性或者错误,因此最好是使用指定的路径来确保可靠性和一致性。

  2. 应该导入protoSourceRoot设置的路径开始设置: 在使用插件编译时,应该以protoSourceRoot设置的路径作为起点来导入其他.proto文件。这个设置指定了protobuf文件的源代码根目录,插件编译器会在这个根目录下查找所需的文件。因此,我们应该确保我们的导入路径以该根目录开始,以便编译器能够正确地定位到需要导入的文件。

 

// 首行: 语法指定行
syntax = "proto3";
package proto3;

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

// 使用插件编译的注意点:
// 1、不能使用相对路径
// 2、应该导入 protoSourceRoot 设置的路径开始设置
import "proto3/phone.proto";

// 定义联系人 message
message PeopleInfo {
  // 字段类型 字段名 = 字段唯一编号;
  string name = 1;
  int32 age = 2;
  //此处需要写引入文件的package.
  repeated phone.Phone phone = 3;
}

编译成功: 

注:在 proto3 文件中可以导入 proto2 消息类型并使用它们,反之亦然。

创建通讯录 2.0 版本

在通讯录 2.0 版本中,我们需要对 contacts.proto 进行更新以支持存储通讯录列表。

首先,我们引入了一个新的消息类型 Contacts,用于表示整个通讯录。在 Contacts 消息中,我们使用了 repeated 关键字来定义一个字段 PeopleInfo,该字段可以存储多个联系人的信息。

每个联系人都是 PeopleInfo 消息类型的实例,包含了姓名、ID、电子邮件以及新增的电话号码等字段。通过这样的更新,我们可以方便地将多个联系人的信息组织成一个通讯录,并将其序列化到文件中。这样的设计使得通讯录变得更加灵活和可扩展,可以轻松地管理和操作大量的联系人信息。

// 首行: 语法指定行
syntax = "proto3";
package proto3;

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

// 定义联系人 message
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。这是因为我们在 .proto 文件中设置了 option java_multiple_files = true;。这个选项告诉编译器为每个自定义的消息类型生成两个文件。

  1. Contacts.java: 包含了 Contacts 消息类型的 Java 类定义,其中包括对 Contacts 消息的序列化、反序列化等操作的方法。

  2. ContactsOrBuilder.java: 是 Contacts 消息类型的构建器接口,定义了用于构建 Contacts 消息实例的方法。

  3. ContactsProtos.java: 包含了一些静态属性资源和注册扩展的方法。通常由 Protobuf 编译器生成,用于协助操作 Protobuf 消息。

  4. PeopleInfo.java: 包含了 PeopleInfo 消息类型的 Java 类定义,其中包括对 PeopleInfo 消息的序列化、反序列化等操作的方法。

  5. PeopleInfoOrBuilder.java: 是 PeopleInfo 消息类型的构建器接口,定义了用于构建 PeopleInfo 消息实例的方法。

这样的文件组织结构使得代码更加清晰和易于维护,每个消息类型都有对应的类和构建器接口,便于在项目中使用和操作。

对于 Protocol Buffers 消息类型的 Java 类,可以再次印证以下特点:

在消息类型(例如 PeopleInfo)的类中,通常包含以下内容:

  1. 获取字段值的方法: 对于每个字段,通常只提供获取其值的方法(例如 getName()),而没有设置值的方法。这是因为 Protocol Buffers 的消息类型是不可变的,一旦创建就不能更改其字段的值。

  2. 序列化和反序列化方法: Java 类中通常包含序列化和反序列化方法。在 MessageLite 接口中定义了序列化和反序列化的方法,例如 parseFrom() 和 writeTo()。这些方法使得消息可以被序列化为字节流或者从字节流中反序列化出来。

  3. newBuilder() 静态方法: 为了创建消息类型的构建器实例,通常会提供一个 newBuilder() 静态方法。构建器(Builder)用于构造消息对象的实例,并提供设置字段值的方法。

在构建器(Builder)类中,通常包含以下内容:

  1. build() 方法: 构建器类中会提供一个 build() 方法,用于构造出一个自定义类对象(例如 PeopleInfo 类对象)。这个方法会将构建器中设置的字段值应用到最终的消息对象中,并返回该对象。

  2. 字段操作方法: 编译器为每个字段提供了获取和设置方法,以及一些其他的操作方法。这些方法包括设置字段值、清除字段值等,使得构建消息对象更加方便和灵活。

以上特点是 Protocol Buffers 消息类型在 Java 类中的常见表现,符合 Protocol Buffers 的设计理念和使用约定。

且在上述例子中:

  1. clear_ 方法: 对于每个字段,在构建器(Builder)类中都会提供一个 clear_ 方法,用于将该字段重新设置为默认的空状态。例如,对于字段 name,可能会提供一个 clearName() 方法,用于清除该字段的值。

  2. mergeFrom(Message other) 方法: 这个方法允许将另一个消息对象(other)的内容合并到当前消息对象中。如果字段是单数域(non-repeated field),则会覆盖当前消息对象中的值;如果字段是重复值(repeated field),则会将另一个消息对象的值追加到当前消息对象中。

  3. 针对 repeated 字段的操作方法: 对于使用 repeated 修饰的字段,即数组类型,在构建器(Builder)类中会提供一系列的 add 方法,用于新增一个值或一个构建器对象到该数组中。例如,如果有一个 phoneNumbers 字段被定义为 repeated,则可能会提供 addPhoneNumber() 方法来添加一个电话号码。同时,也会提供一个 getXXXCount() 方法来获取数组中存放元素的个数,例如 getPhoneNumbersCount() 方法来获取电话号码数组中的元素个数。

这些方法为开发者提供了方便的途径来操作消息对象的字段,使得对消息的处理更加灵活和便捷。

protobuf中常见常用的方法

解释到这里,我干脆直接大概给出Protocol Buffers 中提供的常见常用的方法,因为我们在后续的代码编写过程中也经常会使用到。

在 Protocol Buffers 中,获取字段值通常是通过消息类(Message Class)提供的一系列获取字段相关的方法,这些方法用于获取消息中字段的值、检查字段是否存在以及获取重复字段的元素个数等。这些方法提供了方便的途径来访问消息中的数据。

而设置字段值通常是通过消息类的构建器类(Builder Class)提供的一系列方法来完成的。构建器类除了提供了 build() 方法用于构建消息对象之外,还提供了一系列方法用于设置消息中字段的值、清除字段值、合并字段值等操作。这些方法可以用于构建消息对象实例,并提供了对消息字段的灵活管理和操作。

因此,通过消息类的方法获取字段值,通过构建器类的方法设置字段值,开发者可以方便地进行结构化数据的读取、修改和构建,实现了对数据的高效管理和处理。

首先是我们获取字段相关的方法:

在 Protocol Buffers 中,生成的类(通常是消息类和构建器类)会提供一系列方法来获取消息中定义的字段值。这些方法通常包括以下几种类型:

  1. Getter 方法:

    • 用于获取单个字段值的方法,一般以字段名为基础,采用驼峰命名规则,例如 getName()、getAge()。
    • 如果字段是一个标量类型(如整数、字符串等),则直接返回该字段的值。
    • 如果字段是一个消息类型,则返回对应消息类型的对象。
  2. hasXxx() 方法:

    • 用于检查消息中是否存在某个字段的值,通常以字段名的驼峰形式加上前缀“has”来命名,例如 hasName()、hasAge()。
    • 如果消息中存在该字段的值,则返回 true;否则返回 false。
  3. Count 方法(适用于重复字段):

    • 对于重复字段(repeated field),通常会提供一个获取字段元素个数的方法,以“get”加上字段名和“Count”组成,例如 getPhonesCount()、getAddressesCount()。
    • 返回该重复字段中元素的个数。
  4. Oneof Case 方法(适用于 Oneof 字段):

    • 对于 Oneof 字段,通常会提供一个方法来获取当前 Oneof 中激活的字段名,一般以“get”加上 Oneof 名称和“Case”组成,例如 getGenderCase()、getContactInfoCase()。
    • 返回当前激活的字段名,如果没有激活的字段,则返回默认值或者一个特定的枚举值。
  5. 其他辅助方法:除了上述方法之外,还可能会提供一些其他的辅助方法,用于获取字段的特定属性或者执行特定操作,例如清除字段值、合并字段值等。

一些常见的辅助方法,它们通常与消息类或构建器类一起提供,用于对消息字段进行操作和管理: 

  1. clearXxx() 方法:

    • 用于清除消息中特定字段的值,一般以字段名为基础,加上前缀“clear”,例如 clearName()、clearAge()。
    • 在调用该方法后,该字段将被重置为默认值或者空状态。
  2. setDefaultXxx() 方法:

    • 用于设置消息中特定字段的默认值,一般以字段名为基础,加上前缀“setDefault”,例如 setDefaultName()、setDefaultAge()。
    • 可以用于设置消息在某些情况下的默认值,而不是采用消息定义中指定的默认值。
  3. mergeFrom(Message other) 方法:

    • 用于将另一个消息对象(other)的内容合并到当前消息对象中。
    • 如果字段是单值域(non-repeated field),则会覆盖当前消息对象中的值;如果字段是重复域(repeated field),则会将另一个消息对象的值追加到当前消息对象中。
  4. copyFrom(Message other) 方法:

    • 用于将另一个消息对象(other)的内容复制到当前消息对象中。
    • 与 mergeFrom 方法不同的是,copyFrom 方法会先清除当前消息对象中的所有字段值,然后再复制其他消息对象的值。
  5. isInitialized() 方法:

    • 用于检查消息对象的字段是否已经被初始化,即是否存在所有必需字段的值。
    • 如果消息对象中的所有必需字段都已经被设置了值,则返回 true;否则返回 false。

这些方法组合在一起,为开发者提供了方便的途径来访问和操作消息中的字段值,使得对结构化数据的处理更加灵活和便捷。

其次是一些序列化方法和反序列化方法:

在 Protocol Buffers 中,生成的类(通常是消息类和构建器类)会提供一系列方法来支持消息的序列化和反序列化:

  1. 序列化方法:

    • toByteArray() 方法:

      • 将消息对象序列化为字节数组。
      • 返回表示消息对象的字节数组,可用于网络传输或存储到文件系统。
    • writeTo(OutputStream output) 方法:

      • 将消息对象序列化为字节流并写入到指定的输出流中。
      • 可以通过指定的输出流将消息对象的字节表示写入到网络连接、文件或其他输出目标中。
  2. 反序列化方法:

    • parseFrom(byte[] data) 方法:

      • 从字节数组中反序列化消息对象。
      • 接受一个字节数组作为参数,返回解析后的消息对象。
    • parseFrom(InputStream input) 方法:

      • 从输入流中读取字节流并反序列化消息对象。
      • 接受一个输入流作为参数,返回从输入流中解析出的消息对象。
  3. 构建器(Builder)相关方法:

    • mergeFrom(byte[] data) 方法:

      • 将字节数组中的数据合并到当前构建器对象中。
      • 可以用于将序列化后的数据合并到现有的消息构建器中,用于反序列化消息。
    • mergeFrom(InputStream input) 方法:

      • 从输入流中读取字节流并将数据合并到当前构建器对象中。
      • 可以用于从输入流中读取数据并合并到现有的消息构建器中,用于反序列化消息。

以上这些常见的序列化和反序列化方法使得开发者可以方便地将消息对象转换为字节表示(序列化),以及将字节表示转换回消息对象(反序列化),实现了消息的持久化、数据交换等功能。

最后是class Builder里面除去 build( ) 方法之外的处理字段的相关方法:

在 Protocol Buffers 中,生成的 Builder 类用于构建消息对象的实例,并提供了一系列方法来处理字段的设置和操作。除了常见的 build() 方法之外,Builder 类还通常提供以下一些方法来处理字段:

  1. 设置字段值的方法:

    • 对于每个字段,Builder 类通常会提供一个对应的设置方法,以便设置该字段的值。这些方法通常以字段名为基础,采用驼峰命名规则,并在前面加上 "set" 前缀,例如 setName()、setAge()。
    • 如果字段是一个标量类型,则设置方法接受相应类型的参数,例如字符串、整数等。
    • 如果字段是一个消息类型,则设置方法通常接受对应消息类型的构建器对象作为参数,例如 setAddress(Address.Builder address)。
  2. 清除字段值的方法:对于每个字段,Builder 类通常也会提供一个清除方法,以便将该字段的值重置为默认值或空状态。这些方法通常以字段名为基础,采用驼峰命名规则,并在前面加上 "clear" 前缀,例如 clearName()、clearAge()。

  3. 其他字段操作的方法:

    • addXxx() 方法(适用于重复字段):如果字段是一个重复字段(repeated field),Builder 类通常会提供一个添加元素的方法,以便将一个新值或构建器对象添加到该字段中。这些方法通常以字段名为基础,采用驼峰命名规则,并在前面加上 "add" 前缀,例如 addPhoneNumber()、addAddress()。
    • addAllXxx() 方法(适用于重复字段):如果字段是一个重复字段,Builder 类还可能提供一个添加多个元素的方法,以便将一个集合或数组中的所有值添加到该字段中。
    • clearXxx() 方法(适用于重复字段):与单值字段的清除方法类似,对于重复字段,Builder 类还可能提供一个清除方法,以便清除该字段中的所有值。

通讯录 2.0 的写入实现

package com.example.proto3;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Scanner;

public class TestWrite {
    public static void main(String[] args) throws IOException {
        Contacts.Builder contactsBuilder = Contacts.newBuilder();
//      第①种方法:
//      读取本地已存在的 contacts.bin,反序列化出通讯录对象
//        Contacts contacts = Contacts.parseFrom(
//                new FileInputStream("src/main/java/com/example/proto3/contacts.bin"));
//        contactsBuilder = contacts.toBuilder();

//      第②种方法:
        try {
            contactsBuilder.mergeFrom(
                    new FileInputStream("src/main/java/com/example/proto3/contacts.bin"));
        } catch (FileNotFoundException e) {
            System.out.println("contacts.bin not find, create new file");
        }

        // 向通讯录中新增一个联系人
        contactsBuilder.addContacts(addPeopleInfo());

        // 序列化通讯录,将结果写入文件中
        FileOutputStream outputStream = new FileOutputStream(
                "src/main/java/com/example/proto3/contacts.bin");
        contactsBuilder.build().writeTo(outputStream);
        outputStream.close();
    }

    private static PeopleInfo addPeopleInfo() {
        PeopleInfo.Builder builder = PeopleInfo.newBuilder();
        Scanner scanner = new Scanner(System.in);
        System.out.println("--------------新增联系人-------------");
        System.out.print("请输入联系人姓名:");
        String name = scanner.nextLine();
        builder.setName(name);

        System.out.print("请输入联系人年龄:");
        int age = scanner.nextInt();
        scanner.nextLine();
        builder.setAge(age);

        for (int i = 0;; i++) {
            System.out.print("请输入联系人电话" + (i+1) + "(只输⼊回⻋完成电话新增): ");
            String number = scanner.nextLine();
            if (number.isEmpty()) {
                break;
            }
            PeopleInfo.Phone.Builder phoneBuilder = PeopleInfo.Phone.newBuilder();
            phoneBuilder.setNumber(number);
            builder.addPhone(phoneBuilder);
        }

        System.out.println("-----------添加联系人结束-------------");
        return builder.build();
    }
}

在上面我们写的这段代码中,包含了两种方式来读取已经存在的通讯录文件(contacts.bin)并将其内容反序列化为通讯录对象,然后再将其内容添加到一个新的通讯录构建器中。 

第一种方法:这种方法首先从文件中读取已存在的通讯录对象(contacts.bin),然后通过 parseFrom() 方法将其字节流反序列化为一个通讯录对象(Contacts 类型)。接着,使用 toBuilder() 方法创建一个新的构建器对象,将反序列化后的通讯录对象内容复制到这个构建器中。这样做的目的是为了在已有通讯录对象的基础上进行修改和添加操作,保留了之前通讯录的内容。

第二种方法:这种方法是尝试直接将已存在的通讯录文件内容合并到当前的构建器对象中。如果文件存在,则调用 mergeFrom() 方法从文件中读取字节流并将其合并到当前的构建器对象中;如果文件不存在,则会捕获 FileNotFoundException 异常并输出相应的提示信息。这样做的目的是在文件存在时,直接将文件内容合并到构建器中,而不需要额外的步骤去创建新的通讯录对象。

总的来说,这两种方法都是用来读取已存在的通讯录文件并将其内容反序列化为通讯录对象,然后再将其内容添加到新的通讯录构建器中。它们的差别在于第一种方法会先创建一个新的构建器对象并将文件内容复制到该构建器中,而第二种方法直接在当前构建器对象中合并文件内容。

我们运行一下:

通讯录 2.0 的读取实现

package com.example.proto3;

import com.google.protobuf.InvalidProtocolBufferException;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Map;

public class TestRead {
    public static void main(String[] args) throws IOException {

        // 读取文件,将读取的内容进行反序列化
        Contacts contacts = Contacts.parseFrom(
                new FileInputStream("src/main/java/com/example/proto3/contacts.bin"));

        // 打印
        printContacts(contacts);
    }

    private static void printContacts(Contacts contacts) throws InvalidProtocolBufferException {
        int i = 1;
        for (PeopleInfo peopleInfo : contacts.getContactsList()) {
            System.out.println("-----------联系人" + 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);
        }
    }
}

另⼀种验证方法——toString()

在自定义消息类的父抽象类 AbstractMessage 中重写 toString() 方法是一种常见的做法。通过重写这个方法,可以确保消息对象在打印时输出一个易于阅读和理解的字符串表示形式,从而方便开发人员在调试过程中查看消息对象的内容。

在 AbstractMessage 中重写 toString() 方法,可以返回消息对象的各个字段及其对应的值,通常按照一定的格式排列,以便开发人员更容易地理解和分析。这个可读性高的表示形式可以包括字段名和字段值的对应关系,也可以包括其他有用的信息,例如消息对象的类型、版本号等。

通过重写 toString() 方法,开发人员可以在需要打印消息对象时直接调用 toString() 方法,而不必手动遍历并打印每个字段。这样可以节省开发人员的时间和精力,并提高代码的可维护性和可读性。

例如在 TestRead 类的 main 函数中调用⼀下:

package com.example.proto3;

import com.google.protobuf.InvalidProtocolBufferException;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Map;

public class TestRead {
    public static void main(String[] args) throws IOException {

        // 读取文件,将读取的内容进行反序列化
        Contacts contacts = Contacts.parseFrom(
                new FileInputStream("src/main/java/com/example/proto3/contacts.bin"));

        // 打印
        System.out.println(contacts.toString());
    }

    private static void printContacts(Contacts contacts) throws InvalidProtocolBufferException {
        int i = 1;
        for (PeopleInfo peopleInfo : contacts.getContactsList()) {
            System.out.println("-----------联系人" + 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);
        }
    }
}

这里的字符是把我们的UTF-8的汉字转化为了八进制输出: 

enum 类型

定义规则

当在.proto文件中定义枚举类型时,需要遵循一定的书写规范:

  1. 枚举类型名称:使用驼峰命名法,首字母大写。这有助于提高代码的可读性和一致性。例如,如果枚举表示颜色,可以命名为ColorType或者ColorEnum。

  2. 常量值名称:常量值通常使用全大写字母表示,并且多个单词之间使用下划线连接。这种命名约定有助于清晰地区分枚举中的不同选项,并且提高了代码的可读性。例如,如果枚举表示不同的方向,可以命名为:UP、DOWN、LEFT、RIGHT等。

我们可以定义⼀个名为 PhoneType 的枚举类型,定义如下:

// 首行: 语法指定行
syntax = "proto3";
package proto3;

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


// 0值必须存在,且作为第一个枚举常量的值
// 枚举值范围:32位整数范围,不要设置负数
enum PhoneType {
  MP = 0;  // 移动电话
  TEL = 1; // 固定电话
}

// 定义联系人 message
message PeopleInfo {
  // 字段类型 字段名 = 字段唯一编号;
  string name = 1;
  int32 age = 2;
    message Phone {
      string number = 1;//这样字段编号也不重复
    }
  repeated Phone phone = 3;
}
message Contacts {
  repeated PeopleInfo contacts = 1;
}

也支持嵌套的枚举类型:

// 定义联系人 message
message PeopleInfo {
  // 字段类型 字段名 = 字段唯一编号;
  string name = 1;
  int32 age = 2;
    message Phone {
      string number = 1;//这样字段编号也不重复
      // 0值必须存在,且作为第一个枚举常量的值
      // 枚举值范围:32位整数范围,不要设置负数
      enum PhoneType {
        MP = 0;  // 移动电话
        TEL = 1; // 固定电话
      }
      PhoneType type = 2;
    }
  repeated Phone phone = 3;
}
message Contacts {
  repeated PeopleInfo contacts = 1;
}

枚举类型的定义需要遵循以下几种规则:

  1. 0值常量必须存在:枚举类型中的第一个常量值必须为0,且作为第一个元素。这样做是为了与早期版本(proto2)的语义兼容,其中第一个元素被视为默认值,而默认值为0。因此,无论何时需要一个未指定值的默认情况,都可以使用0值。

  2. 枚举类型的位置:枚举类型可以在消息外部定义,也可以在消息体内部定义(即嵌套在其他消息中)。这样可以根据需要将枚举类型与相关消息进行组织和分组。

  3. 常量值范围:枚举类型的常量值必须在32位整数的范围内。然而,由于负值在编码规则中通常无效,因此不建议使用负值。

举例说明:

// 枚举类型定义在消息外部
enum Direction {
  UNKNOWN = 0;
  NORTH = 1;
  SOUTH = 2;
  EAST = 3;
  WEST = 4;
}

// 在消息体内部定义嵌套枚举类型
message Status {
  enum StatusCode {
    OK = 0;
    ERROR = 1;
    INVALID = 2;
  }

  StatusCode code = 1;
  string message = 2;
}

在上面中,第一个规则确保了每个枚举类型的第一个常量为0,以符合proto2的语义。第二个规则说明了枚举类型可以位于消息外部或内部进行定义。最后,第三个规则强调了常量值必须在32位整数的范围内,并且不建议使用负值。

定义时注意

当在单个 .proto 文件中定义两个具有相同枚举值名称的枚举类型时,编译后会报错,提示某个常量已经被定义。因此,需要注意以下几点:

  1. 同级枚举类型的常量不能重名:在同一层级(同级)的枚举类型中,各个枚举类型中的常量不能重名。如果两个枚举类型处于同一层级且具有相同的常量名称,则会导致编译错误。

  2. 最外层枚举类型和嵌套枚举类型不算同级:单个 .proto 文件中的最外层枚举类型和嵌套在消息内部的枚举类型不被视为同级。因此,它们可以具有相同的常量名称而不会引发编译错误。

  3. 多个 .proto 文件下的枚举类型层级关系:如果多个 .proto 文件位于同一目录且没有声明 package,那么它们中的枚举类型都被视为处于最外层,因此它们之间算是同级。在这种情况下,如果两个 .proto 文件中的枚举类型具有相同的常量名称,则会导致编译错误。

  4. 多个 .proto 文件下的枚举类型声明 package:如果多个 .proto 文件位于不同的 package 中,则它们的枚举类型不算是同级。在这种情况下,即使两个 .proto 文件中的枚举类型具有相同的常量名称,也不会引发编译错误。

综上所述,要避免枚举类型常量重名的编译错误,需要注意枚举类型的层级关系和 package 的声明。

升级通讯录至 2.1 版本

更新 contacts.proto (通讯录 2.1),新增枚举字段并使用,更新内容如下:

// 首行: 语法指定行
syntax = "proto3";
package proto3;

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

// 定义联系人 message
message PeopleInfo {
  // 字段类型 字段名 = 字段唯一编号;
  string name = 1;
  int32 age = 2;
    message Phone {
      string number = 1;//这样字段编号也不重复
      // 0值必须存在,且作为第一个枚举常量的值
      // 枚举值范围:32位整数范围,不要设置负数
      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 extends com.google.protobuf.GeneratedMessageV3
    implements PeopleInfoOrBuilder {
    
    // Phone 类的定义
    public static final class Phone extends com.google.protobuf.GeneratedMessageV3
        implements PhoneOrBuilder {
        
        // 新增的枚举类型 PhoneType
        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;
            
            // 获取枚举值对应的 PhoneType
            public static PhoneType valueOf(int value) {...}
        }
        
        // Phone 类的字段
        private int type_ = 0;
        
        // 获取 type 字段的值
        public int getTypeValue() {...}
        
        // 获取 type 字段对应的 PhoneType 枚举值
        public com.example.proto3.PeopleInfo.Phone.PhoneType getType() {...}
        
        // Phone 构建器类
        public static final class Builder extends com.google.protobuf.GeneratedMessageV3.Builder<Builder>
            implements com.example.proto3.PeopleInfo.PhoneOrBuilder {
            
            // Builder 类的字段
            private int type_ = 0;
            
            // 获取 type 字段的值
            public int getTypeValue() {...}
            
            // 设置 type 字段的值
            public Builder setTypeValue(int value) {...}
            
            // 获取 type 字段对应的 PhoneType 枚举值
            public com.example.proto3.PeopleInfo.Phone.PhoneType getType() {...}
            
            // 设置 type 字段的值
            public Builder setType(com.example.proto3.PeopleInfo.Phone.PhoneType value) {...}
            
            // 清除 type 字段的值
            public Builder clearType() {...}
        }
    }
}

在.proto文件中定义的枚举类型在编译生成的代码中会有相应的枚举类型定义。对于使用了这些枚举类型的字段,在生成的Builder类中会包含设置、获取和清空字段的方法。

所以可以看到上述代码中,定义了一个 PeopleInfo 类和其内部的 Phone 类。Phone 类包含了一个枚举类型 PhoneType,表示电话的类型。Phone 类中的字段 type_ 表示电话的类型。在 Phone 类的构建器 Builder 中,提供了一系列方法用于处理 type_ 字段,包括获取、设置和清除等操作。

更新 TestWrite.java (通讯录 2.1)

package com.example.proto3;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Scanner;

public class TestWrite {
    public static void main(String[] args) throws IOException {
        Contacts.Builder contactsBuilder = Contacts.newBuilder();
//      第①种方法:
//      读取本地已存在的 contacts.bin,反序列化出通讯录对象
//        Contacts contacts = Contacts.parseFrom(
//                new FileInputStream("src/main/java/com/example/proto3/contacts.bin"));
//        contactsBuilder = contacts.toBuilder();

//      第②种方法:
        try {
            contactsBuilder.mergeFrom(
                    new FileInputStream("src/main/java/com/example/proto3/contacts.bin"));
        } catch (FileNotFoundException e) {
            System.out.println("contacts.bin not find, create new file");
        }

        // 向通讯录中新增一个联系人
        contactsBuilder.addContacts(addPeopleInfo());

        // 序列化通讯录,将结果写入文件中
        FileOutputStream outputStream = new FileOutputStream(
                "src/main/java/com/example/proto3/contacts.bin");
        contactsBuilder.build().writeTo(outputStream);
        outputStream.close();
    }

    private static PeopleInfo addPeopleInfo() {
        PeopleInfo.Builder builder = PeopleInfo.newBuilder();
        Scanner scanner = new Scanner(System.in);
        System.out.println("--------------新增联系人-------------");
        System.out.print("请输入联系人姓名:");
        String name = scanner.nextLine();
        builder.setName(name);

        System.out.print("请输入联系人年龄:");
        int age = scanner.nextInt();
        scanner.nextLine();
        builder.setAge(age);

        for (int i = 0;; i++) {
            System.out.print("请输入联系人电话" + (i+1) + "(只输⼊回⻋完成电话新增): ");
            String number = scanner.nextLine();
            if (number.isEmpty()) {
                break;
            }
            PeopleInfo.Phone.Builder phoneBuilder = PeopleInfo.Phone.newBuilder();
            phoneBuilder.setNumber(number);

            System.out.print("请输入此电话类型(1、移动电话    2、固定电话)");
            int type = scanner.nextInt();
            scanner.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;
            }

            builder.addPhone(phoneBuilder);
        }

        System.out.println("-----------添加联系人结束-------------");
        return builder.build();
    }
}

更新 TestRead.java (通讯录 2.1)

package com.example.proto3;

import com.google.protobuf.InvalidProtocolBufferException;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Map;

public class TestRead {
    public static void main(String[] args) throws IOException {

        // 读取文件,将读取的内容进行反序列化
        Contacts contacts = Contacts.parseFrom(
                new FileInputStream("src/main/java/com/example/proto3/contacts.bin"));

        // 打印
        printContacts(contacts);
        //System.out.println(contacts.toString());
    }

    private static void printContacts(Contacts contacts) throws InvalidProtocolBufferException {
        int i = 1;
        for (PeopleInfo peopleInfo : contacts.getContactsList()) {
            System.out.println("-----------联系人" + 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()
                        + "   (" + phone.getType().name() + ")");
            }
        }
    }
}

如果在反序列化过程中,遇到枚举字段没有对应的值,Protocol Buffers 会将该字段设置为枚举类型的默认值,而枚举类型的默认值通常是枚举值为 0 的项。这种行为是 Protocol Buffers 的默认行为,旨在确保反序列化后的对象的字段始终有一个有效的值。
在反序列化 PeopleInfo对象时,枚举字段没有对应的值,那么在反序列化后,type_ 字段将会被设置为默认值 0,即对应枚举类型 PhoneType 的第一项 MP。这样做是为了保证对象的完整性和一致性,在后续的处理中也可以更方便地判断字段是否已经被设置。

如果需要在反序列化时自定义枚举类型的默认值,可以通过在消息定义时设置 default 属性来实现。例如:

enum PhoneType {
    MP = 0 [default = MP]; // 设置默认值为 MP
    TEL = 1;
}

通过将 default 属性设置为 MP,将确保在反序列化时,如果没有为枚举字段指定值,该字段将被设置为默认值 MP。这样可以确保反序列化后的对象始终具有正确的默认值,而不是默认的枚举类型第一项。

Any 类型

当字段声明为Any类型时,它可以存储任意类型的消息对象。这种灵活性使得在处理不确定类型的数据时变得更加方便。在使用Any类型时,通常会将其声明为repeated,表示可以存储多个任意类型的消息对象。

Any类型是由Google预先定义好的类型,其定义在Google的.proto文件中。在安装Protocol Buffers时,可以在include目录下找到这些已经定义好的.proto文件。通过引入这些文件,我们就可以使用Any类型来存储任意类型的消息对象。

在生成的代码中,对于使用了Any类型的字段,在Builder类中会包含设置和获取字段的方法,以及清空字段的方法clear_。这些方法使得在处理Any类型字段时更加方便,可以轻松地操作其中存储的消息对象。

syntax = "proto3";

package google.protobuf;

option csharp_namespace = "Google.Protobuf.WellKnownTypes";
option go_package = "github.com/golang/protobuf/ptypes/any";
option java_package = "com.google.protobuf";
option java_outer_classname = "AnyProto";
option java_multiple_files = true;
option objc_class_prefix = "GPB";

message Any {
  // Note: this functionality is not currently available in the official
  // protobuf release, and it is not used for type URLs beginning with
  // type.googleapis.com.
  //
  // Schemes other than `http`, `https` (or the empty scheme) might be
  // used with implementation specific semantics.
  //
  string type_url = 1;

  // Must be a valid serialized protocol buffer of the above specified type.
  bytes value = 2;
}
// 首行: 语法指定行
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";
// 定义联系人 message
message PeopleInfo {
  // 字段类型 字段名 = 字段唯一编号;
  string name = 1;
  int32 age = 2;
    message Phone {
      string number = 1;//这样字段编号也不重复
      // 0值必须存在,且作为第一个枚举常量的值
      // 枚举值范围:32位整数范围,不要设置负数
      enum PhoneType {
        MP = 0;  // 移动电话
        TEL = 1; // 固定电话
      }
      PhoneType type = 2;
    }
  repeated Phone phone = 3;
  google.protobuf.Any data = 4;
}

message Contacts {
  repeated PeopleInfo contacts = 1;
}

在 Protocol Buffers 中,可以使用 Any 类型来表示字段具有任意消息类型的能力,类似于泛型类型。使用 Any 类型的字段,可以在其中存储任意消息类型的数据,而不需要提前指定具体的消息类型。这为应用程序提供了更大的灵活性和可扩展性,使得它们可以处理各种类型的数据,而不受限于固定的消息结构。 

升级通讯录至 2.2 版本

在通讯录 2.2 版本中,新增了联系人的地址信息。为了支持这一变更,我们可以使用any类型的字段来存储地址信息。

通过使用any类型字段,我们可以灵活地存储各种类型的地址信息,例如家庭地址、办公地址等,而无需提前确定存储地址信息的具体类型。

使用any类型字段的优势在于它可以存储任意类型的消息对象,这样我们就可以根据需要存储不同格式或结构的地址信息。例如,某些联系人可能只有简单的文本地址,而另一些联系人可能有复杂的结构化地址信息。通过使用any类型字段,我们可以统一存储这些不同类型的地址信息,使得通讯录的数据结构更加灵活和通用。

在通讯录 2.2 版本中,添加了any类型的字段用于存储地址信息,这样就可以更好地满足不同联系人的需求,并且为将来可能的变化提供了扩展性。

更新 contacts.proto (通讯录 2.2),更新内容如下:

// 首行: 语法指定行
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";
// 定义联系人 message
message PeopleInfo {
  // 字段类型 字段名 = 字段唯一编号;
  string name = 1;
  int32 age = 2;
    message Phone {
      string number = 1;//这样字段编号也不重复
      // 0值必须存在,且作为第一个枚举常量的值
      // 枚举值范围:32位整数范围,不要设置负数
      enum PhoneType {
        MP = 0;  // 移动电话
        TEL = 1; // 固定电话
      }
      PhoneType type = 2;
    }
  repeated Phone phone = 3;
  google.protobuf.Any data = 4;
}
message Contacts {
  repeated PeopleInfo contacts = 1;
}

message Address {
  string home_address = 1;
  string unit_address = 2;
}

使用 Maven 插件编译后,生成了两个文件:Address.java和AddressOrBuilder.java。同时,PeopleInfo.java也进行了更新。

在Address.java文件中,通常会包含以下内容:

  • 获取地址信息字段的get方法,用于获取地址信息。
  • 反序列化方法,用于将二进制数据反序列化为Address对象。
  • 静态方法newBuilder(),用于创建Address对象的构建器。

在Address类的Builder内部类中,通常会包含以下内容:

  • build()方法,用于构造Address类的对象。
  • 一系列操作字段的方法,如get、set、clear等,用于设置或清除字段值。

这些方法的存在使得对地址信息的处理变得更加方便和灵活。通过Address.java和AddressOrBuilder.java文件,我们可以进行地址信息的获取、设置、序列化和反序列化等操作,从而更好地管理通讯录中的数据。

对于更新的 PeopleInfo.java ,我们来看看其对于 Any 字段更新的内容:

• has 方法用于检测当前字段是否被设置。它返回一个布尔值,指示该字段是否包含有效数据。

• set 方法用于设置字段的值,要求传入一个 Any 类型的对象作为参数。这个方法允许存储任意类型的消息对象。

class Builder里面也增加了新的方法:

 

但是你会发现,这些方法里面操作的都是一个Any类型,那么我们要怎么取到这个Any类型呢?

• 关于 Any 类型:

pack 方法:

  • pack 方法用于将给定的消息对象打包成一个 Any 类型的消息。
  • pack 方法有两个重载版本,一个是只接受消息对象作为参数,另一个是接受消息对象和类型 URL 前缀作为参数。
  • 在方法中,首先构建一个 Any 类型的构建器对象,然后设置类型 URL 和消息的字节序列值,最后构建并返回打包好的 Any 类型的消息对象。

is 方法:

  • is 方法用于检查当前 Any 类型的消息对象是否与给定的类类型相匹配。
  • 方法接受一个类类型作为参数,然后获取该类类型的默认实例,并检查 Any 类型消息的类型 URL 是否与该类类型的消息的全限定名相匹配,如果匹配则返回 true,否则返回 false。

unpack 方法:

  • unpack 方法用于将 Any 类型的消息对象解包成指定的消息类型。
  • 方法接受一个类类型作为参数,然后根据该类类型获取默认实例,并解析 Any 类型的消息对象的字节序列值为指定类型的消息对象。
  • 在解析过程中,方法会先检查 Any 类型的消息对象是否与给定的类类型匹配,如果匹配则解析并返回指定类型的消息对象,否则抛出异常说明类型不匹配。

这些方法可以帮助开发者在处理 Any 类型的消息时,进行打包、解包和类型匹配的操作,实现了对任意类型消息的存储、检索和处理。

更新 TestWrite.java (通讯录 2.2)

package com.example.proto3;

import com.google.protobuf.Any;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Scanner;

public class TestWrite {
    public static void main(String[] args) throws IOException {
        Contacts.Builder contactsBuilder = Contacts.newBuilder();
//      第①种方法:
//      读取本地已存在的 contacts.bin,反序列化出通讯录对象
//        Contacts contacts = Contacts.parseFrom(
//                new FileInputStream("src/main/java/com/example/proto3/contacts.bin"));
//        contactsBuilder = contacts.toBuilder();

//      第②种方法:
        try {
            contactsBuilder.mergeFrom(
                    new FileInputStream("src/main/java/com/example/proto3/contacts.bin"));
        } catch (FileNotFoundException e) {
            System.out.println("contacts.bin not find, create new file");
        }

        // 向通讯录中新增一个联系人
        contactsBuilder.addContacts(addPeopleInfo());

        // 序列化通讯录,将结果写入文件中
        FileOutputStream outputStream = new FileOutputStream(
                "src/main/java/com/example/proto3/contacts.bin");
        contactsBuilder.build().writeTo(outputStream);
        outputStream.close();
    }

    private static PeopleInfo addPeopleInfo() {
        PeopleInfo.Builder builder = PeopleInfo.newBuilder();
        Scanner scanner = new Scanner(System.in);
        System.out.println("--------------新增联系人-------------");
        System.out.print("请输入联系人姓名:");
        String name = scanner.nextLine();
        builder.setName(name);

        System.out.print("请输入联系人年龄:");
        int age = scanner.nextInt();
        scanner.nextLine();
        builder.setAge(age);

        for (int i = 0;; i++) {
            System.out.print("请输入联系人电话" + (i+1) + "(只输⼊回⻋完成电话新增): ");
            String number = scanner.nextLine();
            if (number.isEmpty()) {
                break;
            }
            PeopleInfo.Phone.Builder phoneBuilder = PeopleInfo.Phone.newBuilder();
            phoneBuilder.setNumber(number);

            System.out.print("请输入此电话类型(1、移动电话    2、固定电话)");
            int type = scanner.nextInt();
            scanner.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;
            }

            builder.addPhone(phoneBuilder);
        }

        Address.Builder addressBuilder = Address.newBuilder();
        System.out.print("请输入联系人家庭地址:");
        String homeAddress = scanner.nextLine();
        addressBuilder.setHomeAddress(homeAddress);
        System.out.print("请输入联系人单位地址:");
        String unitAddress = scanner.nextLine();
        addressBuilder.setUnitAddress(unitAddress);
        builder.setData(Any.pack(addressBuilder.build()));

        System.out.println("-----------添加联系人结束-------------");
        return builder.build();
    }
}

更新 TestRead.java (通讯录 2.2)

package com.example.proto3;

import com.google.protobuf.InvalidProtocolBufferException;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Map;

public class TestRead {
    public static void main(String[] args) throws IOException {

        // 读取文件,将读取的内容进行反序列化
        Contacts contacts = Contacts.parseFrom(
                new FileInputStream("src/main/java/com/example/proto3/contacts.bin"));

        // 打印
        printContacts(contacts);
//        System.out.println(contacts.toString());
    }

    private static void printContacts(Contacts contacts) throws InvalidProtocolBufferException {
        int i = 1;
        for (PeopleInfo peopleInfo : contacts.getContactsList()) {
            System.out.println("-----------联系人" + 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);
                System.out.println("电话" + j++ + ": " + phone.getNumber()
                        + "   (" + phone.getType().name() + ")");
            }
            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 来加强这种行为。使用 oneof 可以确保在给定时间内只有一个字段被设置,这样不仅可以提高数据结构的清晰度,还能节约内存空间。

在 Protocol Buffers 中,oneof 用于定义一组字段,其中只能有一个字段被设置为有效值。这意味着当设置了 oneof 中的某个字段时,其他字段会被自动清除。使用 oneof 可以有效地约束消息结构,确保只有一个字段处于活动状态,从而避免了多个字段同时被设置的情况,提高了数据的一致性和可靠性。

升级通讯录至 2.3 版本

Oneof 类型允许定义多个字段,但最终只能设置其中的一个字段,这样可以在不增加消息大小的情况下存储多个可能的字段值。

通讯录 2.3 版本想要新增联系人的其他联系方式,比如 QQ 或者微信号二选一,可以使用 oneof 字段来实现多选一的行为。在 Protocol Buffers 中,oneof 字段定义的格式为:

oneof 字段名 {
    字段1;
    字段2;
    ...
}

通过这种方式,可以确保在给定时间内只有一个字段被设置为有效值,从而实现多选一的效果。例如,可以定义一个 ContactMethod 的 oneof 字段来表示联系人的其他联系方式,其中包括 QQ 号和微信号两个字段,只能选择其中一个字段来填写,而另一个字段则会被自动清除。

更新 contacts.proto (通讯录 2.3),更新内容如下:

// 首行: 语法指定行
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";
// 定义联系人 message
message PeopleInfo {
  // 字段类型 字段名 = 字段唯一编号;
  string name = 1;
  int32 age = 2;
    message Phone {
      string number = 1;//这样字段编号也不重复
      // 0值必须存在,且作为第一个枚举常量的值
      // 枚举值范围:32位整数范围,不要设置负数
      enum PhoneType {
        MP = 0;  // 移动电话
        TEL = 1; // 固定电话
      }
      PhoneType type = 2;
    }
  repeated Phone phone = 3;
  google.protobuf.Any data = 4;
  // 加强多选一的行为,只会保留最后一次设置的成员
  oneof other_contact {
    // repeated string qq = 5;
    string qq = 5;
    string wechat = 6;
  }
}

message Contacts {
  repeated PeopleInfo contacts = 1;
}

message Address {
  string home_address = 1;
  string unit_address = 2;
}

当使用 oneof 字段时,需要注意以下几点:

  1. 字段编号冲突: 可选字段的编号必须与非可选字段的编号不冲突。在定义消息类型时,字段的编号应该唯一,不同类型的字段不能有相同的编号。

  2. 禁止使用重复字段: 在 oneof 中不能使用 repeated 字段,因为 oneof 本身就是用于指定多个可选字段中的一个,而不是用于存储多个值的。

  3. 值的设置与清除: 在设置 oneof 字段的值时,如果设置了多个 oneof 成员,只会保留最后一次设置的成员。之前设置的 oneof 成员会被自动清除,确保只有一个成员字段被设置。

使⽤ maven 插件进行一次编译。

展示更新的 PeopleInfo.java 部分代码:

  private int otherContactCase_ = 0;
  @SuppressWarnings("serial")
  private java.lang.Object otherContact_;
  public enum OtherContactCase
      implements com.google.protobuf.Internal.EnumLite,
          com.google.protobuf.AbstractMessage.InternalOneOfEnum {
    QQ(5),
    WECHAT(6),
    OTHERCONTACT_NOT_SET(0);
    private final int value;
    private OtherContactCase(int value) {
      this.value = value;
    }
    /**
     * @param value The number of the enum to look for.
     * @return The enum associated with the given number.
     * @deprecated Use {@link #forNumber(int)} instead.
     */
    @java.lang.Deprecated
    public static OtherContactCase valueOf(int value) {
      return forNumber(value);
    }

    public static OtherContactCase forNumber(int value) {
      switch (value) {
        case 5: return QQ;
        case 6: return WECHAT;
        case 0: return OTHERCONTACT_NOT_SET;
        default: return null;
      }
    }
    public int getNumber() {
      return this.value;
    }
  };

  public OtherContactCase
  getOtherContactCase() {
    return OtherContactCase.forNumber(
        otherContactCase_);
  }
  public static final int QQ_FIELD_NUMBER = 5;
  /**
   * <pre>
   * repeated string qq = 5;
   * </pre>
   *
   * <code>string qq = 5;</code>
   * @return Whether the qq field is set.
   */
  public boolean hasQq() {
    return otherContactCase_ == 5;
  }
  /**
   * <pre>
   * repeated string qq = 5;
   * </pre>
   *
   * <code>string qq = 5;</code>
   * @return The qq.
   */
  public java.lang.String getQq() {
    java.lang.Object ref = "";
    if (otherContactCase_ == 5) {
      ref = otherContact_;
    }
    if (ref instanceof java.lang.String) {
      return (java.lang.String) ref;
    } else {
      com.google.protobuf.ByteString bs = 
          (com.google.protobuf.ByteString) ref;
      java.lang.String s = bs.toStringUtf8();
      if (otherContactCase_ == 5) {
        otherContact_ = s;
      }
      return s;
    }
  }
  /**
   * <pre>
   * repeated string qq = 5;
   * </pre>
   *
   * <code>string qq = 5;</code>
   * @return The bytes for qq.
   */
  public com.google.protobuf.ByteString
      getQqBytes() {
    java.lang.Object ref = "";
    if (otherContactCase_ == 5) {
      ref = otherContact_;
    }
    if (ref instanceof java.lang.String) {
      com.google.protobuf.ByteString b = 
          com.google.protobuf.ByteString.copyFromUtf8(
              (java.lang.String) ref);
      if (otherContactCase_ == 5) {
        otherContact_ = b;
      }
      return b;
    } else {
      return (com.google.protobuf.ByteString) ref;
    }
  }

  public static final int WECHAT_FIELD_NUMBER = 6;
  /**
   * <code>string wechat = 6;</code>
   * @return Whether the wechat field is set.
   */
  public boolean hasWechat() {
    return otherContactCase_ == 6;
  }
  /**
   * <code>string wechat = 6;</code>
   * @return The wechat.
   */
  public java.lang.String getWechat() {
    java.lang.Object ref = "";
    if (otherContactCase_ == 6) {
      ref = otherContact_;
    }
    if (ref instanceof java.lang.String) {
      return (java.lang.String) ref;
    } else {
      com.google.protobuf.ByteString bs = 
          (com.google.protobuf.ByteString) ref;
      java.lang.String s = bs.toStringUtf8();
      if (otherContactCase_ == 6) {
        otherContact_ = s;
      }
      return s;
    }
  }
  /**
   * <code>string wechat = 6;</code>
   * @return The bytes for wechat.
   */
  public com.google.protobuf.ByteString
      getWechatBytes() {
    java.lang.Object ref = "";
    if (otherContactCase_ == 6) {
      ref = otherContact_;
    }
    if (ref instanceof java.lang.String) {
      com.google.protobuf.ByteString b = 
          com.google.protobuf.ByteString.copyFromUtf8(
              (java.lang.String) ref);
      if (otherContactCase_ == 6) {
        otherContact_ = b;
      }
      return b;
    } else {
      return (com.google.protobuf.ByteString) ref;
    }
  }
  • private int otherContactCase_ = 0;: 这个变量用于跟踪 Oneof 类型字段中当前设置的字段,其中 0 表示没有设置任何字段。
  • private java.lang.Object otherContact_;: 这是一个泛型对象,用于存储 Oneof 类型字段的值。由于 Oneof 允许存储不同类型的值,因此这里使用了 Object 类型来存储。
  • public enum OtherContactCase: 这个枚举类型表示 Oneof 类型字段中可能的字段选项,例如 QQ、Wechat 等。其中包括了每个选项的值以及一些辅助方法用于获取字段选项的值。
  • getOtherContactCase(): 这个方法用于获取当前 Oneof 类型字段设置的字段选项。

接下来是每个字段的相关方法:

  • hasQq(), hasWechat(): 这些方法用于检查特定字段是否被设置。
  • getQq(), getWechat(): 这些方法用于获取特定字段的值。
  • getQqBytes(), getWechatBytes(): 这些方法用于获取特定字段的字节表示形式。

总的来说,这些方法和变量的组合用于处理 Oneof 类型字段的设置和获取,确保在使用 Oneof 类型字段时能够准确地跟踪和操作当前设置的字段选项。

Build中:

    /**
     * <pre>
     * repeated string qq = 5;
     * </pre>
     *
     * <code>string qq = 5;</code>
     * @return Whether the qq field is set.
     */
    @java.lang.Override
    public boolean hasQq() {
      return otherContactCase_ == 5;
    }
    /**
     * <pre>
     * repeated string qq = 5;
     * </pre>
     *
     * <code>string qq = 5;</code>
     * @return The qq.
     */
    @java.lang.Override
    public java.lang.String getQq() {
      java.lang.Object ref = "";
      if (otherContactCase_ == 5) {
        ref = otherContact_;
      }
      if (!(ref instanceof java.lang.String)) {
        com.google.protobuf.ByteString bs =
            (com.google.protobuf.ByteString) ref;
        java.lang.String s = bs.toStringUtf8();
        if (otherContactCase_ == 5) {
          otherContact_ = s;
        }
        return s;
      } else {
        return (java.lang.String) ref;
      }
    }
    /**
     * <pre>
     * repeated string qq = 5;
     * </pre>
     *
     * <code>string qq = 5;</code>
     * @return The bytes for qq.
     */
    @java.lang.Override
    public com.google.protobuf.ByteString
        getQqBytes() {
      java.lang.Object ref = "";
      if (otherContactCase_ == 5) {
        ref = otherContact_;
      }
      if (ref instanceof String) {
        com.google.protobuf.ByteString b = 
            com.google.protobuf.ByteString.copyFromUtf8(
                (java.lang.String) ref);
        if (otherContactCase_ == 5) {
          otherContact_ = b;
        }
        return b;
      } else {
        return (com.google.protobuf.ByteString) ref;
      }
    }
    /**
     * <pre>
     * repeated string qq = 5;
     * </pre>
     *
     * <code>string qq = 5;</code>
     * @param value The qq to set.
     * @return This builder for chaining.
     */
    public Builder setQq(
        java.lang.String value) {
      if (value == null) { throw new NullPointerException(); }
      otherContactCase_ = 5;
      otherContact_ = value;
      onChanged();
      return this;
    }
    /**
     * <pre>
     * repeated string qq = 5;
     * </pre>
     *
     * <code>string qq = 5;</code>
     * @return This builder for chaining.
     */
    public Builder clearQq() {
      if (otherContactCase_ == 5) {
        otherContactCase_ = 0;
        otherContact_ = null;
        onChanged();
      }
      return this;
    }
    /**
     * <pre>
     * repeated string qq = 5;
     * </pre>
     *
     * <code>string qq = 5;</code>
     * @param value The bytes for qq to set.
     * @return This builder for chaining.
     */
    public Builder setQqBytes(
        com.google.protobuf.ByteString value) {
      if (value == null) { throw new NullPointerException(); }
      checkByteStringIsUtf8(value);
      otherContactCase_ = 5;
      otherContact_ = value;
      onChanged();
      return this;
    }

    /**
     * <code>string wechat = 6;</code>
     * @return Whether the wechat field is set.
     */
    @java.lang.Override
    public boolean hasWechat() {
      return otherContactCase_ == 6;
    }
    /**
     * <code>string wechat = 6;</code>
     * @return The wechat.
     */
    @java.lang.Override
    public java.lang.String getWechat() {
      java.lang.Object ref = "";
      if (otherContactCase_ == 6) {
        ref = otherContact_;
      }
      if (!(ref instanceof java.lang.String)) {
        com.google.protobuf.ByteString bs =
            (com.google.protobuf.ByteString) ref;
        java.lang.String s = bs.toStringUtf8();
        if (otherContactCase_ == 6) {
          otherContact_ = s;
        }
        return s;
      } else {
        return (java.lang.String) ref;
      }
    }
    /**
     * <code>string wechat = 6;</code>
     * @return The bytes for wechat.
     */
    @java.lang.Override
    public com.google.protobuf.ByteString
        getWechatBytes() {
      java.lang.Object ref = "";
      if (otherContactCase_ == 6) {
        ref = otherContact_;
      }
      if (ref instanceof String) {
        com.google.protobuf.ByteString b = 
            com.google.protobuf.ByteString.copyFromUtf8(
                (java.lang.String) ref);
        if (otherContactCase_ == 6) {
          otherContact_ = b;
        }
        return b;
      } else {
        return (com.google.protobuf.ByteString) ref;
      }
    }
    /**
     * <code>string wechat = 6;</code>
     * @param value The wechat to set.
     * @return This builder for chaining.
     */
    public Builder setWechat(
        java.lang.String value) {
      if (value == null) { throw new NullPointerException(); }
      otherContactCase_ = 6;
      otherContact_ = value;
      onChanged();
      return this;
    }
    /**
     * <code>string wechat = 6;</code>
     * @return This builder for chaining.
     */
    public Builder clearWechat() {
      if (otherContactCase_ == 6) {
        otherContactCase_ = 0;
        otherContact_ = null;
        onChanged();
      }
      return this;
    }
    /**
     * <code>string wechat = 6;</code>
     * @param value The bytes for wechat to set.
     * @return This builder for chaining.
     */
    public Builder setWechatBytes(
        com.google.protobuf.ByteString value) {
      if (value == null) { throw new NullPointerException(); }
      checkByteStringIsUtf8(value);
      otherContactCase_ = 6;
      otherContact_ = value;
      onChanged();
      return this;
    }

上述的代码中,对于 oneof 字段:

  • 枚举类型定义: 编译器会将 oneof 中的多个字段定义为一个枚举类型,用于标识当前设置了哪个字段。
  • 设置和获取: 对 oneof 内的字段进行常规的设置和获取即可,但只能设置一个字段的值。如果设置多个字段,则只会保留最后一次设置的成员。
  • clear() 方法: 可以使用 clear() 方法来清空 oneof 字段,将其中的所有成员字段重置为空。
  • getXXXCase() 方法: 可以使用 getXXXCase() 方法来获取当前设置了哪个字段。这个方法返回一个枚举值,指示当前 oneof 中哪个字段被设置了值。
  • hasXXX() 方法: 可以使用 hasXXX() 方法来检测当前字段是否被设置了值。如果某个字段被设置了值,则返回 true,否则返回 false。

更新 TestWrite.java (通讯录 2.3)

package com.example.proto3;

import com.google.protobuf.Any;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Scanner;

public class TestWrite {
    public static void main(String[] args) throws IOException {
        Contacts.Builder contactsBuilder = Contacts.newBuilder();
//      第①种方法:
//      读取本地已存在的 contacts.bin,反序列化出通讯录对象
//        Contacts contacts = Contacts.parseFrom(
//                new FileInputStream("src/main/java/com/example/proto3/contacts.bin"));
//        contactsBuilder = contacts.toBuilder();

//      第②种方法:
        try {
            contactsBuilder.mergeFrom(
                    new FileInputStream("src/main/java/com/example/proto3/contacts.bin"));
        } catch (FileNotFoundException e) {
            System.out.println("contacts.bin not find, create new file");
        }

        // 向通讯录中新增一个联系人
        contactsBuilder.addContacts(addPeopleInfo());

        // 序列化通讯录,将结果写入文件中
        FileOutputStream outputStream = new FileOutputStream(
                "src/main/java/com/example/proto3/contacts.bin");
        contactsBuilder.build().writeTo(outputStream);
        outputStream.close();
    }

    private static PeopleInfo addPeopleInfo() {
        PeopleInfo.Builder builder = PeopleInfo.newBuilder();
        Scanner scanner = new Scanner(System.in);
        System.out.println("--------------新增联系人-------------");
        System.out.print("请输入联系人姓名:");
        String name = scanner.nextLine();
        builder.setName(name);

        System.out.print("请输入联系人年龄:");
        int age = scanner.nextInt();
        scanner.nextLine();
        builder.setAge(age);

        for (int i = 0;; i++) {
            System.out.print("请输入联系人电话" + (i+1) + "(只输⼊回⻋完成电话新增): ");
            String number = scanner.nextLine();
            if (number.isEmpty()) {
                break;
            }
            PeopleInfo.Phone.Builder phoneBuilder = PeopleInfo.Phone.newBuilder();
            phoneBuilder.setNumber(number);

            System.out.print("请输入此电话类型(1、移动电话    2、固定电话)");
            int type = scanner.nextInt();
            scanner.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;
            }

            builder.addPhone(phoneBuilder);
        }

        Address.Builder addressBuilder = Address.newBuilder();
        System.out.print("请输入联系人家庭地址:");
        String homeAddress = scanner.nextLine();
        addressBuilder.setHomeAddress(homeAddress);
        System.out.print("请输入联系人单位地址:");
        String unitAddress = scanner.nextLine();
        addressBuilder.setUnitAddress(unitAddress);
        builder.setData(Any.pack(addressBuilder.build()));

        System.out.print("请选择要添加的其他联系方式(1、qq号   2、微信号):");
        int otherContact = scanner.nextInt();
        scanner.nextLine();
        if (1 == otherContact) {
            System.out.print("请输入qq号:");
            String qq = scanner.nextLine();
            builder.setQq(qq);
        } else if (2 == otherContact) {
            System.out.print("请输入微信号:");
            String wechat = scanner.nextLine();
            builder.setWechat(wechat);
        } else {
            System.out.println("无效选择,设置失败!");
        }

        System.out.println("-----------添加联系人结束-------------");
        return builder.build();
    }
}

更新 TestRead.java (通讯录 2.3)

package com.example.proto3;

import com.google.protobuf.InvalidProtocolBufferException;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Map;

public class TestRead {
    public static void main(String[] args) throws IOException {

        // 读取文件,将读取的内容进行反序列化
        Contacts contacts = Contacts.parseFrom(
                new FileInputStream("src/main/java/com/example/proto3/contacts.bin"));

        // 打印
        printContacts(contacts);
//        System.out.println(contacts.toString());
    }

    private static void printContacts(Contacts contacts) throws InvalidProtocolBufferException {
        int i = 1;
        for (PeopleInfo peopleInfo : contacts.getContactsList()) {
            System.out.println("-----------联系人" + 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);
                System.out.println("电话" + j++ + ": " + phone.getNumber()
                        + "   (" + phone.getType().name() + ")");
            }
            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());
                }
            }

//            if (peopleInfo.hasQq()) {
//
//            } else if (peopleInfo.hasWechat()) {
//
//            }

            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 类型

在 Protocol Buffers 中,我们可以使用 map 类型来声明关联映射字段,其格式为:

map<key_type, value_type> map_field = N;

当在 Protocol Buffers 中使用 map 类型声明字段时,需要注意以下几点:

  • 键类型(key_type)和值类型(value_type):键类型必须是除了 float 和 bytes 类型以外的任意标量类型,例如 int32、string 等;值类型则可以是任意类型,包括标量类型(如 int32、string)、消息类型等。
  • map 字段的修饰:map 字段不能使用 repeated 修饰,因为 map 已经隐含了重复的概念。每个键在 map 中是唯一的,因此不需要使用 repeated。
  • 元素无序性:map 中存储的元素是无序的,这意味着不保证按照插入顺序进行排列。Protocol Buffers 实现中会根据键的哈希值来存储和检索元素,因此不会保留插入的顺序。
  • 字段编号(N):map 字段声明时需要指定一个字段编号(N),用于在 Protocol Buffers 文件中唯一标识该字段。字段编号需要是一个正整数,用于标识消息中的字段位置。
  • 使用 map 类型可以方便地表示键值对形式的数据,例如表示字典、映射等数据结构,在 Protocol Buffers 中提供了高效的序列化和反序列化机制来处理 map 类型的字段。

升级通讯录至 2.4 版本

在通讯录版本2.4中,我们希望新增联系人的备注信息。为了实现这一功能,我们可以使用map类型的字段来存储备注信息。

更新 contacts.proto (通讯录 2.4),更新内容如下:

// 首行: 语法指定行
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";
// 定义联系人 message
message PeopleInfo {
  // 字段类型 字段名 = 字段唯一编号;
  string name = 1;
  int32 age = 2;
    message Phone {
      string number = 1;//这样字段编号也不重复
      // 0值必须存在,且作为第一个枚举常量的值
      // 枚举值范围:32位整数范围,不要设置负数
      enum PhoneType {
        MP = 0;  // 移动电话
        TEL = 1; // 固定电话
      }
      PhoneType type = 2;
    }
  repeated Phone phone = 3;
  google.protobuf.Any data = 4;
  // 加强多选一的行为,只会保留最后一次设置的成员
  oneof other_contact {
    // repeated string qq = 5;
    string qq = 5;
    string wechat = 6;
  }
  // key:除了 float bytes 之外的任意标量类型
  // repeated map<string, string> remark = 7; // 备注
  // 无序
  map<string, string> remark = 7; // 备注
}
message Contacts {
  repeated PeopleInfo contacts = 1;
}

message Address {
  string home_address = 1;
  string unit_address = 2;
}

使用 maven 插件进行一次编译。

展示更新的 PeopleInfo.java 部分代码:

  public static final int REMARK_FIELD_NUMBER = 7;
  private static final class RemarkDefaultEntryHolder {
    static final com.google.protobuf.MapEntry<
        java.lang.String, java.lang.String> defaultEntry =
            com.google.protobuf.MapEntry
            .<java.lang.String, java.lang.String>newDefaultInstance(
                com.example.proto3.ContactsProtos.internal_static_proto3_PeopleInfo_RemarkEntry_descriptor, 
                com.google.protobuf.WireFormat.FieldType.STRING,
                "",
                com.google.protobuf.WireFormat.FieldType.STRING,
                "");
  }
  @SuppressWarnings("serial")
  private com.google.protobuf.MapField<
      java.lang.String, java.lang.String> remark_;
  private com.google.protobuf.MapField<java.lang.String, java.lang.String>
  internalGetRemark() {
    if (remark_ == null) {
      return com.google.protobuf.MapField.emptyMapField(
          RemarkDefaultEntryHolder.defaultEntry);
    }
    return remark_;
  }
  public int getRemarkCount() {
    return internalGetRemark().getMap().size();
  }
  /**
   * <pre>
   * key:除了 float bytes 之外的任意标量类型
   * repeated map&lt;string, string&gt; remark = 7; // 备注
   * 无序
   * </pre>
   *
   * <code>map&lt;string, string&gt; remark = 7;</code>
   */
  @java.lang.Override
  public boolean containsRemark(
      java.lang.String key) {
    if (key == null) { throw new NullPointerException("map key"); }
    return internalGetRemark().getMap().containsKey(key);
  }
  /**
   * Use {@link #getRemarkMap()} instead.
   */
  @java.lang.Override
  @java.lang.Deprecated
  public java.util.Map<java.lang.String, java.lang.String> getRemark() {
    return getRemarkMap();
  }
  /**
   * <pre>
   * key:除了 float bytes 之外的任意标量类型
   * repeated map&lt;string, string&gt; remark = 7; // 备注
   * 无序
   * </pre>
   *
   * <code>map&lt;string, string&gt; remark = 7;</code>
   */
  @java.lang.Override
  public java.util.Map<java.lang.String, java.lang.String> getRemarkMap() {
    return internalGetRemark().getMap();
  }
  /**
   * <pre>
   * key:除了 float bytes 之外的任意标量类型
   * repeated map&lt;string, string&gt; remark = 7; // 备注
   * 无序
   * </pre>
   *
   * <code>map&lt;string, string&gt; remark = 7;</code>
   */
  @java.lang.Override
  public /* nullable */
java.lang.String getRemarkOrDefault(
      java.lang.String key,
      /* nullable */
java.lang.String defaultValue) {
    if (key == null) { throw new NullPointerException("map key"); }
    java.util.Map<java.lang.String, java.lang.String> map =
        internalGetRemark().getMap();
    return map.containsKey(key) ? map.get(key) : defaultValue;
  }
  /**
   * <pre>
   * key:除了 float bytes 之外的任意标量类型
   * repeated map&lt;string, string&gt; remark = 7; // 备注
   * 无序
   * </pre>
   *
   * <code>map&lt;string, string&gt; remark = 7;</code>
   */
  @java.lang.Override
  public java.lang.String getRemarkOrThrow(
      java.lang.String key) {
    if (key == null) { throw new NullPointerException("map key"); }
    java.util.Map<java.lang.String, java.lang.String> map =
        internalGetRemark().getMap();
    if (!map.containsKey(key)) {
      throw new java.lang.IllegalArgumentException();
    }
    return map.get(key);
  }
  • internalGetRemark() 和 internalGetMutableRemark() 方法:这两个方法用于获取备注字段的值。internalGetRemark() 方法用于获取不可变的备注字段值,如果当前备注字段为空,则返回一个空的 MapField 对象;internalGetMutableRemark() 方法用于获取可变的备注字段值,如果当前备注字段为空,则创建一个新的 MapField 对象。这两个方法是内部方法,用于获取备注字段的值,不对外暴露。
  • getRemarkCount() 方法:getRemarkCount() 方法用于获取备注字段中键值对的数量。
  • containsRemark() 方法:containsRemark(String key) 方法用于检查备注字段中是否包含指定键的值。
  • getRemarkMap() 方法:getRemarkMap() 方法用于获取备注字段的键值对映射。
  • getRemarkOrDefault() 和 getRemarkOrThrow() 方法:getRemarkOrDefault(String key, String defaultValue) 方法用于获取指定键对应的值,如果该键不存在,则返回默认值;getRemarkOrThrow(String key) 方法用于获取指定键对应的值,如果该键不存在,则抛出异常。
  • clearRemark() 和 removeRemark() 方法:clearRemark() 方法用于清除备注字段的所有键值对;removeRemark(String key) 方法用于删除指定键的值。
  • putRemark() 和 putAllRemark() 方法:putRemark(String key, String value) 方法用于添加或更新指定键对应的值;putAllRemark(Map<String, String> values) 方法用于添加或更新多个键值对。

这些方法用于操作 map 类型字段,提供了对键值对映射的获取、设置、删除等功能,使得开发者可以方便地对 map 类型字段进行操作和管理。

Build中:

  private com.google.protobuf.MapField<
        java.lang.String, java.lang.String> remark_;
    private com.google.protobuf.MapField<java.lang.String, java.lang.String>
        internalGetRemark() {
      if (remark_ == null) {
        return com.google.protobuf.MapField.emptyMapField(
            RemarkDefaultEntryHolder.defaultEntry);
      }
      return remark_;
    }
    private com.google.protobuf.MapField<java.lang.String, java.lang.String>
        internalGetMutableRemark() {
      if (remark_ == null) {
        remark_ = com.google.protobuf.MapField.newMapField(
            RemarkDefaultEntryHolder.defaultEntry);
      }
      if (!remark_.isMutable()) {
        remark_ = remark_.copy();
      }
      bitField0_ |= 0x00000040;
      onChanged();
      return remark_;
    }
    public int getRemarkCount() {
      return internalGetRemark().getMap().size();
    }
    /**
     * <pre>
     * key:除了 float bytes 之外的任意标量类型
     * repeated map&lt;string, string&gt; remark = 7; // 备注
     * 无序
     * </pre>
     *
     * <code>map&lt;string, string&gt; remark = 7;</code>
     */
    @java.lang.Override
    public boolean containsRemark(
        java.lang.String key) {
      if (key == null) { throw new NullPointerException("map key"); }
      return internalGetRemark().getMap().containsKey(key);
    }
    /**
     * Use {@link #getRemarkMap()} instead.
     */
    @java.lang.Override
    @java.lang.Deprecated
    public java.util.Map<java.lang.String, java.lang.String> getRemark() {
      return getRemarkMap();
    }
    /**
     * <pre>
     * key:除了 float bytes 之外的任意标量类型
     * repeated map&lt;string, string&gt; remark = 7; // 备注
     * 无序
     * </pre>
     *
     * <code>map&lt;string, string&gt; remark = 7;</code>
     */
    @java.lang.Override
    public java.util.Map<java.lang.String, java.lang.String> getRemarkMap() {
      return internalGetRemark().getMap();
    }
    /**
     * <pre>
     * key:除了 float bytes 之外的任意标量类型
     * repeated map&lt;string, string&gt; remark = 7; // 备注
     * 无序
     * </pre>
     *
     * <code>map&lt;string, string&gt; remark = 7;</code>
     */
    @java.lang.Override
    public /* nullable */
java.lang.String getRemarkOrDefault(
        java.lang.String key,
        /* nullable */
java.lang.String defaultValue) {
      if (key == null) { throw new NullPointerException("map key"); }
      java.util.Map<java.lang.String, java.lang.String> map =
          internalGetRemark().getMap();
      return map.containsKey(key) ? map.get(key) : defaultValue;
    }
    /**
     * <pre>
     * key:除了 float bytes 之外的任意标量类型
     * repeated map&lt;string, string&gt; remark = 7; // 备注
     * 无序
     * </pre>
     *
     * <code>map&lt;string, string&gt; remark = 7;</code>
     */
    @java.lang.Override
    public java.lang.String getRemarkOrThrow(
        java.lang.String key) {
      if (key == null) { throw new NullPointerException("map key"); }
      java.util.Map<java.lang.String, java.lang.String> map =
          internalGetRemark().getMap();
      if (!map.containsKey(key)) {
        throw new java.lang.IllegalArgumentException();
      }
      return map.get(key);
    }
    public Builder clearRemark() {
      bitField0_ = (bitField0_ & ~0x00000040);
      internalGetMutableRemark().getMutableMap()
          .clear();
      return this;
    }
    /**
     * <pre>
     * key:除了 float bytes 之外的任意标量类型
     * repeated map&lt;string, string&gt; remark = 7; // 备注
     * 无序
     * </pre>
     *
     * <code>map&lt;string, string&gt; remark = 7;</code>
     */
    public Builder removeRemark(
        java.lang.String key) {
      if (key == null) { throw new NullPointerException("map key"); }
      internalGetMutableRemark().getMutableMap()
          .remove(key);
      return this;
    }
    /**
     * Use alternate mutation accessors instead.
     */
    @java.lang.Deprecated
    public java.util.Map<java.lang.String, java.lang.String>
        getMutableRemark() {
      bitField0_ |= 0x00000040;
      return internalGetMutableRemark().getMutableMap();
    }
    /**
     * <pre>
     * key:除了 float bytes 之外的任意标量类型
     * repeated map&lt;string, string&gt; remark = 7; // 备注
     * 无序
     * </pre>
     *
     * <code>map&lt;string, string&gt; remark = 7;</code>
     */
    public Builder putRemark(
        java.lang.String key,
        java.lang.String value) {
      if (key == null) { throw new NullPointerException("map key"); }
      if (value == null) { throw new NullPointerException("map value"); }
      internalGetMutableRemark().getMutableMap()
          .put(key, value);
      bitField0_ |= 0x00000040;
      return this;
    }
    /**
     * <pre>
     * key:除了 float bytes 之外的任意标量类型
     * repeated map&lt;string, string&gt; remark = 7; // 备注
     * 无序
     * </pre>
     *
     * <code>map&lt;string, string&gt; remark = 7;</code>
     */
    public Builder putAllRemark(
        java.util.Map<java.lang.String, java.lang.String> values) {
      internalGetMutableRemark().getMutableMap()
          .putAll(values);
      bitField0_ |= 0x00000040;
      return this;
    }
  • remark_ 字段:remark_ 是一个私有字段,用于存储备注信息,类型为 com.google.protobuf.MapField<java.lang.String, java.lang.String>。
  • internalGetRemark() 方法:internalGetRemark() 方法用于获取备注字段的值。如果 remark_ 为空,则返回一个空的 MapField 对象,其中包含默认的 RemarkDefaultEntry;否则,返回当前的 remark_ 对象。
  • internalGetMutableRemark() 方法:internalGetMutableRemark() 方法用于获取可变的备注字段值。如果 remark_ 为空,则创建一个新的 MapField 对象,并将其初始化为默认值;如果 remark_ 不为空且不可变,则复制一个可变的副本。然后将 bitField0_ 标志位的第 7 位设置为 1,表示备注字段已被修改,最后调用 onChanged() 方法。
  • getRemarkCount() 方法:getRemarkCount() 方法用于获取备注字段中键值对的数量,即备注信息的条目数。
  • containsRemark() 方法:containsRemark(String key) 方法用于检查备注字段中是否包含指定键的值。如果指定键存在,则返回 true;否则返回 false。
  • getRemark()、getRemarkMap() 和 getRemarkOrDefault() 方法:getRemark() 方法是对 getRemarkMap() 方法的过时调用,不推荐使用;getRemarkMap() 方法用于获取备注字段的键值对映射;getRemarkOrDefault(String key, String defaultValue) 方法用于获取指定键对应的值,如果该键不存在,则返回默认值。
  • getRemarkOrThrow() 方法:getRemarkOrThrow(String key) 方法用于获取指定键对应的值,如果该键不存在,则抛出 IllegalArgumentException 异常。
  • clearRemark() 和 removeRemark() 方法:clearRemark() 方法用于清除备注字段的所有键值对;
  • removeRemark(String key) 方法用于删除指定键的值。
  • getMutableRemark() 方法:getMutableRemark() 方法用于获取可变的备注字段映射,并将 bitField0_ 标志位的第 7 位设置为 1,表示备注字段已被修改。
  • putRemark() 和 putAllRemark() 方法:putRemark(String key, String value) 方法用于添加或更新指定键对应的值;putAllRemark(Map<String, String> values) 方法用于添加或更新多个键值对。

这些方法组合起来,提供了对 map 类型字段的常用操作,包括获取、设置、删除等功能,使得开发者可以方便地对 map 类型字段进行操作和管理。

更新 TestWrite.java (通讯录 2.4)

package com.example.proto3;

import com.google.protobuf.Any;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Scanner;

public class TestWrite {
    public static void main(String[] args) throws IOException {
        Contacts.Builder contactsBuilder = Contacts.newBuilder();
//      第①种方法:
//      读取本地已存在的 contacts.bin,反序列化出通讯录对象
//        Contacts contacts = Contacts.parseFrom(
//                new FileInputStream("src/main/java/com/example/proto3/contacts.bin"));
//        contactsBuilder = contacts.toBuilder();

//      第②种方法:
        try {
            contactsBuilder.mergeFrom(
                    new FileInputStream("src/main/java/com/example/proto3/contacts.bin"));
        } catch (FileNotFoundException e) {
            System.out.println("contacts.bin not find, create new file");
        }

        // 向通讯录中新增一个联系人
        contactsBuilder.addContacts(addPeopleInfo());

        // 序列化通讯录,将结果写入文件中
        FileOutputStream outputStream = new FileOutputStream(
                "src/main/java/com/example/proto3/contacts.bin");
        contactsBuilder.build().writeTo(outputStream);
        outputStream.close();
    }

    private static PeopleInfo addPeopleInfo() {
        PeopleInfo.Builder builder = PeopleInfo.newBuilder();
        Scanner scanner = new Scanner(System.in);
        System.out.println("--------------新增联系人-------------");
        System.out.print("请输入联系人姓名:");
        String name = scanner.nextLine();
        builder.setName(name);

        System.out.print("请输入联系人年龄:");
        int age = scanner.nextInt();
        scanner.nextLine();
        builder.setAge(age);

        for (int i = 0;; i++) {
            System.out.print("请输入联系人电话" + (i+1) + "(只输⼊回⻋完成电话新增): ");
            String number = scanner.nextLine();
            if (number.isEmpty()) {
                break;
            }
            PeopleInfo.Phone.Builder phoneBuilder = PeopleInfo.Phone.newBuilder();
            phoneBuilder.setNumber(number);

            System.out.print("请输入此电话类型(1、移动电话    2、固定电话)");
            int type = scanner.nextInt();
            scanner.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;
            }

            builder.addPhone(phoneBuilder);
        }

        Address.Builder addressBuilder = Address.newBuilder();
        System.out.print("请输入联系人家庭地址:");
        String homeAddress = scanner.nextLine();
        addressBuilder.setHomeAddress(homeAddress);
        System.out.print("请输入联系人单位地址:");
        String unitAddress = scanner.nextLine();
        addressBuilder.setUnitAddress(unitAddress);
        builder.setData(Any.pack(addressBuilder.build()));

        System.out.print("请选择要添加的其他联系方式(1、qq号   2、微信号):");
        int otherContact = scanner.nextInt();
        scanner.nextLine();
        if (1 == otherContact) {
            System.out.print("请输入qq号:");
            String qq = scanner.nextLine();
            builder.setQq(qq);
        } else if (2 == otherContact) {
            System.out.print("请输入微信号:");
            String wechat = scanner.nextLine();
            builder.setWechat(wechat);
        } else {
            System.out.println("无效选择,设置失败!");
        }

        for (int i = 0; ; i++) {
            System.out.print("请输入备注" + (i+1) + "标题(只输⼊回⻋完成备注新增):");
            String key = scanner.nextLine();
            if (key.isEmpty()) {
                break;
            }

            System.out.print("请输入备注内容:");
            String value = scanner.nextLine();
            builder.putRemark(key, value);
        }

        System.out.println("-----------添加联系人结束-------------");
        return builder.build();
    }
}

更新 TestRead.java (通讯录 2.4)

package com.example.proto3;

import com.google.protobuf.InvalidProtocolBufferException;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Map;

public class TestRead {
    public static void main(String[] args) throws IOException {

        // 读取文件,将读取的内容进行反序列化
        Contacts contacts = Contacts.parseFrom(
                new FileInputStream("src/main/java/com/example/proto3/contacts.bin"));

        // 打印
        printContacts(contacts);
//        System.out.println(contacts.toString());
    }

    private static void printContacts(Contacts contacts) throws InvalidProtocolBufferException {
        int i = 1;
        for (PeopleInfo peopleInfo : contacts.getContactsList()) {
            System.out.println("-----------联系人" + 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);
                System.out.println("电话" + j++ + ": " + phone.getNumber()
                        + "   (" + phone.getType().name() + ")");
            }
            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());
                }
            }

//            if (peopleInfo.hasQq()) {
//
//            } else if (peopleInfo.hasWechat()) {
//
//            }

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

            for (Map.Entry<String, String> entry : peopleInfo.getRemarkMap().entrySet()) {
                System.out.println("   " + entry.getKey() + ": " + entry.getValue());
            }
        }
    }
}

当涉及到使用Map类型的字段时,我们可以使用以下方法来操作这些字段:

  • clear(): 此方法将清空整个map,从而删除其中所有的键值对。调用此方法后,map将不包含任何元素。
  • putXXX(): 使用此方法可以向map中添加一个键值对。你可以传递键和相应的值作为参数。如果键已经存在于map中,则新的值将替换旧值。此外,我们还可以传递整个map作为参数,以一次性将另一个map的所有键值对添加到当前map中。
  • getXXX(): 使用此方法可以获取map中指定键的值。你可以传递键作为参数,并且此方法将返回与该键关联的值。如果该键不存在于map中,则返回null。另外,我们还可以调用此方法来获取整个map,从而获得map中的所有键值对。

在完成通讯录 2.x 版本的任务过程中,我们不仅成功将通讯录升级到了 2.4 版本,还进一步熟悉了 Protocol Buffers 的使用。通过这个过程,我们掌握了 ProtoBuf 的 proto3 语法所支持的大部分类型以及它们的使用方法。然而,仅仅掌握了基础的使用方法还远远不够。通过接下来的学习,我们将有机会深入了解 Protocol Buffers 的更多内容,进一步探索其强大的功能和灵活性,为我们的开发工作提供更多可能性。

默认值

当进行消息反序列化时,如果反序列化的二进制序列中不包含某个字段,反序列化后的对象会根据该字段的类型设置默认值。这些默认值规则如下:

  • 字符串(String): 默认值为空字符串,即 ""。
  • 字节(Bytes): 默认值为空字节,即一个长度为零的字节数组。
  • 布尔值(Boolean): 默认值为 false。
  • 数值类型(Numeric Types): 默认值为 0,对于浮点数类型也是 0.0。
  • 枚举(Enum): 默认值是该枚举类型定义的第一个枚举值,通常是 0。在 proto 文件中,第一个定义的枚举值应该为 0。
  • 消息字段(Message Fields): 如果未设置该字段,其取值取决于所使用的编程语言和框架的约定。通常情况下,该字段会被设置为默认的消息类型实例。在这种情况下,可以使用 has 方法来检测当前字段是否被设置。
  • 重复字段(Repeated Fields): 如果字段被标记为 repeated,即可重复的,它的默认值是一个空列表,不包含任何元素。
  • oneof 字段(Oneof Fields): 对于 oneof 字段,如果未设置任何成员字段,则 oneof 字段也被视为未设置。在这种情况下,可以使用 has 方法来检测当前字段是否被设置。
  • any 字段(Any Fields): any 字段中存储的消息类型可以是任意的,如果未设置任何消息类型,则该 any 字段也被视为未设置。可以使用 has 方法来检测当前字段是否被设置。

这些默认值规则在 ProtoBuf 中是固定的,但在具体的编程语言和框架中可能会有所不同。

更新消息

更新规则

如果现有的消息类型已经不再满足我们的需求,例如需要扩展一个字段,在不破坏任何现有代码的情 况下更新消息类型非常简单。

遵循如下规则即可:

  1. 禁止修改任何已有字段的字段编号:不应该更改现有字段的字段编号,因为这可能会导致现有的数据解析错误。

  2. 移除旧字段时的处理:如果需要移除旧字段,应该将其保留为已预留字段(reserved),而不是直接删除或注释掉字段。这样做可以确保该字段编号不会被重复使用,从而保持消息的兼容性。

  3. 整型和布尔类型的兼容性:int32、uint32、int64、uint64 和 bool 类型是完全兼容的,可以相互转换而不会破坏前后兼容性。例如,可以从其中一个类型改为另一个类型而不影响现有代码。需要注意的是,如果解析出来的数值与相应的类型不匹配,可能会被截断。

  4. 有符号整型的兼容性:sint32 和 sint64 类型相互兼容,但它们与其他整型类型不兼容。

  5. 字符串和字节串的兼容性:在合法的 UTF-8 字节前提下,string 和 bytes 类型也是兼容的。这意味着可以相互转换而不会破坏前后兼容性。

  6. 消息编码版本的兼容性:如果 bytes 类型包含消息编码版本,那么嵌套消息和 bytes 类型也是兼容的。

  7. 固定长度整型的兼容性:fixed32 与 sfixed32 类型相互兼容,fixed64 与 sfixed64 类型相互兼容。

  8. 枚举类型的兼容性:枚举类型与 int32、uint32、int64 和 uint64 类型兼容。但需要注意,如果枚举值超出了目标类型的范围,可能会导致截断。在反序列化消息时,未识别的枚举类型可能会以特定方式处理,具体取决于编程语言。

  9. oneof 类型的处理:

    • 将单个值更改为新的 oneof 类型成员是安全和二进制兼容的。
    • 如果确定代码不会一次性设置多个值,那么将多个字段移到新的 oneof 类型也是可行的。
    • 但是,将字段移入已存在的 oneof 类型是不安全的,应该避免这样做,因为这可能会导致现有数据解析错误。

综上所述,根据这些规则,可以在不破坏现有代码的情况下更新消息类型,以满足新的需求或进行扩展。

那么删除老字段有什么规则吗?

我们不得不强调一点,在protobuf里面我们不建议直接删除老字段!!!

这会带来无法估量的后果。

直接删除老字段可能会导致以下问题:

  1. 数据损坏: 如果删除了某个字段,而在存储的数据中仍然存在该字段的值,那么当尝试解析这些数据时就会出现数据损坏的情况。这可能会导致应用程序崩溃或数据丢失。

  2. 兼容性问题: 删除老字段会破坏现有代码的向后兼容性,因为旧版本的代码无法识别或处理新版本中删除的字段。这可能导致升级后的应用程序与旧版本的消息格式不兼容,从而造成系统不稳定或功能错误。

  3. 隐私问题: 删除字段可能会暴露之前存储在该字段中的敏感信息。即使该字段在新版本中已经删除,但如果旧版本的数据仍然存在,那么这些敏感信息可能会被恶意方访问到,从而引发隐私泄露问题。

而且,在 Protocol Buffers 中,反序列化过程是根据字段的编号(而不是字段的名称)来设置值的。因此,如果在更新消息类型时直接删除字段,旧版本的数据中仍然可能存在对应编号的值。这可能导致以下问题:

  1. 数据错乱: 如果删除了某个字段,但是在存储的数据中仍然存在该字段的编号及其对应的值,那么当尝试反序列化这些数据时,由于字段已被删除,反序列化器可能会将该编号的值设置到错误的字段上,导致数据错乱。

  2. 解析错误: 反序列化过程中,如果尝试设置的字段不存在或已被删除,可能会导致解析错误或异常,从而影响程序的正常运行。

所以得出结论:若是移除老字段,我们要保证不再使用移除字段的字段编号,不建议直接删除或注 释掉字段。 那么正确的做法是保留字段编号(reserved),以确保该编号将不能被重复使用。 

因此,为了避免这些潜在的问题,我们强烈建议遵循上述提到的做法,即通过使用 reserved 关键字将字段标记为保留项,而不是直接删除或注释掉字段。这样可以确保在将来的更新中不会破坏现有数据或代码,并且保持向后兼容性和数据完整性。

保留字段 reserved

当更新消息类型时,需要谨慎处理以确保向后兼容性和数据完整性。一种常见的情况是向现有消息类型添加新字段,但必须小心处理以避免破坏现有代码或导致数据损坏。下面是一些详细的步骤和注意事项:

  1. 禁止修改字段编号: 在更新消息类型时,绝对禁止修改任何已有字段的字段编号。字段编号在序列化和反序列化过程中起着关键作用,一旦修改会导致无法正确解析现有的消息数据。

  2. 移除字段的处理: 如果需要移除旧字段,不能直接删除或注释掉字段,而是应该将字段的编号保留起来。可以使用 reserved 关键字将这些字段标记为保留项,以确保它们不会被重新使用。

  3. 兼容性注意事项:

    • 整数类型(如 int32、uint32、int64、uint64 和 bool)之间是完全兼容的,可以相互转换而不会破坏消息的兼容性。
    • 字符串类型(string 和 bytes)在合法的 UTF-8 字节前提下也是兼容的,但需要注意保证数据的编码正确性。
    • 枚举类型与整数类型兼容,但需要注意值的匹配情况,以避免数据截断或意外解析错误。
    • 使用 oneof 类型时,需要确保在向已有 oneof 类型中添加字段时不会影响到已有代码的正确性。
  4. 保留字段的警告: 使用 reserved 将指定字段的编号或名称设置为保留项 ,ProtoBuf可以在编译阶段就阻止对已保留字段编号的使用。当我们再使用这些编号或名称时,protocol buffer 的编译器将会警告这些编号或名称不可用。所以,通过使用 reserved 关键字,将字段编号或名称设置为保留项,我们就可以确保在将来使用这些编号或名称时,Protocol Buffer 编译器会发出警告,提示这些编号或名称不可用。这有助于防止用户在添加新字段时意外地使用已删除或注释掉的字段编号,从而保护数据的完整性和隐私安全。

总之,对于更新消息类型,保持向后兼容性是至关重要的。通过遵循上述步骤和注意事项,可以确保在进行更新时不会破坏现有代码,同时保护数据的完整性和安全性。

// 定义联系人 message
message PeopleInfo {
  reserved 2,10,11,15,100 to 200;
  reserved "age","field1";
  //注意,不可以在一行既保留字段编号,又保留字段名称
  //reserved 2,"age";
  // 字段类型 字段名 = 字段唯一编号;
  string name = 1;
  //int32 age = 2;
  int32 birthday = 3;
}
  1. 保留单个字段编号: 使用 reserved 关键字后面跟着要保留的字段的唯一编号。例如,reserved 2; 将字段编号为2的字段标记为保留项,防止其被重新使用。
  2. 保留多个字段编号: 可以在 reserved 关键字后面列出多个字段编号,以将这些字段一起标记为保留项。例如,reserved 2, 3, 4; 将字段编号为2、3和4的字段都标记为保留项。
  3. 保留字段编号的范围: 使用 to 关键字可以指定字段编号的范围,将该范围内的所有字段都标记为保留项。例如,reserved 5 to 10; 将字段编号从5到10的所有字段都标记为保留项。
  4. 保留字段名称: 可以使用字段名称来标记字段为保留项。在 reserved 关键字后面跟着要保留的字段的名称即可。例如,reserved "age"; 将名为 "age" 的字段标记为保留项。
  5. 不允许混合使用字段编号和字段名称: 在一行中不允许同时指定字段编号和字段名称,应该选择其中一种方式来标记保留项。例如,reserved 2, "age"; 是不被允许的,应该分开指定或选择其中一种方式。

未知字段

在通讯录 3.0 版本中,我们向服务目录下的 contacts.proto 文件中新增了一个名为“生日”的字段。这个变动并没有对客户端相关的代码做出任何修改。经过验证,我们会发现新版本的代码(服务端)序列化的消息也可以被旧版本的代码(客户端)成功解析。这意味着即使客户端代码没有更新,它仍然可以正常处理包含了新增“生日”字段的消息。需要强调的是,虽然在旧版本的程序(客户端)中并没有明确处理新增的“生日”字段,但它们并没有丢失,而是被视为未知字段。

未知字段:未知字段是指在解析经过结构良好的 Protocol Buffer 序列化数据时,那些旧版本程序无法识别的字段。举例来说,假设我们在新版本的 Protocol Buffer 消息中新增了一个字段,而旧版本的程序没有对这个新字段进行处理。当旧版本的程序尝试解析包含这个新字段的数据时,这个新字段就会被视为未知字段。在这种情况下,旧版本程序可能会忽略或者丢弃这些未知字段,或者采取其他处理方式。

在proto3中的行为:

在之前的 proto3 版本中,解析消息时通常会丢弃未知字段。然而,在 3.5 版本中,重新引入了对未知字段的保留机制。因此,在 3.5 版本或更新版本中,当进行反序列化操作时,未知字段会被保留,并且会被包含在序列化的结果中。

这种行为变化的结果是,即使在更新了消息类型的情况下,新版本的服务端可以与旧版本的客户端进行兼容,而无需对客户端的代码进行修改。这种兼容性是通过保留机制来实现的,它使得新版本的消息在传递给旧版本的程序时不会丢失任何信息。

未知字段从哪获取?

在 PeopleInfo.java 文件中的 PeopleInfo 类中,存在一个名为 getUnknownFields() 的方法,该方法用于获取未知字段。这个方法的存在允许程序员能够检索和处理由旧版本程序无法识别的字段,从而保证了对这些字段的兼容性处理。

UnknownFieldSet 类介绍

UnknownFieldSet 包含在分析消息时遇到但未由其类型定义的所有字段。

import java.util.TreeMap;

// UnknownFieldSet 类用于表示 Protocol Buffers 消息中的未知字段集合
public final class UnknownFieldSet implements MessageLite {
    // fields 是一个 TreeMap,用于存储未知字段,键为字段编号,值为 Field 对象
    private final TreeMap<Integer, Field> fields;
    // defaultInstance 是 UnknownFieldSet 的默认实例,初始化为空的 TreeMap
    private static final UnknownFieldSet defaultInstance = new UnknownFieldSet(new TreeMap());
    // PARSER 是 UnknownFieldSet 的解析器
    private static final Parser PARSER = new Parser();

    // 私有构造函数,用于创建 UnknownFieldSet 实例
    private UnknownFieldSet(TreeMap<Integer, Field> fields) {
        this.fields = fields;
    }

    // 创建一个新的 UnknownFieldSet.Builder 实例
    public static Builder newBuilder() {
        return UnknownFieldSet.Builder.create();
    }

    // 创建一个新的 UnknownFieldSet.Builder 实例,从另一个 UnknownFieldSet 对象进行复制
    public static Builder newBuilder(UnknownFieldSet copyFrom) {
        return newBuilder().mergeFrom(copyFrom);
    }

    // 获取 UnknownFieldSet 的默认实例
    public static UnknownFieldSet getDefaultInstance() {
        return defaultInstance;
    }

    // 获取 UnknownFieldSet 的默认实例
    public UnknownFieldSet getDefaultInstanceForType() {
        return defaultInstance;
    }

    // 判断当前 UnknownFieldSet 对象是否与另一个对象相等
    public boolean equals(Object other) {
        if (this == other) {
            return true;
        } else {
            return other instanceof UnknownFieldSet && this.fields.equals(((UnknownFieldSet)other).fields);
        }
    }

    …………

}
import java.util.List;

// Field 类用于表示 Protocol Buffers 消息中的未知字段
public static final class Field {
    // fieldDefaultInstance 是 Field 类的默认实例
    private static final Field fieldDefaultInstance = newBuilder().build();
    // varint 是存储 varint 类型的未知字段的列表
    private List<Long> varint;
    // fixed32 是存储 fixed32 类型的未知字段的列表
    private List<Integer> fixed32;
    // fixed64 是存储 fixed64 类型的未知字段的列表
    private List<Long> fixed64;
    // lengthDelimited 是存储 lengthDelimited 类型的未知字段的列表
    private List<ByteString> lengthDelimited;
    // group 是存储 group 类型的未知字段的列表
    private List<UnknownFieldSet> group;

    // 私有构造函数,禁止直接实例化 Field 对象
    private Field() {
    }

    // 创建一个新的 Field.Builder 实例
    public static Builder newBuilder() {
        return UnknownFieldSet.Field.Builder.create();
    }

    // 创建一个新的 Field.Builder 实例,从另一个 Field 对象进行复制
    public static Builder newBuilder(Field copyFrom) {
        return newBuilder().mergeFrom(copyFrom);
    }

    // 获取 Field 的默认实例
    public static Field getDefaultInstance() {
        return fieldDefaultInstance;
    }

    // 获取 varint 类型未知字段的列表
    public List<Long> getVarintList() {
        return this.varint;
    }

    // 获取 fixed32 类型未知字段的列表
    public List<Integer> getFixed32List() {
        return this.fixed32;
    }

    // 获取 fixed64 类型未知字段的列表
    public List<Long> getFixed64List() {
        return this.fixed64;
    }

    // 获取 lengthDelimited 类型未知字段的列表
    public List<ByteString> getLengthDelimitedList() {
        return this.lengthDelimited;
    }

    // 获取 group 类型未知字段的列表
    public List<UnknownFieldSet> getGroupList() {
        return this.group;
    }

    …………

}

UnknownFieldSet 类中重写了 toString( ) 方法:

所以我们在代码中可以通过:

// 未知字段
System.out.println("未知字段:\n" + peopleInfo.getUnknownFields());

 打印未知字段。

前后兼容性

当升级一个分布式系统中的某个模块时,通常会涉及到两种不同的模块:新模块和旧模块。在这种情况下,我们希望新模块的通讯协议能够与旧模块保持向前兼容和向后兼容。

  • 向前兼容性意味着旧模块能够正确识别新模块生成或发送的协议。对于旧模块而言,新添加的属性(例如“生日”属性)会被视为未知字段,这意味着即使在更新协议后,旧模块仍然能够解析新模块发送的消息,尽管它可能无法理解新增的属性。
  • 向后兼容性则表示新模块能够正确识别旧模块生成或发送的协议。这意味着新模块能够正确处理旧模块发送的消息,即使旧模块在升级之前并不知道新模块引入的变化。

维护前后兼容性的重要性在于,当一个分布式系统由多个模块组成时,我们很难同时升级所有模块。因此,为了保证在升级过程中整个系统尽可能不受影响,我们需要尽量保持通讯协议的向后兼容或向前兼容。这样,即使在系统的不同部分升级的时间点不同,系统仍然能够正常运行,并且不会因为协议的变化而导致通讯失败或数据丢失的问题。

选项 option

.proto 文件中可以声明许多选项,使用 option 标注。

选项能影响 proto 编译器的某些处理方式。

选项分类

选项的完整列表在 google/protobuf/descriptor.proto 中定义。

在 google/protobuf/descriptor.proto 文件中,选项被定义为能够应用于不同级别的元素,包括文件级别、消息级别、字段级别等等。然而,并没有一种选项能够作用于所有的类型,即没有一种选项能够适用于所有的元素类型。

这意味着在 Protocol Buffers 中,每种选项都有其特定的作用范围和使用场景。例如,文件级选项通常用于指定文件的一般性配置,消息级选项用于指定特定消息的配置,字段级选项则用于指定消息中特定字段的配置。因此,在定义选项时,需要根据其所需的作用范围来选择合适的级别,并且不同级别的选项可以根据需要相互组合使用,以实现更精细的配置和控制。

JAVA 常用选项列举

  • java_multiple_files:这是一个文件级选项,用于指定编译后是否生成多个Java文件。如果设置为true,则每个消息类型会生成一个单独的Java文件;如果设置为false,则所有消息类型会生成在同一个Java文件中。
  • java_package:这是一个文件级选项,用于指定编译后生成的Java文件所在的包路径。
  • java_outer_classname:这是一个文件级选项,用于指定编译后生成的.proto文件的包装类的类名。包装类是一个包含所有消息类型的静态内部类。
  • allow_alias:这是一个枚举级选项,用于定义是否允许在枚举类型中将相同的常量值分配给不同的枚举常量。如果设置为true,则允许定义别名,否则不允许。
     
    enum PhoneType {
      option allow_alias = true;
      MP = 0;
      TEL = 1;
      LANDLINE = 1; // 若不加 option allow_alias = true; 这⼀⾏会编译报错
    }
    
  • java_generic_services:这是一个文件级选项,用于指定是否生成支持泛型服务的代码。如果设置为true,则生成的Java代码将包含用于支持泛型服务的代码。

  • java_string_check_utf8:这是一个文件级选项,用于指定是否在生成的Java代码中对字符串进行UTF-8检查。如果设置为true,则生成的代码将在解析或构建消息时验证字符串是否为有效的UTF-8编码。

  • optimize_for:这是一个文件级选项,用于指定代码生成器优化生成的代码以便于速度、大小或代码的通用性。它有三个可能的值:SPEED(速度)、CODE_SIZE(代码大小)和 LITE_RUNTIME(轻量级运行时)。

  • cc_generic_services:这是一个文件级选项,用于指定是否生成支持泛型服务的C++代码。如果设置为true,则生成的C++代码将包含用于支持泛型服务的代码。

  • deprecated:这是一个字段级选项,用于标记该字段已被弃用。当设置为true时,编译器会在生成的代码中生成相应的注释或警告。

设置自定义选项

Protocol Buffers 允许用户定义和使用自定义选项(Custom Options)。这使得用户能够根据自己的需求添加额外的元数据或配置信息到 .proto 文件中,以便生成的代码能够根据这些选项进行相应的处理。

用户可以通过在 .proto 文件中使用 option 关键字来定义选项,然后在生成代码时使用这些选项。自定义选项的语法与其他选项类似,它们的名称最好使用一些特殊的前缀或命名约定以避免与标准选项冲突。

import "google/protobuf/descriptor.proto";

// 定义一个自定义选项
option my_option = "hello_world";

// 在消息定义中使用自定义选项
message MyMessage {
  option (my_option) = true;
  // 其他消息字段...
}

在这个例子中,我们定义了一个名为 my_option 的自定义选项,并将其设置为 "hello_world"。然后,在消息 MyMessage 的定义中,我们使用了这个自定义选项,并将其设置为 true。在生成代码时,可以根据这个选项的值执行相应的逻辑。

使用自定义选项可以使 Protocol Buffers 更加灵活和适应不同的需求,但需要注意的是,生成的代码必须能够正确地处理这些自定义选项才能发挥其作用。

感兴趣可以参考官方文档:Language Guide (proto 2) | Protocol Buffers Documentation (protobuf.dev)

通讯录 4.0 实现——网络版

在通讯协议和服务端数据交换场景中,Protocol Buffers(Protobuf)是一种常用的工具。在这个示例中,我们将创建一个网络版本的通讯录,用于模拟客户端和服务端之间的交互。通过使用 Protobuf 实现协议序列化,我们可以确保各个端之间的数据交换能够顺利进行。

具体需求如下:

  • 客户端:客户端将负责向服务端发送联系人信息,并等待服务端的响应。
  • 服务端:服务端将接收客户端发送的联系人信息,并将其打印出来。
  • 交互数据:客户端和服务端之间的数据交换将使用 Protobuf 进行序列化和反序列化。

代码环境

Maven 项目 + UDP 数据报套接字编程

约定双端交互req/resp

contacts.proto(注意:客⼾端服务端都私有一份)

syntax = "proto3";
package client_internet;

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

message Request {
  string name = 1; // 姓名
  int32 age = 2; // 年龄
  message Phone {
    string number = 1; // 电话号码
  }
  repeated Phone phone = 3; // 电话
}

message Response {
  string uid = 1;
}
syntax = "proto3";
package service_internet;

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

message Request {
  string name = 1; // 姓名
  int32 age = 2; // 年龄
  message Phone {
    string number = 1; // 电话号码
  }
  repeated Phone phone = 3; // 电话
}

message Response {
  string uid = 1;
}

然后编译一下。 

客户端代码实现

ContactsClient.java:

package com.example.internet.client;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.SocketAddress;

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 request = Request.newBuilder()
                .setName("张三")
                .setAge(20)
                .addPhone(Request.Phone.newBuilder()
                        .setNumber("1311111")
                        .build())
                .build();
        byte[] requestData = request.toByteArray();
        // byte[] requestData = {'h','e','l','l','o'};
        // 创建 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[] responseData = BytesUtils.subByte(udpResponse, 0, length);
        Response response =  Response.parseFrom(responseData);
        System.out.printf("---->接收到服务端返回的响应:%s", response.toString());

        // System.out.printf("---->接收到服务端返回的响应:%s", new String(responseData));
    }

}

BytesUtils.java:bytes工具类:

package com.example.internet.client;

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

服务端代码实现

ContactsService.java:

package com.example.internet.service;

import java.io.IOException;
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 request = Request.parseFrom(requestData);
            System.out.println("---->接收到请求数据:");
            // System.out.println(new String(requestData));
            System.out.println(request.toString());

            Response response = Response.newBuilder()
                    .setUid("111111111")
                    .build();
            byte[] responseData = response.toByteArray();
            // byte[] responseData = {'s', 'u', 'c', 'c', 'e', 's', 's'};
            // 构造 response 数据报,注意接收的客⼾端数据报包含IP和端⼝号,要设置到响应的数据报中
            DatagramPacket responsePacket = new
                    DatagramPacket(responseData, responseData.length,
                    requestPacket.getSocketAddress());
            // 发送 response 数据报
            socket.send(responsePacket);
            System.out.println("---->发送响应成功!\n");
        }
    }
}

BytesUtils.java:bytes工具类:

package com.example.internet.service;

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

 

总结

序列化能力对比验证

在这个测试中,我们将分别测试使用 Protocol Buffers(PB)和 JSON 进行序列化和反序列化的性能,以便对具有相同值的一份结构化数据进行多次性能测试。

为了方便阅读,以下展示了需要进行测试的结构化数据内容,使用了 JSON 格式:

{
  "age": 20,
  "name": "米莱",
  "phone": [
    {
      "number": "110112119",
      "type": 0
    },
    {
      "number": "110112119",
      "type": 0
    },
    {
      "number": "110112119",
      "type": 0
    },
    {
      "number": "110112119",
      "type": 0
    },
    {
      "number": "110112119",
      "type": 0
    }
  ],
  "qq": "95991122",
  "address": {
    "home_address": "江苏省南京市鼓楼区",
    "unit_address": "江苏省南京市鼓楼区"
  },
  "remark": {
    "key1": "value1",
    "key2": "value2",
    "key3": "value3",
    "key4": "value4",
    "key5": "value5"
  }
}

开始进行测试代码编写,我们在新的目录 src/main/proto/compare 下新建 contacts.proto 文件。

内容如下:

syntax = "proto3";
package compare_serialization;
option java_multiple_files = true; // 编译后⽣成的⽂件是否分为多个⽂件
option java_package = "com.example.compare"; // 编译后⽣成⽂件所在的包路径
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 weixin = 6;
  }
  map<string, string> remark = 7; // 备注
}

编译一下:

使用 maven 插件编译文件后,先添加相关JSON的依赖:

        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.14.2</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>2.0.28</version>
        </dependency>

再新增 PeopleInfoForJson 类,方便使用 JSON 进行测试:

package com.example.compare;

import lombok.Getter;
import lombok.Setter;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Getter
@Setter
public class PeopleInfoForJson {
    @Getter
    @Setter
    public static class Phone {
        private String number;
        enum PhoneType {
            MP, TEL
        }
        private PhoneType type;
    }
    @Getter
    @Setter
    public static class Address {
        private String homeAddress;
        private String unitAddress;
    }

    private String name;
    private int age;
    private List<Phone> phones = new ArrayList<>();
    private Address address;
    private String qq;
    private String wechat;
    Map<String, String> remark = new HashMap<>();
}

在新建的性能测试文件 Compare.java 中,我们对相同的结构化数据进行 100、1000、10000、100000 次的序列化与反序列化操作。我们将使用 PB、fastjson2 和 jackson 这三种序列化工具,并分别记录它们的耗时以及序列化后的数据大小。具体内容如下:

package com.example.compare;

import com.alibaba.fastjson2.JSON;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.protobuf.Any;
import com.google.protobuf.InvalidProtocolBufferException;

import static com.example.compare.PeopleInfoForJson.Phone.PhoneType.MP;

public class Compare {
    private static int TEST_COUNT = 100000;

    public static void main(String[] args) throws InvalidProtocolBufferException, JsonProcessingException {

        int count;
        byte[] pbBytes = new byte[0];
        String jsonStr = null;
        ObjectMapper objectMapper = new ObjectMapper();

        // ------------------------------Protobuf 序列化 -----------------------------------
        {
            PeopleInfo peopleInfo = buildPeopleInfo();
            count = TEST_COUNT;
            long stime = System.currentTimeMillis();
            // 序列化count次
            while ((count--) > 0) {
                pbBytes = peopleInfo.toByteArray();
            }
            long etime = System.currentTimeMillis();
            System.out.printf("%d次 [pb序列化]耗时:%dms, 序列化后的大小: %d\n",
                    TEST_COUNT, etime-stime, pbBytes.length);
        }

        // ------------------------------Protobuf 反序列化 ---------------------------------
        {
            count = TEST_COUNT;
            long stime = System.currentTimeMillis();
            // 反序列化count次
            while ((count--) > 0) {
                PeopleInfo.parseFrom(pbBytes);
            }
            long etime = System.currentTimeMillis();
            System.out.printf("%d次 [pb反序列化]耗时:%dms\n",
                    TEST_COUNT, etime-stime);
        }

        // ---------------------------- fastjson2 序列化 ------------------------------------
        {
            PeopleInfoForJson peopleInfoForJson = buildPeopleInfoForJson();
            count = TEST_COUNT;
            long stime = System.currentTimeMillis();
            // 序列化count次
            while ((count--) > 0) {
                jsonStr = JSON.toJSONString(peopleInfoForJson);
                //JSON.toJSONString(peopleInfoForJson);

            }
            long etime = System.currentTimeMillis();
            System.out.printf("%d次 [fastjson2序列化]耗时:%dms, 序列化后的大小: %d\n",
                    TEST_COUNT, etime-stime, jsonStr.length());
        }

        // --------------------------- fastjson2 反序列化 -----------------------------------
        {
            count = TEST_COUNT;
            long stime = System.currentTimeMillis();
            // 反序列化count次
            while ((count--) > 0) {
                JSON.parseObject(jsonStr, PeopleInfoForJson.class);
            }
            long etime = System.currentTimeMillis();
            System.out.printf("%d次 [fastjson2反序列化]耗时:%dms\n",
                    TEST_COUNT, etime-stime);
        }

        // ------------------------------jackson 序列化 ---------------------------------------
        {
            PeopleInfoForJson peopleInfoForJson = buildPeopleInfoForJson();
            count = TEST_COUNT;
            long stime = System.currentTimeMillis();
            // 序列化count次
            while ((count--) > 0) {
                jsonStr = objectMapper.writeValueAsString(peopleInfoForJson);
            }
            long etime = System.currentTimeMillis();
            System.out.printf("%d次 [jackson序列化]耗时:%dms, 序列化后的大小: %d\n",
                    TEST_COUNT, etime-stime, jsonStr.length());
        }

        // ------------------------------jackson 反序列化 -------------------------------------
        {
            count = TEST_COUNT;
            long stime = System.currentTimeMillis();
            // 反序列化count次
            while ((count--) > 0) {
                objectMapper.readValue(jsonStr, PeopleInfoForJson.class);
            }
            long etime = System.currentTimeMillis();
            System.out.printf("%d次 [jackson反序列化]耗时:%dms\n",
                    TEST_COUNT, etime-stime);
        }
    }

    private static PeopleInfo buildPeopleInfo() {
        PeopleInfo.Builder peopleBuilder = PeopleInfo.newBuilder();
        peopleBuilder.setName("米莱");
        peopleBuilder.setAge(20);
        peopleBuilder.setQq("95991122");
        for(int i = 0; i < 5; i++) {
            PeopleInfo.Phone.Builder phoneBuild = PeopleInfo.Phone.newBuilder();
            phoneBuild.setNumber("110112119");
            phoneBuild.setType(PeopleInfo.Phone.PhoneType.MP);
        }
        com.example.proto3.Address.Builder addressBuilder = com.example.proto3.Address.newBuilder();
        addressBuilder.setHomeAddress("江苏省南京市鼓楼区");
        addressBuilder.setUnitAddress("江苏省南京市鼓楼区");
        peopleBuilder.setData(Any.pack(addressBuilder.build()));
        peopleBuilder.putRemark("key1", "value1");
        peopleBuilder.putRemark("key2", "value2");
        peopleBuilder.putRemark("key3", "value3");
        peopleBuilder.putRemark("key4", "value4");
        peopleBuilder.putRemark("key5", "value5");
        return peopleBuilder.build();
    }

    private static PeopleInfoForJson buildPeopleInfoForJson() {
        PeopleInfoForJson peopleInfo = new PeopleInfoForJson();
        peopleInfo.setName("米莱");
        peopleInfo.setAge(20);
        peopleInfo.setQq("95991122");
        for(int i = 0; i < 5; i++) {
            PeopleInfoForJson.Phone phone = new PeopleInfoForJson.Phone();
            phone.setNumber("110112119");
            phone.setType(MP);
            peopleInfo.getPhones().add(phone);
        }
        PeopleInfoForJson.Address address = new PeopleInfoForJson.Address();
        address.setHomeAddress("江苏省南京市鼓楼区");
        address.setUnitAddress("江苏省南京市鼓楼区");
        peopleInfo.setAddress(address);
        peopleInfo.getRemark().put("key1", "value1");
        peopleInfo.getRemark().put("key2", "value2");
        peopleInfo.getRemark().put("key3", "value3");
        peopleInfo.getRemark().put("key4", "value4");
        peopleInfo.getRemark().put("key5", "value5");
        return peopleInfo;
    }
}

根据实验结果得出以下结论:

  • 编解码性能方面:ProtoBuf > Jackson > FastJSON。
  • 内存占用方面:ProtoBuf > Jackson ≈ FastJSON。

需要注意的是,以上结论基于该项实验的数据得出。实际上,由于受到不同字段类型、字段数量等因素的影响,实际测试结果可能会有所不同。

该实验仍然存在许多可以优化的地方,但即使是这种粗略的测试也清楚地展示了 ProtoBuf 的优势。ProtoBuf 在性能和内存占用方面表现出色,尤其在需要高效数据传输的场景下具有明显的优势。

序列化协议通用性格式可读性序列化大小序列化性能适用场景
JSON通用文本格式轻量Web项目。因为浏览器对于JSON数据支持非常好,有很多内建的函数支持。
XML通用文本格式重量XML作为一种扩展标记语言,衍生出了HTML、RDF/RDFS,它强调数据结构化的能力和可读性。
ProtoBuf独立二进制格式轻量适合高性能,对响应速度有要求的数据传输场景。Protobuf比XML、JSON 更小、更快。
  1. XML、JSON 和 ProtoBuf 都具备数据结构化和数据序列化的能力。这意味着它们都能够组织数据以及将数据转换成可传输的格式。

  2. XML 和 JSON 更加注重数据的结构化,强调可读性和语义表达能力。这两种格式通常用于需要人类可读的数据交换,例如配置文件、RESTful API 的数据传输等。相比之下,ProtoBuf 更注重数据的序列化,更关注效率、空间和速度。尽管 ProtoBuf 也能表示数据结构,但为了达到极致的效率,它可能会舍弃一部分元信息,导致可读性较差、语义表达能力不足。

  3. ProtoBuf 在应用场景上更为明确,通常用于对性能要求较高、对数据传输效率有严格要求的场景。例如,大规模分布式系统中的数据通信、高性能网络服务的数据交换等。而 XML 和 JSON 的应用场景更为丰富,可以用于各种需要人类可读的数据交换场景,例如 Web 开发、配置文件、数据存储等。

  • 21
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值