ProtoBuf速成【基于C++讲解】

ProtoBuf速成

  • [1. 初识 ProtoBuf](#1. 初识 ProtoBuf)
    • [1.1 序列化概念](#1.1 序列化概念)
    • [1.2 ProtoBuf 是什么](#1.2 ProtoBuf 是什么)
    • [1.3 ProtoBuf 的使用特点](#1.3 ProtoBuf 的使用特点)
  • [2. 快速上手(通讯录1.0)](#2. 快速上手(通讯录1.0))
    • [2.1 创建 .proto 文件](#2.1 创建 .proto 文件)
    • [2.2 编译 contacts.proto 文件,生成 C++ 文件](#2.2 编译 contacts.proto 文件,生成 C++ 文件)
    • [2.3 小结 ProtoBuf 使用流程](#2.3 小结 ProtoBuf 使用流程)
  • [3. proto3 语法详解](#3. proto3 语法详解)
    • [3.1 字段规则](#3.1 字段规则)
    • [3.2 消息类型的定义与使用(通讯录2.0)](#3.2 消息类型的定义与使用(通讯录2.0))
      • [3.2.1 创建通讯录 2.0 版本](#3.2.1 创建通讯录 2.0 版本)
      • [3.2.2 通讯录 2.0 的写入实现](#3.2.2 通讯录 2.0 的写入实现)
      • [3.2.3 通讯录 2.0 的读取实现](#3.2.3 通讯录 2.0 的读取实现)
    • [3.3 enum 枚举类型(通讯录2.1)](#3.3 enum 枚举类型(通讯录2.1))
      • [3.3.1 升级通讯录至 2.1 版本](#3.3.1 升级通讯录至 2.1 版本)
    • [3.4 Any 类型(通讯录2.2)](#3.4 Any 类型(通讯录2.2))
      • [3.4.1 升级通讯录至 2.2 版本](#3.4.1 升级通讯录至 2.2 版本)
    • [3.5 oneof 类型(通讯录2.3)](#3.5 oneof 类型(通讯录2.3))
      • [3.5.1 升级通讯录至 2.3 版本](#3.5.1 升级通讯录至 2.3 版本)
    • [3.6 map 类型(通讯录2.4)](#3.6 map 类型(通讯录2.4))
      • [3.6.1 升级通讯录至 2.4 版本](#3.6.1 升级通讯录至 2.4 版本)
    • [3.7 默认值](#3.7 默认值)
    • [3.8 更新信息](#3.8 更新信息)
      • [3.8.1 更新规则](#3.8.1 更新规则)
      • [3.8.2 保留字段 reserved(通讯录3.0)](#3.8.2 保留字段 reserved(通讯录3.0))
        • [3.8.2.1 创建通讯录 3.0 版本---验证 错误删除字段 造成的数据损坏](#3.8.2.1 创建通讯录 3.0 版本---验证 错误删除字段 造成的数据损坏)
      • [3.8.3 未知字段(通讯录3.1)](#3.8.3 未知字段(通讯录3.1))
        • [3.8.3.1 未知字段从哪获取](#3.8.3.1 未知字段从哪获取)
        • [3.8.3.2 升级通讯录 3.1 版本---验证未知字段](#3.8.3.2 升级通讯录 3.1 版本---验证未知字段)
      • [3.8.4 前后兼容性](#3.8.4 前后兼容性)
    • [3.9 选项 option](#3.9 选项 option)
      • [3.9.1 选项分类](#3.9.1 选项分类)
      • [3.9.2 常用选项列举](#3.9.2 常用选项列举)

1. 初识 ProtoBuf


1.1 序列化概念


1. 序列化和反序列化

  • 序列化:把对象转换为字节序列的过程,称为对象的序列化。
  • 反序列化:把字节序列恢复为对象的过程,称为对象的反序列化。

2. 什么情况下需要序列化

  • 存储数据:当你想把的内存中的对象状态保存到一个文件中或者存到数据库中时。
  • 网络传输:网络直接传输数据,但是无法直接传输对象,所以要在传输前序列化,传输完成后反序列化成对象。例如我们之前学习过 socket 编程中发送与接收数据。

3. 如何实现序列化

  • xmljsonprotobuf

1.2 ProtoBuf 是什么


1. 我们先来看看官方给出的答案是什么

  • Protocol buffers are Google's language-neutral, platform-neutral, extensible mechanism for serializing structure data ‒ think XML, but smaller, faster, and simpler. You define how you want your data to be structured once, then you can use special generated source code to easily write and read your structured data to and from a variety of data streams and using a variety of languages.

2. 翻译过来的意思就是

  • Protocol Buffers Google 的一种语言无关、平台无关、可扩展的序列化结构数据方法,它可用于(数据)通信协议、数据存储等。
  • Protocol Buffers 类比于 XML,是一种灵活,高效,自动化机制的结构数据序列化方法,但是比XML更小、更快、更为简单。
  • 你可以定义数据的结构,然后使用特殊生成的源代码轻松的在各种数据流中使⽤各种语言进行编写和读取结构数据。你甚至可以更新数据结构,而不破坏由旧数据结构编译的已部署程序。

3. 简单来讲,ProtoBuf(全称为 Protocol Buffer)是让结构数据序列化的方法,其具有以下特点:

  • 语言无关、平台无关:即 ProtoBuf 支持 Java、C++、Python等多种语言,支持多个平台。
  • 高效:即比 XML 更小、更快、更为简单。
  • 扩展性、兼容性好:你可以更新数据结构,而不影响和破坏原有的旧程序。

1.3 ProtoBuf 的使用特点


  • 编写 .proto 文件,目的是为了定义结构对象(message)及属性内容。
  • 使用 protoc 编译器编译 .proto 文件,生成一系列接口代码,存放在新生成头文件和源文件中。
  • 依赖生成的接口,将编译生成的头文件包含进我们的代码中,实现对 .proto 文件中定义的字段进行设置和获取,和对 message 对象进行序列化和反序列化。

总的来说:ProtoBuf 是需要依赖通过编译生成的头文件和源文件来使用的。有了这种代码生成机制,开发人员再也不用吭哧吭哧地编写那些协议解析的代码了(干这种活是典型的吃力不讨好)。


2. 快速上手(通讯录1.0)

  • 在快速上手中,会编写第一版本的通讯录 1.0。在通讯录 1.0 版本中,将实现:
    • 对一个联系人的信息使用 PB 进行序列化,并将结果打印出来。
    • 对序列化后的内容使用 PB 进行反序列,解析出联系人信息并打印出来。
    • 联系人包含以下信息:姓名、年龄。

通过通讯录 1.0,我们便能了解使用 ProtoBuf 初步要掌握的内容,以及体验到 ProtoBuf 的完整使用流程。


2.1 创建 .proto 文件


1. 文件规范

  • 创建 .proto 文件时,文件命名应该使用全小写字母命名,多个字母之间用 _ 连接。 例如:lower_snake_case.proto
  • 书写 .proto 文件代码时,应使用 2 个空格的缩进(不做定性要求)。

我们为通讯录 1.0 新建文件:contacts.proto

2. 注释

  • 向文件添加注释,可使用 // 或者 /* ... */

3. 指定 proto3 语法

  • Protocol Buffers 语言版本3,简称 proto3,是 .proto 文件最新的语法版本。proto3 简化了 Protocol Buffers 语言,既易于使用,又可以在更广泛的编程语言中使用。它允许你使用 Java,C++,Python 等多种语言生成 protocol buffer 代码。
  • .proto 文件中,要使用 syntax = "proto3";来指定文件语法为 proto3,并且必须写在除去注释内容的第一行。 如果没有指定,编译器会使用 proto2 语法。
  • 在通讯录 1.0 的 contacts.proto 文件中,可以为文件指定 proto3 语法,内容如下:
bash 复制代码
syntax = "proto3";

4. package 声明符

  • package 是一个可选的声明符,能表示 .proto 文件的命名空间,在项目中要有唯一性。它的作用是为了避免我们定义的消息出现冲突。
proto 复制代码
package contacts;

5. 定义消息(message)

  • 消息(message):要定义的结构化对象,我们可以给这个结构化对象中定义其对应的属性内容。

  • 这里再提一下为什么要定义消息?

    • 在网络传输中,我们需要为传输双方定制协议。定制协议说白了就是定义结构体或者结构化数据,比如,tcp,udp 报文就是结构化的。
    • 再比如将数据持久化存储到数据库时,会将一系列元数据统一用对象组织起来,再进行存储。
  • 所以 ProtoBuf 就是以 message 的方式来支持我们定制协议字段,后期帮助我们形成类和方法来使用。在通讯录 1.0 中我们就需要为 联系人 定义一个 message

  • .proto 文件中定义一个消息类型的格式为:

bash 复制代码
message 消息类型名{

}
消息类型命名规范:使⽤驼峰命名法,⾸字母大写。
  • contacts.proto(通讯录 1.0)新增联系人 message,内容如下:
java 复制代码
// 首行:语法指定行
syntax = "proto3";
package contacts;

// 定义联系人message
message PeopleInfo{
    
}

6. 定义消息字段

message 中我们可以定义其属性字段,字段定义格式为:字段类型 字段名 = 字段唯一编号;

  • 字段名称命名规范:全小写字母,多个字母之间用 _ 连接。
  • 字段类型分为:标量数据类型 和 特殊类型(包括枚举、其他消息类型等)。
  • 字段唯一编号:用来标识字段,一旦开始使用就不能够再改变。

该表格展示了定义于消息体中的标量数据类型,以及编译 .proto 文件之后自动生成的类中与之对应的字段类型。在这里展示了与 C++ 语言对应的类型。

.proto Type Notes C++ Type
double double
float float
int32 使用变长编码。负数的编码效率较低------若字段可能为负值,应使用 sint32 代替。 int32
int64 使用变长编码。负数的编码效率较低------若字段可能为负值,应使用 sint64 代替。 int64
uint32 使用变长编码。 uint32
uint64 使用变长编码。 uint64
sint32 使用变长编码。符号整型。负值的编码效率高于常规的 int32 类型。 int32
sint64 使用变长编码。符号整型。负值的编码效率高于常规的 int64 类型。 int64
fixed32 定长 4 字节。若值常⼤于2^28 则会比 uint32 更高效。 uint32
fixed64 定长 8 字节。若值常⼤于2^56 则会比 uint64 更高效。 uint64
sfixed32 定长 4 字节。 int32
sfixed64 定长 8 字节。 int64
bool bool
string 包含 UTF-8 和 ASCII 编码的字符串,长度不能超过 2^32 。 string
bytes 可包含任意的字节序列但长度不能超过 2^32 。 string

变长编码是指:经过 protobuf 编码后,原本4字节或8字节的数可能会被变为其他字节数。

  • 更新 contacts.proto (通讯录1.0),新增姓名、年龄字段:
java 复制代码
// 首行:语法指定行
syntax = "proto3";
package contacts;

// 定义联系人message
message PeopleInfo{
    string name = 1;    // 姓名
    int32 age = 2;      // 年龄
}
  • 在这里还要特别讲解一下字段唯一编号的范围:

1 ~ 536,870,911 (2^29 - 1) ,其中 19000 ~ 19999 不可用。

  • 19000 ~ 19999 不可用是因为:在 Protobuf 协议的实现中,对这些数进行了预留。如果非要在 .proto 文件中使用这些预留标识号,例如将 name 字段的编号设置为19000,编译时就会报警:
c 复制代码
// 消息中定义了如下编号,代码会告警: 
// Field numbers 19,000 through 19,999 are reserved for the protobuf implementation
string name = 19000; 
  • 值得一提的是,范围为 1 ~ 15 的字段编号需要一个字节进行编码, 16 ~ 2047 内的数字需要两个字节进行编码。编码后的字节不仅只包含了编号,还包含了字段类型。所以 1 ~ 15 要用来标记出现非常频繁的字段,要为将来有可能添加的、频繁出现的字段预留一些出来。

2.2 编译 contacts.proto 文件,生成 C++ 文件


1. 编译命令

  • 编译命令行格式为:
0 复制代码
protoc [--proto_path=IMPORT_PATH] --cpp_out=DST_DIR path/to/file.proto

protoc 					是 Protocol Buffer 提供的命令行编译工具。
--proto_path 			指定 被编译的.proto文件所在目录,可多次指定。可简写成 -I IMPORT_PATH 。
						如不指定该参数,则在当前目录进行搜索。当某个.proto 文件 import 其他 
						.proto 文件时,或需要编译的 .proto 文件不在当前目录下,这时就要用-I来指定搜索目录。
						
--cpp_out= 				指编译后的文件为 C++ 文件。
OUT_DIR 				编译后生成文件的目标路径。
path/to/file.proto 		要编译的.proto文件。
  • 编译 contacts.proto 文件命令如下:
0 复制代码
protoc --cpp_out=. contacts.proto

2. 编译 contacts.proto 文件后会生成什么

  • 编译 contacts.proto 文件后,会生成所选择语言的代码,我们选择的是C++,所以编译后生成了两个文件:contacts.pb.h contacts.pb.cc
  • 对于编译生成的 C++ 代码,包含了以下内容 :
    • 对于每个 message ,都会生成一个对应的消息类。
    • 在消息类中,编译器为每个字段提供了获取和设置方法,以及一下其他能够操作字段的方法。
    • 编辑器会针对于每个 .proto 文件生成 .h.cc 文件,分别用来存放类的声明与类的实现。
  • contacts.pb.h 部分代码展示:
cpp 复制代码
class PeopleInfo final : public ::PROTOBUF_NAMESPACE_ID::Message {
 public:
 using ::PROTOBUF_NAMESPACE_ID::Message::CopyFrom;
 void CopyFrom(const PeopleInfo& from);
 using ::PROTOBUF_NAMESPACE_ID::Message::MergeFrom;
 void MergeFrom( const PeopleInfo& from) {
 	PeopleInfo::MergeImpl(*this, from);
 }
 static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() {
 	return "PeopleInfo";
 }
 
 // string name = 1;
 void clear_name();
 const std::string& name() const;
 template <typename ArgT0 = const std::string&, typename... ArgT>
 void set_name(ArgT0&& arg0, ArgT... args);
 std::string* mutable_name();
 PROTOBUF_NODISCARD std::string* release_name();
 void set_allocated_name(std::string* name);
 
 // int32 age = 2;
 void clear_age();
 int32_t age() const;
 void set_age(int32_t value);
};

上述的例子中:

  • 每个字段都有设置和获取的方法,getter 的名称与小写字段完全相同,setter 方法以 set_ 开头。
  • 每个字段都有一个 clear_ 方法,可以将字段重新设置回 empty 状态。
  • contacts.pb.cc 中的代码就是对类声明方法的一些实现,在这里就不展开了。

到这里有同学可能就有疑惑了,那之前提到的序列化和反序列化方法在哪里呢?在消息类的父类 MessageLite 中,提供了读写消息实例的方法,包括序列化方法和反序列化方法。

c 复制代码
class MessageLite {
public:
 //序列化: 
 bool SerializeToOstream(ostream* output) const; // 将序列化后数据写入文件流 
 bool SerializeToArray(void *data, int size) const;
 bool SerializeToString(string* output) const;
 
 //反序列化: 
 bool ParseFromIstream(istream* input); // 从流中读取数据,再进⾏反序列化动作 
 bool ParseFromArray(const void* data, int size);
 bool ParseFromString(const string& data);
};
  • 注意:
    • 序列化的结果为二进制字节序列,而非文本格式。
    • 以上三种序列化的方法没有本质上的区别,只是序列化后输出的格式不同,可以供不同的应用场景使用。
    • 序列化的 API 函数均为 const 成员函数,因为序列化不会改变类对象的内容, 而是将序列化的结果保存到函数入参指定的地址中。
    • 详细 message API 可以参见 完整列表

3. 序列化与反序列化的使用

  • 创建一个测试文件 main.cc,方法中我们实现:
    • 对一个联系人的信息使用 PB 进行序列化,并将结果打印出来。
    • 对序列化后的内容使用 PB 进行反序列,解析出联系人信息并打印出来。
cpp 复制代码
#include <iostream>
#include "contacts.pb.h"

int main()
{
    std::string people_str;
    // 对一个联系人的信息使用PB进行序列化,并将结果打印出来
    contacts::PeopleInfo people;
    people.set_name("张三");
    people.set_age(20);
    if (!people.SerializeToString(&people_str)){
        std::cout << "序列化联系人失败!" << std::endl;
        return -1;
    }
    std::cout << "序列化成功,结果:" << people_str << std::endl;

    {
        // 对序列化后的内容使用PB进行反序列化,解析出联系人信息并打印出来
        contacts::PeopleInfo people;
        if (!people.ParseFromString(people_str)){
            std::cout << "反序列化联系人失败!" << std::endl;
            return -1;
        }
        std::cout << "反序列化成功,结果:" << std::endl
                    << "姓名:" << people.name() << std::endl
                    << "年龄:" << people.age() << std::endl;
    }
    return 0;
}
  • 代码书写完成后,编译 main.cc,生成可执行程序 TestPb
    • -lprotobuf:必加,不然会有链接错误。
    • -std=c++11:必加,使用C++11语法。
bash 复制代码
g++ main.cc contacts.pb.cc -o TestPBb -std=c++11 -lprotobuf
  • 执行 TestPb,可以看见 people 经过序列化和反序列化后的结果:
bash 复制代码
(base) ubuntu@Ubuntu22:~/Learn/protubuf-learn/fast_start$ ./TestPb 
序列化成功,结果:
张三
反序列化成功,结果:
姓名:张三
年龄:20
  • 由于 ProtoBuf 是把联系人对象序列化成了二进制序列,这里用 string 来作为接收二进制序列的容器。所以在终端打印的时候会有换行等一些乱码显示。
  • 所以相对于 xml 和 JSON 来说,因为被编码成二进制,破解成本增大,ProtoBuf 编码是相对安全的。

2.3 小结 ProtoBuf 使用流程


  • 编写 .proto 文件,目的是为了定义结构对象(message)及属性内容。
  • 使用 protoc 编译器编译 .proto 文件,生成一系列接口代码,存放在新生成头文件和源文件中。
  • 依赖生成的接口,将编译生成的头文件包含进我们的代码中,实现对 .proto 文件中定义的字段进行设置和获取,和对 message 对象进行序列化和反序列化。

总的来说:ProtoBuf 是需要依赖通过编译生成的头文件和源文件来使用的。有了这种代码生成机制,开发人员再也不用吭哧吭哧地编写那些协议解析的代码了(干这种活是典型的吃力不讨好)。


3. proto3 语法详解

  • 在语法详解部分,依旧使用 项目推进 的方式完成教学。这个部分会对通讯录进行多次升级,使用 2.x 表示升级的版本,最终将会升级如下内容:
    • 不再打印联系人的序列化结果,而是将通讯录序列化后并写入文件中;
    • 从文件中将通讯录解析出来,并进行打印;
    • 新增联系人属性,共包括:姓名、年龄、电话信息、地址、其他联系方式、备注。

3.1 字段规则


消息的字段可以用下面几种规则来修饰:

  • singular:消息中可以包含该字段零次或一次(不超过一次)。proto3 语法中,字段默认使用该规则。
  • repeated:消息中可以包含该字段任意多次(包括零次),其中重复值的顺序会被保留。可以理解为定义了一个数组。

更新 contacts.protoPeopleInfo 消息中新增 phone_numbers 字段,表示一个联系人有多个号码,可将其设置为 repeated,写法如下:

java 复制代码
syntax = "proto3";
package contacts;

message PeopleInfo {
  string name = 1;
  int32 age = 2;
  repeated string phone_numbers = 3;
}

3.2 消息类型的定义与使用(通讯录2.0)


1. 定义

  • 在单个 .proto 文件中可以定义多个消息体,且支持定义嵌套类型的消息(任意多层)。每个消息体中的字段编号可以重复。
  • 更新 contacts.proto,我们可以将 phone_number 提取出来,单独成为一个消息:
java 复制代码
// -------------------------- 嵌套写法 -------------------------
syntax = "proto3";
package contacts;

message PeopleInfo {
 	string name = 1;
 	int32 age = 2;
	message Phone {
   	  string number = 1;
  }
}
// -------------------------- 非嵌套写法 -------------------------
syntax = "proto3";
package contacts;

message Phone {
  string number = 1;
}

message PeopleInfo {
  string name = 1;
  int32 age = 2;
}

2. 使用

  • 消息类型可作为字段类型使用:
java 复制代码
syntax = "proto3";
package contacts;

// 联系人
message PeopleInfo {
 	string name = 1;
 	int32 age = 2;
 	message Phone {
 		string number = 1;
 	}
 	repeated Phone phone = 3;
}
  • 可导入其他 .proto 文件的消息并使用:
    • 例如 Phone 消息定义在 phone.proto 文件中:
java 复制代码
syntax = "proto3";
package phone;

message Phone {
 string number = 1;
}
    • contacts.proto 中的 PeopleInfo 使用 Phone 消息:
java 复制代码
syntax = "proto3";
package contacts;

import "phone.proto"; // 使用 import 将 phone.proto 文件导入进来 !!!

message PeopleInfo {
 	string name = 1;
 	int32 age = 2;

 	// 引入的文件声明了package,使用消息时,需要用 '命名空间.消息类型' 格式

 	repeated phone.Phone phone = 3;
}

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


3.2.1 创建通讯录 2.0 版本


通讯录 2.x 的需求是向文件中写入通讯录列表,以上我们只是定义了一个联系人的消息,并不能存放通讯录列表,所以还需要在完善一下 contacts.proto (终版通讯录 2.0):

java 复制代码
syntax = "proto3";
package contacts;

// 定义联系人message
message PeopleInfo{
    string name = 1;    // 姓名
    int32 age = 2;      // 年龄
    message Phone{
        string number = 1;  // 字段编号和外层不冲突
    }
    repeated Phone phone = 3;   // 电话数组
}

// 通讯录message
message Contacts{
    repeated PeopleInfo contacts = 1;
}
  • 接着进行一次编译:
bash 复制代码
protoc --cpp_out=. contacts.proto
  • 编译后生成的 contacts.pb.h contacts.pb.cc 会将在快速上手的生成文件覆盖掉。contacts.pb.h 更新的部分代码展示:
cpp 复制代码
// 新增了 PeopleInfo_Phone 类
class PeopleInfo_Phone final : public ::PROTOBUF_NAMESPACE_ID::Message {
	public:
 		using ::PROTOBUF_NAMESPACE_ID::Message::CopyFrom;
 		void CopyFrom(const PeopleInfo_Phone& from);
 		using ::PROTOBUF_NAMESPACE_ID::Message::MergeFrom;
 		void MergeFrom( const PeopleInfo_Phone& from) {
 			PeopleInfo_Phone::MergeImpl(*this, from);
 		}
 		static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() {
 			return "PeopleInfo.Phone";
 		}
 
 		// string number = 1;
 		void clear_number();
 		const std::string& number() const;
 		template <typename ArgT0 = const std::string&, typename... ArgT>
 		void set_number(ArgT0&& arg0, ArgT... args);
 		std::string* mutable_number();
		PROTOBUF_NODISCARD std::string* release_number();
 		void set_allocated_number(std::string* number);
};

// 更新了 PeopleInfo 类
class PeopleInfo final : public ::PROTOBUF_NAMESPACE_ID::Message {
	public:
 		using ::PROTOBUF_NAMESPACE_ID::Message::CopyFrom;

 		void CopyFrom(const PeopleInfo& from);
 		using ::PROTOBUF_NAMESPACE_ID::Message::MergeFrom;
 		void MergeFrom( const PeopleInfo& from) {
 			PeopleInfo::MergeImpl(*this, from);
 		}	
 		
 		static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() {
 			return "PeopleInfo";
 		}
 
 		typedef PeopleInfo_Phone Phone;
 		// repeated .PeopleInfo.Phone phone = 3;
 		int phone_size() const;
 		void clear_phone();
 		::PeopleInfo_Phone* mutable_phone(int index);
 		::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::PeopleInfo_Phone >*
 			mutable_phone();
 		const ::PeopleInfo_Phone& phone(int index) const;
 		::PeopleInfo_Phone* add_phone();
 		const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::PeopleInfo_Phone >&
 			phone() const;
};

// 新增了 Contacts 类
class Contacts final : public ::PROTOBUF_NAMESPACE_ID::Message {
public:
 	using ::PROTOBUF_NAMESPACE_ID::Message::CopyFrom;
 	void CopyFrom(const Contacts& from);
 	using ::PROTOBUF_NAMESPACE_ID::Message::MergeFrom;
 	void MergeFrom( const Contacts& from) {
 		Contacts::MergeImpl(*this, from);
 	}
 	static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() {
 		return "Contacts";
 	}
 	
 	// repeated .PeopleInfo contacts = 1;
 	int contacts_size() const;
 	void clear_contacts();
 	::PeopleInfo* mutable_contacts(int index);
 	::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::PeopleInfo >*
 		mutable_contacts();
 	const ::PeopleInfo& contacts(int index) const;
 	::PeopleInfo* add_contacts();
 	const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::PeopleInfo >&
 		contacts() const;
};
  • 上述的例子中:
    • 每个字段都有一个 clear_ 方法,可以将字段重新设置回 empty 状态。
    • 每个字段都有设置和获取的方法,获取方法的方法名称与小写字段名称完全相同。
    • 但如果是消息类型的字段,其设置方法为 mutable_ 方法,返回值为消息类型的指针,这类方法会为我们开辟好空间,可以直接对这块空间的内容进行修改。
    • 对于使用 repeated 修饰的字段,也就是数组类型,pb 为我们提供了 add_ 方法来新增一个值,并且提供了 _size 方法来判断数组存放元素的个数。

3.2.2 通讯录 2.0 的写入实现


  • write.cc (通讯录 2.0):
cpp 复制代码
#include <iostream>
#include <fstream>
#include "contacts.pb.h"

using namespace std;

void AddPeopleInfo(contacts::PeopleInfo* people){
    cout << "------------新增联系人--------------" << endl;
    cout << "请输入联系人姓名:";
    string name;
    getline(cin, name);
    people->set_name(name);
    
    cout << "请输入联系人年龄:";
    int age;
    cin >> age;
    people->set_age(age);
    cin.ignore(256, '\n');   // 清除输入缓冲区中的内容,当遇到\n停止清除(会把\n清除掉),不超过256个字节

    for(int i = 0; ; i++){
        cout << "请输入联系人电话" << i + 1 << "(只输入回车完成电话新增):";
        string number;
        getline(cin, number);
        if (number.empty()){
            break;
        }
        contacts::PeopleInfo_Phone* phone = people->add_phone();
        phone->set_number(number);
    }
    cout << "------------新增联系人成功--------------" << endl;
}

int main()
{   
/* GOOGLE_PROTOBUF_VERIFY_VERSION 宏: 验证没有意外链接到与编译的头文件不兼容的库版
本。如果检测到版本不匹配,程序将中止。注意,每个 .pb.cc 文件在启动时都会自动用此宏。在使
用 C++ Protocol Buffer 库之前执行此宏是一种很好的做法,但不是绝对必要的。 */
    GOOGLE_PROTOBUF_VERIFY_VERSION;

    contacts::Contacts contacts;
    // 读取本地已经存在的联系人文件
    fstream input("contacts.bin", ios::in | ios::binary);
    if (!input){
        cout << "contacts.bin not find, create new file!" << endl;
    } else if (!contacts.ParseFromIstream(&input)) {
        cerr << "parse error!" << endl;
        input.close();
        return -1;
    }
    // 向通讯录中添加一个联系人
    AddPeopleInfo(contacts.add_contacts());

    // 将通讯录写入本地文件中
    fstream output("contacts.bin", ios::out | ios::trunc | ios::binary);
    if (!contacts.SerializeToOstream(&output)){
        cerr << "write error!" << endl;
        input.close();
        output.close();
        return -1;
    }
    cout << "write success!" << endl;
    input.close();
    output.close();

/* 在程序结束时调用 ShutdownProtobufLibrary(),为了删除 Protocol Buffer 库分配的所
有全局对象。对于大多数程序来说这是不必要的,因为该过程无论如何都要退出,并且操作系统将负责
回收其所有内存。但是,如果你使用了内存泄漏检查程序,该程序需要释放每个最后对象,或者你正在
编写可以由单个进程多次加载和卸载的库,那么你可能希望强制使用 Protocol Buffers 来清理所有
内容。 */
    google::protobuf::ShutdownProtobufLibrary();

    return 0;
}

后续整个版本迭代中,main 函数不再改变了。

  • makefile
bash 复制代码
write:write.cc contacts.pb.cc
	g++ -o $@ $^ -std=c++11 -lprotobuf

.PHONY:clean
clean:
	rm -f write
  • make之后,运行 write
0 复制代码
(base) ubuntu@Ubuntu22:~/Learn/protubuf-learn/proto3$ make
g++ -o write write.cc contacts.pb.cc -std=c++11 -lprotobuf
(base) ubuntu@Ubuntu22:~/Learn/protubuf-learn/proto3$ ./write 
contacts.bin not find, create new file!
------------新增联系人--------------
请输入联系人姓名:张三 
请输入联系人年龄:20
请输入联系人电话1(只输入回车完成电话新增):1511111
请输入联系人电话2(只输入回车完成电话新增):1311111
请输入联系人电话3(只输入回车完成电话新增):
------------新增联系人成功--------------
write success!
  • 查看二进制文件:
0 复制代码
(base) ubuntu@Ubuntu22:~/Learn/protubuf-learn/proto3$ hexdump -C contacts.bin
00000000  0a 20 0a 06 e5 bc a0 e4  b8 89 10 14 1a 09 0a 07  |. ..............|
00000010  31 35 31 31 31 31 31 1a  09 0a 07 31 33 31 31 31  |1511111....13111|
00000020  31 31                                             |11|
00000022

hexdump:是Linux下的一个二进制文件查看工具,它可以将二进制文件转换为ASCII、八进制、十进制、十六进制格式进行查看。-C:表示每个字节显示为16进制和相应的ASCII字符。


3.2.3 通讯录 2.0 的读取实现


1. 这一步其实是为了验证contacts.bin中的内容是否写入成功

  • read.cc (通讯录 2.0):
cpp 复制代码
#include <iostream>
#include <fstream>
#include "contacts.pb.h"

using namespace std;

void PrintContacts(contacts::Contacts& contacts){
    for (int i = 0; i < contacts.contacts_size(); i++){
        cout << "---------------联系人 " << i + 1 << "-------------------" << endl;
        const contacts::PeopleInfo& people =  contacts.contacts(i);   // 获取第i个下标的元素
        cout << "联系人姓名:" << people.name() << endl;
        cout << "联系人年龄:" << people.age() << endl;
        for (int j = 0; j < people.phone_size(); j++){
            const contacts::PeopleInfo_Phone& phone = people.phone(j);
            cout << "联系人电话" << j + 1 << ":" << phone.number() << endl;
        }
    }
}

int main()
{
    contacts::Contacts contacts;
    // 1. 读取本地已存在的通讯录文件
    fstream input("contacts.bin", ios::in | ios::binary);
    if (!contacts.ParseFromIstream(&input)){
        cerr << "parse error!" << endl;
        input.close();
        return -1;
    }
    // 2. 打印已经存在的通讯录列表
    PrintContacts(contacts);
    return 0;
}

这里也是,整个版本迭代中,main函数不再改变。

  • makefile
shell 复制代码
all:write read

write:write.cc contacts.pb.cc
	g++ -o $@ $^ -std=c++11 -lprotobuf

read:read.cc contacts.pb.cc
	g++ -o $@ $^ -std=c++11 -lprotobuf

.PHONY:clean
clean:
	rm -f write read
  • make后运行read
0 复制代码
(base) ubuntu@Ubuntu22:~/Learn/protubuf-learn/proto3$ make clean
rm -f write read
(base) ubuntu@Ubuntu22:~/Learn/protubuf-learn/proto3$ make
g++ -o write write.cc contacts.pb.cc -std=c++11 -lprotobuf
g++ -o read read.cc contacts.pb.cc -std=c++11 -lprotobuf
(base) ubuntu@Ubuntu22:~/Learn/protubuf-learn/proto3$ ./read 
---------------联系人 1-------------------
联系人姓名:张三
联系人年龄:20
联系人电话1:1511111
联系人电话2:1311111

2. 第二种验证方式

  • 我们可以用 protoc -h 命令来查看 ProtoBuf 为我们提供的所有命令 option。其中 ProtoBuf 提供一个命令选项 --decode ,表示从标准输入中读取给定类型的二进制消息,并将其以文本格式写入标准输出。 消息类型必须在 .proto 文件或导入的文件中定义。
0 复制代码
(base) ubuntu@Ubuntu22:~/Learn/protubuf-learn/proto3$ protoc --decode=contacts.Contacts contacts.proto < contacts.bin 
contacts {
  name: "\345\274\240\344\270\211"
  age: 20
  phone {
    number: "1511111"
  }
  phone {
    number: "1311111"
  }
}
  • 解释一下上述命令:
    • --decode=contacts.Contacts contacts.proto 指明要解码的消息类型,该类型是 contacts.proto 文件中的 contacts.Contacts,其中 contactspackage
    • 要解码的内容通过输入重定向 < ,重定向到 contacts.bin 文件。

3.3 enum 枚举类型(通讯录2.1)


1. 定义规则

  • 语法支持我们定义枚举类型并使用。在 .proto 文件中枚举类型的书写规范为:

    • 枚举类型名称:
      • 使用驼峰命名法,首字母大写。 例如:MyEnum
    • 常量值名称:
      • 全大写字母,多个字母之间用 _ 连接。例如: ENUM_CONST = 0
  • 我们可以定义一个名为 PhoneType 的枚举类型,定义如下:

java 复制代码
enum PhoneType {
 MP = 0; // 移动电话
 TEL = 1; // 固定电话
}
  • 要注意枚举类型的定义有以下几种规则:
    • 0 值常量必须存在,且要作为第一个元素。这是为了与 proto2 的语义兼容:第一个元素作为默认值,且值为 0。
    • 枚举类型可以在消息外定义,也可以在消息体内定义(嵌套)。
    • 枚举的常量值在 32 位整数的范围内。但因负值无效因而不建议使用(与编码规则有关)。

2. 定义时注意

  • 将两个具有相同枚举值名称的枚举类型放在单个 .proto 文件下测试时,编译后会报错:某某某常量已经被定义!所以这里要注意:
    • 同级(同层)的枚举类型,各个枚举类型中的常量不能重名。
    • 单个 .proto 文件下,最外层枚举类型和嵌套枚举类型,不算同级。
    • 多个 .proto 文件下,若一个文件引入了其他文件,且每个文件都未声明 package,每个 proto 文件中的枚举类型都在最外层,算同级。
    • 多个 .proto 文件下,若一个文件引入了其他文件,且每个文件都声明了 package,不算同级。
java 复制代码
// ---------------------- 情况1:同级枚举类型包含相同枚举值名称-------------------
enum PhoneType {
 	MP = 0; // 移动电话
 	TEL = 1; // 固定电话
}

enum PhoneTypeCopy {
 	MP = 0; // 移动电话 // 编译后报错:MP 已经定义
}

// ---------------------- 情况2:不同级枚举类型包含相同枚举值名称--------------------
enum PhoneTypeCopy {
	MP = 0; // 移动电话 // 用法正确
}

message Phone {
 	string number = 1; // 电话号码
 	enum PhoneType {
 		MP = 0; // 移动电话
 		TEL = 1; // 固定电话
 	}
}

// ---------------------- 情况3:多文件下都未声明package--------------------
// phone1.proto
import "phone2.proto"

enum PhoneType {
 	MP = 0; // 移动电话 // 编译后报错:MP 已经定义
 	TEL = 1; // 固定电话
}

// phone2.proto
enum PhoneTypeCopy {
 	MP = 0; // 移动电话
}

// ---------------------- 情况4:多文件下都声明了package--------------------
// phone1.proto
import "phone2.proto"
package phone1;

enum PhoneType {
 	MP = 0; // 移动电话 // 用法正确
 	TEL = 1; // 固定电话
}

// phone2.proto
package phone2;
enum PhoneTypeCopy {
 	 MP = 0; // 移动电话
}

3.3.1 升级通讯录至 2.1 版本


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

java 复制代码
syntax = "proto3";
package contacts;

// 定义联系人message
message PeopleInfo{
    string name = 1;    // 姓名
    int32 age = 2;      // 年龄
    
    message Phone{
        string number = 1;  // 字段编号和外层不冲突
        enum PhonType {
            MP = 0;     // 移动电话
            TEL = 1;    // 固定电话
        }
        PhonType type = 2;
    }
    repeated Phone phone = 3;   // 电话数组
}

// 通讯录message
message Contacts{
    repeated PeopleInfo contacts = 1;
}
  • 编译:
0 复制代码
protoc --cpp_out=. contacts.proto

2. contacts.pb.h 更新的部分代码展示:

cpp 复制代码
// 新生成的 PeopleInfo_Phone_PhoneType 枚举类
enum PeopleInfo_Phone_PhoneType : int {
 	PeopleInfo_Phone_PhoneType_MP = 0,
 	PeopleInfo_Phone_PhoneType_TEL = 1,

	PeopleInfo_Phone_PhoneType_PeopleInfo_Phone_PhoneType_INT_MIN_SENTINEL_DO_NOT_USE_ 
	= std::numeric_limits<int32_t>::min(),

	PeopleInfo_Phone_PhoneType_PeopleInfo_Phone_PhoneType_INT_MAX_SENTINEL_DO_NOT_USE_ 
	= std::numeric_limits<int32_t>::max()
};

// 更新的 PeopleInfo_Phone 类
class PeopleInfo_Phone final : public ::PROTOBUF_NAMESPACE_ID::Message {
public:
 	typedef PeopleInfo_Phone_PhoneType PhoneType;
 	static inline bool PhoneType_IsValid(int value) {
 	return PeopleInfo_Phone_PhoneType_IsValid(value);
}

template<typename T>
static inline const std::string& PhoneType_Name(T enum_t_value) {...}
static inline bool PhoneType_Parse(
 	::PROTOBUF_NAMESPACE_ID::ConstStringParam name, PhoneType* value) {...}
 	// .contacts.PeopleInfo.Phone.PhoneType type = 2;
 	void clear_type();
 	::contacts::PeopleInfo_Phone_PhoneType type() const;
 	void set_type(::contacts::PeopleInfo_Phone_PhoneType value);
};
  • 上述的代码中:
    • 对于在 .proto 文件中定义的枚举类型,编译生成的代码中会含有与之对应的枚举类型、校验枚举值是否有效的方法 _IsValid、以及获取枚举值名称的方法 _Name
    • 对于使用了枚举类型的字段,包含设置和获取字段的方法,已经清空字段的方法 clear_

3. 更新 write.cc(通讯录 2.1):

cpp 复制代码
#include <iostream>
#include <fstream>
#include "contacts.pb.h"

using namespace std;

// 2.1 版本
void AddPeopleInfo(contacts::PeopleInfo* people){
	// 此处省略2.0版本重复内容
    ...

    for(int i = 0; ; i++){
        cout << "请输入联系人电话" << i + 1 << "(只输入回车完成电话新增):";
        string number;
        getline(cin, number);
        if (number.empty()){
            break;
        }
        contacts::PeopleInfo_Phone* phone = people->add_phone();
        phone->set_number(number);

        cout << "请输入该电话类型(1. 移动电话 2. 固定电话):";
        int type;
        cin >> type;
        cin.ignore(256, '\n');
        switch (type){
            case 1:
                phone->set_type(contacts::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_MP);
                break;
            case 2:
                phone->set_type(contacts::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_TEL);
                break;
            default:
                cout << "选择有误!" << endl;
                break;
        }
    }
    cout << "------------新增联系人成功--------------" << endl;
}

int main()
{
	// main函数在整个版本迭代中不再改变了
	...
}

4. 更新 read.cc(通讯录2.1):

cpp 复制代码
#include <iostream>
#include <fstream>
#include "contacts.pb.h"

using namespace std;

// 2.1 版本
void PrintContacts(contacts::Contacts& contacts){
    for (int i = 0; i < contacts.contacts_size(); i++){
        cout << "---------------联系人 " << i + 1 << "-------------------" << endl;
        const contacts::PeopleInfo& people =  contacts.contacts(i);   // 获取第i个下标的元素
        cout << "联系人姓名:" << people.name() << endl;
        cout << "联系人年龄:" << people.age() << endl;
        for (int j = 0; j < people.phone_size(); j++){
            const contacts::PeopleInfo_Phone& phone = people.phone(j);
            cout << "联系人电话" << j + 1 << ":" << phone.number();
            // 格式:联系人电话1: 1311111 (MP)
            cout << " (" << phone.PhoneType_Name(phone.type()) << ")" << endl;
        }
    }
}

int main()
{
	...
}

5. 代码完成编译后进行读写验证:

  • 联系人一电话 Type 打印出 MP 是因为未设置该字段,导致用了枚举的第一个元素(字段编号为0的字段)作为默认值。
0 复制代码
(base) ubuntu@Ubuntu22:~/Learn/protubuf-learn/proto3$ ./write 
------------新增联系人--------------
请输入联系人姓名:李四
请输入联系人年龄:30
请输入联系人电话1(只输入回车完成电话新增):0916231312
请输入该电话类型(1. 移动电话 2. 固定电话):2
请输入联系人电话2(只输入回车完成电话新增):
------------新增联系人成功--------------
(base) ubuntu@Ubuntu22:~/Learn/protubuf-learn/proto3$ ./read 
---------------联系人 1-------------------
联系人姓名:张三
联系人年龄:20
联系人电话1:1511111 (MP)
联系人电话2:1311111 (MP)
---------------联系人 2-------------------
联系人姓名:李四
联系人年龄:30
联系人电话1:0916231312 (TEL)

3.4 Any 类型(通讯录2.2)


字段还可以声明为 Any 类型,可以理解为泛型类型。使用时可以在 Any 中存储任意消息类型。Any 类型的字段也用 repeated 来修饰。

Any 类型是 google 已经帮我们定义好的类型,在安装 ProtoBuf 时,其中的 include 目录下查找所有 google 已经定义好的 .proto 文件。


3.4.1 升级通讯录至 2.2 版本

通讯录 2.2 版本会新增联系人的地址信息,我们可以使用 Any 类型的字段来存储地址信息。


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

java 复制代码
syntax = "proto3";
package contacts;

// 2.2 版本
import "google/protobuf/any.proto";

message Address{
    string home_address = 1;    // 家庭住址
    string unit_address = 2;    // 单位地址
}

message PeopleInfo{
    string name = 1;    // 姓名
    int32 age = 2;      // 年龄
    // repeated string phone_numbers = 3;  // 电话数组
    message Phone{
        string number = 1;  // 字段编号和外层不冲突
        enum PhoneType {
            MP = 0;     // 移动电话
            TEL = 1;    // 固定电话
        }
        PhoneType type = 2;
    }
    repeated Phone phone = 3;   // 电话数组
    google.protobuf.Any data = 4;   // 任意类型,存储地址
}

// 通讯录 message
message Contacts{
    repeated PeopleInfo contacts = 1;
}
  • 编译:
0 复制代码
protoc --cpp_out=. contacts.proto

2. contacts.pb.h 更新的部分代码展示:

cpp 复制代码
// 新生成的 Address 类
class Address final : public ::PROTOBUF_NAMESPACE_ID::Message {
public:
 	using ::PROTOBUF_NAMESPACE_ID::Message::CopyFrom;
 	void CopyFrom(const Address& from);
 	using ::PROTOBUF_NAMESPACE_ID::Message::MergeFrom;
 	void MergeFrom( const Address& from) {
 		Address::MergeImpl(*this, from);
 	}
 	
 	// string home_address = 1;
 	void clear_home_address();
 	const std::string& home_address() const;
 	template <typename ArgT0 = const std::string&, typename... ArgT>
 	void set_home_address(ArgT0&& arg0, ArgT... args);
 	std::string* mutable_home_address();
 	PROTOBUF_NODISCARD std::string* release_home_address();
 	void set_allocated_home_address(std::string* home_address);
 
 	// string unit_address = 2;
 	void clear_unit_address();
 	const std::string& unit_address() const;
 	template <typename ArgT0 = const std::string&, typename... ArgT>
 	void set_unit_address(ArgT0&& arg0, ArgT... args);
 	std::string* mutable_unit_address();
 	PROTOBUF_NODISCARD std::string* release_unit_address();
 	void set_allocated_unit_address(std::string* unit_address);
};

// 更新的 PeopleInfo 类
class PeopleInfo final : public ::PROTOBUF_NAMESPACE_ID::Message {
public:
 	// .google.protobuf.Any data = 4;
 	bool has_data() const;
 	void clear_data();
 	const ::PROTOBUF_NAMESPACE_ID::Any& data() const;
 	PROTOBUF_NODISCARD ::PROTOBUF_NAMESPACE_ID::Any* release_data();
 	::PROTOBUF_NAMESPACE_ID::Any* mutable_data();
 	void set_allocated_data(::PROTOBUF_NAMESPACE_ID::Any* data);
};
  • 上述的代码中,对于 Any 类型字段:

    • 设置和获取:获取方法的方法名称与小写字段名称完全相同。设置方法可以使用 mutable_ 方法,返回值为 Any 类型的指针,这类方法会为我们开辟好空间,可以直接对这块空间的内容进行修改。
  • 之前讲过,我们可以在 Any 字段中存储任意消息类型,这就要涉及到任意消息类型和 Any 类型的互转。这部分代码就在 Google为我们写好的头文件 any.pb.h 中。对 any.pb.h 部分代码展示:

cpp 复制代码
class PROTOBUF_EXPORT Any final : public ::PROTOBUF_NAMESPACE_ID::Message {
 	bool PackFrom(const ::PROTOBUF_NAMESPACE_ID::Message& message) {
 		...
 	}
 	bool UnpackTo(::PROTOBUF_NAMESPACE_ID::Message* message) const {
 		...
 	}
 	
 	template<typename T> bool Is() const {
 		return _impl_._any_metadata_.Is<T>();
 	}
};
  • 解释:
    • 使用 PackFrom() 方法可以将任意消息类型转为 Any 类型。
    • 使用 UnpackTo() 方法可以将 Any 类型转回之前设置的任意消息类型。
    • 使用 Is() 方法可以用来判断存放的消息类型是否为 typename T

3. 更新 write.cc (通讯录 2.2)

cpp 复制代码
#include <iostream>
#include <fstream>
#include "contacts.pb.h"

using namespace std;

void AddPeopleInfo(contacts::PeopleInfo* people){
    // 2.1 版本重复内容
    ...

    contacts::Address address;
    cout << "请输入联系人家庭地址:";
    string home_address;
    getline(cin, home_address);
    address.set_home_address(home_address);
    cout << "请输入联系人单位地址:";
    string uint_address;
    getline(cin, uint_address);
    address.set_unit_address(uint_address);
    // Address对象->Any对象
    people->mutable_data()->PackFrom(address);

    cout << "------------新增联系人成功--------------" << endl;
}

int main()
{
	...
}

4. 更新 read.cc (通讯录 2.2)

cpp 复制代码
#include <iostream>
#include <fstream>
#include "contacts.pb.h"

using namespace std;

void PrintContacts(contacts::Contacts& contacts){
    for (int i = 0; i < contacts.contacts_size(); i++){
        // 2.1 版本重复内容
        ...
        
        // 既给data设置过数据,又给data设置的是Address类型
        if (people.has_data() && people.data().Is<contacts::Address>()){
            contacts::Address address;
            people.data().UnpackTo(&address);
            if (!address.home_address().empty()){
                cout << "联系人家庭地址:" << address.home_address() << endl;
            }
            if (!address.unit_address().empty()){
                cout << "联系人单位地址:" << address.unit_address() << endl;
            }
        }
    }
}

int main()
{
	...
}

5. 代码编写完成后,编译后进行读写:

0 复制代码
(base) ubuntu@Ubuntu22:~/Learn/protubuf-learn/proto3$ ./write 
------------新增联系人--------------
请输入联系人姓名:赵六
请输入联系人年龄:33
请输入联系人电话1(只输入回车完成电话新增):3271283682
请输入该电话类型(1. 移动电话 2. 固定电话):1
请输入联系人电话2(只输入回车完成电话新增):
请输入联系人家庭地址:上海
请输入联系人单位地址:上海
------------新增联系人成功--------------
write success!
(base) ubuntu@Ubuntu22:~/Learn/protubuf-learn/proto3$ ./read 
---------------联系人 1-------------------
联系人姓名:张三
联系人年龄:20
联系人电话1:1511111 (MP)
联系人电话2:1311111 (MP)
---------------联系人 2-------------------
联系人姓名:李四
联系人年龄:30
联系人电话1:0916231312 (TEL)
---------------联系人 3-------------------
联系人姓名:赵六
联系人年龄:33
联系人电话1:3271283682 (MP)
联系人家庭地址:上海
联系人单位地址:上海

3.5 oneof 类型(通讯录2.3)

如果消息中有很多可选字段,并且将来同时只有一个字段会被设置,那么就可以使用 oneof 加强这个行为,也能有节约内存的效果。


3.5.1 升级通讯录至 2.3 版本

  • 通讯录 2.3 版本想新增联系人的其他联系方式,比如qq或者微信号二选一,我们就可以使用 oneof 字段来加强多选一这个行为。oneof 字段定义的格式为:oneof 字段名 { 字段1; 字段2; ... }

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

java 复制代码
syntax = "proto3";
package contacts;

// 2.3 版本
import "google/protobuf/any.proto";

message Address{
    string home_address = 1;    // 家庭住址
    string unit_address = 2;    // 单位地址
}

message PeopleInfo{
    string name = 1;    // 姓名
    int32 age = 2;      // 年龄
    // repeated string phone_numbers = 3;  // 电话数组
    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 {
        // repeated string qq = 5;     // (报错)不能使用 repeated
        string qq = 5;
        string wechat = 6;
    }
}   

// 通讯录 message
message Contacts{
    repeated PeopleInfo contacts = 1;
}
  • 注意:

    • 可选字段中的字段编号,不能与非可选字段的编号冲突;
    • 不能在 oneof 中使用 repeated 字段;
    • 将来在设置 oneof 字段中值时,如果将 oneof 中的字段设置多个,那么只会保留最后一次设置的成员,之前设置的 oneof 成员会自动清除。
  • 编译:

0 复制代码
protoc --cpp_out=. contacts.proto

2. contacts.pb.h 更新的部分代码展示:

cpp 复制代码
// 更新的 PeopleInfo 类
class PeopleInfo final : public ::PROTOBUF_NAMESPACE_ID::Message {
 	enum OtherContactCase {
 		kQq = 5,
 		kWeixin = 6,
 		OTHER_CONTACT_NOT_SET = 0,
	};
 	
 	// string qq = 5;
 	bool has_qq() const;
 	void clear_qq();
 	const std::string& qq() const;
 	template <typename ArgT0 = const std::string&, typename... ArgT>
 	void set_qq(ArgT0&& arg0, ArgT... args);
 	std::string* mutable_qq();
 	PROTOBUF_NODISCARD std::string* release_qq();
 	void set_allocated_qq(std::string* qq);

 	// string weixin = 6;
 	bool has_weixin() const;
 	void clear_weixin();
 	const std::string& weixin() const;
 	template <typename ArgT0 = const std::string&, typename... ArgT>
 	void set_weixin(ArgT0&& arg0, ArgT... args);
 	std::string* mutable_weixin();
 	PROTOBUF_NODISCARD std::string* release_weixin();
 	void set_allocated_weixin(std::string* weixin);
 	void clear_other_contact();
 	OtherContactCase other_contact_case() const;
};
  • 上述的代码中,对于 oneof 字段:
    • 会将 oneof 中的多个字段定义为一个枚举类型;
    • 设置和获取:对 oneof 内的字段进行常规的设置和获取即可,但要注意只能设置一个。如果设置多个,那么只会保留最后一次设置的成员;
    • 清空 oneof 字段:clear_ 方法;
    • 获取当前设置了哪个字段:_case 方法

3. 更新 write.cc (通讯录 2.3),更新内容如下:

cpp 复制代码
#include <iostream>
#include <fstream>
#include "contacts.pb.h"

using namespace std;

void AddPeopleInfo(contacts::PeopleInfo* people){
    // 2.2 版本重复内容
    ...

    cout << "请选择要添加的其他联系方式(1. qq 2. wechat):";
    int other_contact;
    cin >> other_contact;
    cin.ignore(256, '\n');
    if (other_contact == 1) {
        cout << "请输入联系人qq号:";
        string qq;
        getline(cin, qq);
        people->set_qq(qq);
    } else if (other_contact == 2){
        cout << "请输入联系人微信号:";
        string wechat;
        getline(cin, wechat);
        people->set_wechat(wechat);
    } else {
        cout << "选择有误,未成功设置其他联系方式!" << endl;
    }

    cout << "------------新增联系人成功--------------" << endl;
}

int main()
{
	...
}

4. 更新 read.cc (通讯录 2.3),更新内容如下:

cpp 复制代码
#include <iostream>
#include <fstream>
#include "contacts.pb.h"

using namespace std;

// 2.3 版本
void PrintContacts(contacts::Contacts& contacts){
    for (int i = 0; i < contacts.contacts_size(); i++){
        // 2.2 版本重复内容
        ...

        // 打印联系人的其他联系方式
        switch (people.other_contact_case()){
            case contacts::PeopleInfo::OtherContactCase::kQq:
                cout << "联系人qq:" << people.qq() << endl;
                break;
            case contacts::PeopleInfo::OtherContactCase::kWechat:
                cout << "联系人微信:" << people.wechat() << endl;
                break;
            default:
                break;
        }
    }
}

int main()
{
	...
}

5. 代码编写完成后,编译后进行读写:

0 复制代码
(base) ubuntu@Ubuntu22:~/Learn/protubuf-learn/proto3$ ./write 
------------新增联系人--------------
请输入联系人姓名:王五
请输入联系人年龄:18
请输入联系人电话1(只输入回车完成电话新增):1221321112
请输入该电话类型(1. 移动电话 2. 固定电话):2
请输入联系人电话2(只输入回车完成电话新增):
请输入联系人家庭地址:北京
请输入联系人单位地址:北京  
请选择要添加的其他联系方式(1. qq 2. wechat):2
请输入联系人微信号:lyh232122
------------新增联系人成功--------------
write success!
(base) ubuntu@Ubuntu22:~/Learn/protubuf-learn/proto3$ ./read 
---------------联系人 1-------------------
联系人姓名:张三
联系人年龄:20
联系人电话1:1511111 (MP)
联系人电话2:1311111 (MP)
---------------联系人 2-------------------
联系人姓名:李四
联系人年龄:30
联系人电话1:0916231312 (TEL)
---------------联系人 3-------------------
联系人姓名:赵六
联系人年龄:33
联系人电话1:3271283682 (MP)
联系人家庭地址:上海
联系人单位地址:上海
---------------联系人 4-------------------
联系人姓名:王五
联系人年龄:18
联系人电话1:1221321112 (TEL)
联系人家庭地址:北京
联系人单位地址:北京
联系人微信:lyh232122

3.6 map 类型(通讯录2.4)

  • 语法支持创建一个关联映射字段,也就是可以使用 map 类型去声明字段类型,格式为:map<key_type, value_type> map_field = N;
  • 要注意的是:
    • key_type 是除了 floatbytes 类型以外的任意标量类型。value_type 可以是任意类型;
    • map 字段不可以用 repeated 修饰;
    • map 中存入的元素是无序的。

3.6.1 升级通讯录至 2.4 版本

最后,通讯录 2.4 版本想新增联系人的备注信息,我们可以使用 map 类型的字段来存储备注信息。


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

java 复制代码
syntax = "proto3";
package contacts;

// 2.4 版本
import "google/protobuf/any.proto";

message Address{
    string home_address = 1;    // 家庭住址
    string unit_address = 2;    // 单位地址
}

message PeopleInfo{
    string name = 1;    // 姓名
    int32 age = 2;      // 年龄
    // repeated string phone_numbers = 3;  // 电话数组
    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 {
        // repeated string qq = 5;     // (报错)不能使用 repeated
        string qq = 5;
        string wechat = 6;
    }

    map<string, string> remark = 7;     // 备注信息
}   

// 通讯录 message
message Contacts{
    repeated PeopleInfo contacts = 1;
}
  • 编译:
0 复制代码
protoc --cpp_out=. contacts.proto

2. contacts.pb.h 更新的部分代码展示:

cpp 复制代码
// 更新的 PeopleInfo 类
class PeopleInfo final : public ::PROTOBUF_NAMESPACE_ID::Message {
 	// map<string, string> remark = 7;
 	int remark_size() const;
 	void clear_remark();
 	const ::PROTOBUF_NAMESPACE_ID::Map< std::string, std::string >&
 		remark() const;
 	::PROTOBUF_NAMESPACE_ID::Map< std::string, std::string >*
 		mutable_remark();
};
  • 上述的代码中,对于 Map 类型的字段:
    • 清空 mapclear_ 方法;
    • 设置和获取:获取方法的方法名称与小写字段名称完全相同。设置方法为 mutable_ 方法,返回值为 Map 类型的指针,这类方法会为我们开辟好空间,可以直接对这块空间的内容进行修改。

3. 更新 write.cc (通讯录 2.4),更新内容如下:

cpp 复制代码
#include <iostream>
#include <fstream>
#include "contacts.pb.h"

using namespace std;

// 2.4 版本
void AddPeopleInfo(contacts::PeopleInfo* people){
    // 2.3 版本重复内容
    ...

    for (int i = 0; ; i++){
        cout << "请输入备注" << i + 1 << "标题(只输入回车完成新增):";
        string remark_key;
        getline(cin, remark_key);
        if (remark_key.empty()) {
            break;
        }

        cout << "请输入备注" << i + 1 << "内容:";
        string remark_value;
        getline(cin, remark_value);
        people->mutable_remark()->insert({remark_key, remark_value});
    }

    cout << "------------新增联系人成功--------------" << endl;
}

int main()
{
	...
}

4. 更新 read.cc (通讯录 2.4),更新内容如下:

cpp 复制代码
#include <iostream>
#include <fstream>
#include "contacts.pb.h"

using namespace std;

// 2.4 版本
void PrintContacts(contacts::Contacts& contacts){
    for (int i = 0; i < contacts.contacts_size(); i++){
        // 2.3 版本重复内容
        ...
        
        if (people.remark_size()) {
            cout << "备注信息:" << endl;
        }
        for (auto it = people.remark().cbegin(); it != people.remark().cend(); it++){
            cout << "   " << it->first << ": " << it->second << endl;
        }
    }
}

int main()
{
	...
}

5. 代码编写完成后,编译后进行读写:

0 复制代码
(base) ubuntu@Ubuntu22:~/Learn/protubuf-learn/proto3$ ./write 
------------新增联系人--------------
请输入联系人姓名:田七
请输入联系人年龄:40
请输入联系人电话1(只输入回车完成电话新增):23132113
请输入该电话类型(1. 移动电话 2. 固定电话):2
请输入联系人电话2(只输入回车完成电话新增):
请输入联系人家庭地址:海南  
请输入联系人单位地址:海南
请选择要添加的其他联系方式(1. qq 2. wechat):2
请输入联系人微信号:tq111189637
请输入备注1标题(只输入回车完成新增):日程
请输入备注1内容:10月1号出去旅游
请输入备注2标题(只输入回车完成新增):
------------新增联系人成功--------------
write success!
(base) ubuntu@Ubuntu22:~/Learn/protubuf-learn/proto3$ ./read 
---------------联系人 1-------------------
联系人姓名:张三
联系人年龄:20
联系人电话1:1511111 (MP)
联系人电话2:1311111 (MP)
---------------联系人 2-------------------
联系人姓名:李四
联系人年龄:30
联系人电话1:0916231312 (TEL)
---------------联系人 3-------------------
联系人姓名:赵六
联系人年龄:33
联系人电话1:3271283682 (MP)
联系人家庭地址:上海
联系人单位地址:上海
---------------联系人 4-------------------
联系人姓名:王五
联系人年龄:18
联系人电话1:1221321112 (TEL)
联系人家庭地址:北京
联系人单位地址:北京
联系人微信:lyh232122
---------------联系人 5-------------------
联系人姓名:田七
联系人年龄:40
联系人电话1:23132113 (TEL)
联系人家庭地址:南
联系人单位地址:南
联系人微信:tq111189637
备注信息:
   日程: 10月1号出去旅游
  • 到此,我们对通讯录 2.x 要求的任务全部完成。在这个过程中我们将通讯录升级到了 2.4 版本,同时对ProtoBuf 的使用也进一步熟练了,并且也掌握了 ProtoBuf 的 proto3 语法支持的大部分类型及其使用,但只是正常使用还是完全不够的。通过接下来的学习,我们就能更进一步了解到 ProtoBuf 深入的内容。

3.7 默认值


反序列化消息时,如果被反序列化的二进制序列中不包含某个字段,反序列化对象中相应字段时,就会设置为该字段的默认值。不同的类型对应的默认值不同:

  • 对于字符串,默认值为空字符串;
  • 对于字节,默认值为空字节;
  • 对于布尔值,默认值为 false
  • 对于数值类型,默认值为 0;
  • 对于枚举,默认值是第一个定义的枚举值, 必须为 0;
  • 对于消息字段,未设置该字段。它的取值是依赖于语言;
  • 对于设置了 repeated 的字段的默认值是空的(通常是相应语言的一个空列表);
  • 对于消息字段 、oneof字段 和 any字段,C++ 和 Java 语言中都有 has_ 方法来检测当前字段是否被设置。

对于标量数据类型,在proto3语法下,没有生成has_方法。


3.8 更新信息


3.8.1 更新规则


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

  • 禁止修改任何已有字段的字段编号。
  • 若是移除老字段,要保证不再使用移除字段的字段编号。正确的做法是保留字段编号(reserved),以确保该编号将不能被重复使用。不建议直接删除或注释掉字段。
  • int32uint32int64uint64bool 是完全兼容的。可以从这些类型中的一个改为另一个,而不破坏前后兼容性。若解析出来的数值与相应的类型不匹配,会采用与 C++ 一致的处理方案(例如,若将 64 位整数当做 32 位进行读取,它将被截断为 32 位)。
  • sint32sint64 相互兼容但不与其他的整型兼容。
  • stringbytes 在合法 UTF-8 字节前提下也是兼容的。
  • bytes 包含消息编码版本的情况下,嵌套消息与 bytes 也是兼容的。
  • fixed32sfixed32 兼容,fixed64sfixed64兼容。
  • enumint32uint32int64uint64 兼容(注意若值不匹配会被截断)。但要注意当反序列化消息时会根据语言采用不同的处理方案:例如,未识别的 proto3 枚举类型会被保存在消息中,但是当消息反序列化时如何表示是依赖于编程语言的。整型字段总是会保持其的值。
  • oneof
    • 将一个单独的值更改为新 oneof 类型成员之一是安全和二进制兼容的。
    • 若确定没有代码一次性设置多个值那么将多个字段移入一个新 oneof 类型也是可行的。
    • 将任何字段移入已存在的 oneof 类型是不安全的。

3.8.2 保留字段 reserved(通讯录3.0)

  • 如果通过 删除注释掉 字段来更新消息类型,未来的用户在添加新字段时,有可能会使用以前已经存在,但已经被删除或注释掉的字段编号。将来使用该 .proto 的旧版本时的程序会引发很多问题:数据损坏、隐私错误等等。

  • 确保不会发生这种情况的一种方法是:使用 reserved 将指定字段的编号或名称设置为保留项 。当我们再使用这些编号或名称时,protocol buffer 的编译器将会警告这些编号或名称不可用。举个例子:

java 复制代码
ssage Message {
 	// 设置保留项
 	reserved 100, 101, 200 to 299;
 	reserved "field3", "field4";
 	// 注意:不要在一行 reserved 声明中同时声明字段编号和名称。
 	// reserved 102, "field5";

 	// 设置保留项之后,下面代码会告警
 	int32 field1 = 100; //告警:Field 'field1' uses reserved number 100
 	int32 field2 = 101; //告警:Field 'field2' uses reserved number 101
 	int32 field3 = 102; //告警:Field name 'field3' is reserved
 	int32 field4 = 103; //告警:Field name 'field4' is reserved
}

3.8.2.1 创建通讯录 3.0 版本---验证 错误删除字段 造成的数据损坏
  • 现模拟有两个服务,他们各自使用一份通讯录 .proto 文件,内容约定好了是一模一样的。
    • 服务1(service):负责序列化通讯录对象,并写入文件中。
    • 服务2(client):负责读取文件中的数据,解析并打印出来。
  • 一段时间后,service 更新了自己的 .proto 文件,更新内容为:删除了某个字段,并新增了一个字段,新增的字段使用了被删除字段的字段编号。并将新的序列化对象写进了文件。
  • client 并没有更新自己的 .proto 文件。根据结论,可能会出现数据损坏的现象,接下来就让我们来验证下这个结论。
  • 新建两个目录:serviceclient。分别存放两个服务的代码。
  • service 目录下新增 contacts.proto (通讯录 3.0)

1. 目录结构:

2. service 目录下新增 contacts.proto (通讯录 3.0)

java 复制代码
syntax = "proto3";
package s_contacts;

// 联系人
message PeopleInfo {
    string name = 1; // 姓名
    int32 age = 2; // 年龄

    message Phone {
        string number = 1; // 电话号码
    }
    repeated Phone phone = 3; // 电话
}

// 通讯录
message Contacts {
    repeated PeopleInfo contacts = 1;
}

3. client 目录下新增 contacts.proto (通讯录 3.0)

java 复制代码
syntax = "proto3";
package c_contacts;

// 联系人
message PeopleInfo {
    string name = 1; // 姓名
    int32 age = 2; // 年龄

    message Phone {
        string number = 1; // 电话号码
    }
    repeated Phone phone = 3; // 电话
}

// 通讯录
message Contacts {
    repeated PeopleInfo contacts = 1;
}
  • 分别对两个文件进行编译,可自行操作。

4. 继续对 service 目录下新增 service.cc (通讯录 3.0),负责向文件中写通讯录消息,内容如下:

cpp 复制代码
#include <iostream>
#include <fstream>
#include "contacts.pb.h"

using namespace std;
using namespace s_contacts;
/**
 * 新增联系人
 */
void AddPeopleInfo(PeopleInfo *people_info_ptr)
{
    cout << "-------------新增联系人-------------" << endl;
    cout << "请输入联系人姓名: ";
    string name;
    getline(cin, name);
    people_info_ptr->set_name(name);

    cout << "请输入联系人年龄: ";
    int age;
    cin >> age;
    people_info_ptr->set_age(age);
    cin.ignore(256, '\n');
    for (int i = 1;; i++)
    {
        cout << "请输入联系人电话" << i << "(只输入回车完成电话新增): ";
        string number;
        getline(cin, number);
        if (number.empty())
        {
            break;
        }
        PeopleInfo_Phone *phone = people_info_ptr->add_phone();
        phone->set_number(number);
    }

    cout << "-----------添加联系人成功-----------" << endl;
}

int main(int argc, char *argv[])
{
    GOOGLE_PROTOBUF_VERIFY_VERSION;

    if (argc != 2)
    {
        cerr << "Usage: " << argv[0] << " CONTACTS_FILE" << endl;
        return -1;
    }
    Contacts contacts;

    // 先读取已存在的 contacts
    fstream input(argv[1], ios::in | ios::binary);
    if (!input)
    {
        cout << argv[1] << ": File not found. Creating a new file." << endl;
    }
    else if (!contacts.ParseFromIstream(&input))
    {
        cerr << "Failed to parse contacts." << endl;
        input.close();
        return -1;
    }

    // 新增一个联系人
    AddPeopleInfo(contacts.add_contacts());

    // 向磁盘文件写入新的 contacts
    fstream output(argv[1], ios::out | ios::trunc | ios::binary);
    if (!contacts.SerializeToOstream(&output))
    {
        cerr << "Failed to write contacts." << endl;
        input.close();
        output.close();
        return -1;
    }
    input.close();
    output.close();
    
    google::protobuf::ShutdownProtobufLibrary();
    return 0;
}
  • service 目录下新增 makefile
bash 复制代码
service:service.cc contacts.pb.cc
	g++ -o $@ $^ -std=c++11 -lprotobuf

.PHONY:clean
clean:
	rm -f service

5. client 目录下新增 client.cc (通讯录 3.0),负责向读出文件中的通讯录消息,内容如下:

cpp 复制代码
#include <iostream>
#include <fstream>
#include "contacts.pb.h"

using namespace std;
using namespace c_contacts;
/**
 * 打印联系人列表
 */
void PrintfContacts(const Contacts &contacts)
{
    for (int i = 0; i < contacts.contacts_size(); ++i)
    {
        const PeopleInfo &people = contacts.contacts(i);
        cout << "------------联系人" << i + 1 << "------------" << endl;
        cout << "姓名:" << people.name() << endl;
        cout << "年龄:" << people.age() << endl;
        int j = 1;
        for (const PeopleInfo_Phone &phone : people.phone())
        {
            cout << "电话" << j++ << ": " << phone.number() << endl;
        }
    }
}

int main(int argc, char *argv[])
{

    GOOGLE_PROTOBUF_VERIFY_VERSION;
    if (argc != 2)
    {
        cerr << "Usage: " << argv[0] << "CONTACTS_FILE" << endl;
        return -1;
    }
    // 以二进制方式读取 contacts
    Contacts contacts;
    fstream input(argv[1], ios::in | ios::binary);
    if (!contacts.ParseFromIstream(&input))
    {
        cerr << "Failed to parse contacts." << endl;
        input.close();
        return -1;
    }

    // 打印 contacts
    PrintfContacts(contacts);
    input.close();
    google::protobuf::ShutdownProtobufLibrary();
    return 0;
}
  • client 目录下新增 makefile
bash 复制代码
client:client.cc contacts.pb.cc
	g++ -o $@ $^ -std=c++11 -lprotobuf
	
.PHONY:clean
clean:
	rm -f client

6. 代码编写完成后,进行一次读写(读写前的编译过程省略,自行操作)

0 复制代码
(base) ubuntu@Ubuntu22:~/Learn/protubuf-learn/update/service$ ./service ../contacts.bin
../contacts.bin: File not found. Creating a new file.
-------------新增联系人-------------
请输入联系人姓名: 张三
请输入联系人年龄: 20
请输入联系人电话1(只输入回车完成电话新增): 23211111
请输入联系人电话2(只输入回车完成电话新增): 23233233
请输入联系人电话3(只输入回车完成电话新增): 
-----------添加联系人成功-----------
(base) ubuntu@Ubuntu22:~/Learn/protubuf-learn/update/client$ ./client ../contacts.bin
------------联系人1------------
姓名:张三
年龄:20
电话1: 23211111
电话2: 23233233

7. 确认无误后,对 service 目录下的 contacts.proto 文件进行更新:删除 age 字段,新增 birthday 字段,新增的字段使用被删除字段的字段编号。

  • 更新后的 contacts.proto(通讯录 3.0)内容如下:
java 复制代码
syntax = "proto3";
package s_contacts;

// 联系人
message PeopleInfo {
    string name = 1; // 姓名
    // int32 age = 2; // 年龄
    int32 birthday = 2; // 生日

    message Phone {
        string number = 1; // 电话号码
    }
    repeated Phone phone = 3; // 电话
}

// 通讯录
message Contacts {
    repeated PeopleInfo contacts = 1;
}
  • 编译文件 .proto 后,还需要更新一下对应的 service.cc(通讯录 3.0):
cpp 复制代码
#include <iostream>
#include <fstream>
#include "contacts.pb.h"

using namespace std;
using namespace s_contacts;
/**
 * 新增联系人
 */
void AddPeopleInfo(PeopleInfo *people_info_ptr)
{
    cout << "-------------新增联系人-------------" << endl;
    cout << "请输入联系人姓名: ";
    string name;
    getline(cin, name);
    people_info_ptr->set_name(name);

    /*cout << "请输入联系人年龄: ";
    int age;
    cin >> age;
    people_info_ptr->set_age(age);
    cin.ignore(256, '\n'); */

    cout << "请输入联系人生日: ";
    int birthday;
    cin >> birthday;
    people_info_ptr->set_birthday(birthday);
    cin.ignore(256, '\n');

    for (int i = 1;; i++)
    {
        cout << "请输入联系人电话" << i << "(只输入回车完成电话新增): ";
        string number;
        getline(cin, number);
        if (number.empty())
        {
            break;
        }
        PeopleInfo_Phone *phone = people_info_ptr->add_phone();
        phone->set_number(number);
    }

    cout << "-----------添加联系人成功-----------" << endl;
}

int main(int argc, char *argv[])
{
    GOOGLE_PROTOBUF_VERIFY_VERSION;

    if (argc != 2)
    {
        cerr << "Usage: " << argv[0] << " CONTACTS_FILE" << endl;
        return -1;
    }
    Contacts contacts;

    // 先读取已存在的 contacts
    fstream input(argv[1], ios::in | ios::binary);
    if (!input)
    {
        cout << argv[1] << ": File not found. Creating a new file." << endl;
    }
    else if (!contacts.ParseFromIstream(&input))
    {
        cerr << "Failed to parse contacts." << endl;
        input.close();
        return -1;
    }

    // 新增一个联系人
    AddPeopleInfo(contacts.add_contacts());

    // 向磁盘文件写入新的 contacts
    fstream output(argv[1], ios::out | ios::trunc | ios::binary);
    if (!contacts.SerializeToOstream(&output))
    {
        cerr << "Failed to write contacts." << endl;
        input.close();
        output.close();
        return -1;
    }
    input.close();
    output.close();

    google::protobuf::ShutdownProtobufLibrary();
    return 0;
}
  • 我们对 client 相关的代码保持原样,不进行更新。

8. 再进行一次读写(对 service.cc 编译过程省略,自行操作)

0 复制代码
(base) ubuntu@Ubuntu22:~/Learn/protubuf-learn/update/service$ ./service ../contacts.bin
-------------新增联系人-------------
请输入联系人姓名: 李四
请输入联系人生日: 1221
请输入联系人电话1(只输入回车完成电话新增): 897812  
请输入联系人电话2(只输入回车完成电话新增): 
-----------添加联系人成功-----------
(base) ubuntu@Ubuntu22:~/Learn/protubuf-learn/update/service$ cd ../client/
(base) ubuntu@Ubuntu22:~/Learn/protubuf-learn/update/client$ ./client ../contacts.bin
------------联系人1------------
姓名:张三
年龄:20
电话1: 23211111
电话2: 23233233
------------联系人2------------
姓名:李四
年龄:1221
电话1: 897812
  • 这时问题便出现了,我们发现输入的生日,在反序列化时,被设置到了使用了相同字段编号的年龄上!!所以得出结论:若是移除老字段,要保证不再使用移除字段的字段编号,不建议直接删除或注释掉字段

9. 那么正确的做法是保留字段编号(reserved),以确保该编号将不能被重复使用

  • 正确 service 目录下的 contacts.proto 写法如下(终版通讯录 3.0):
java 复制代码
syntax = "proto3";
package s_contacts;

// 联系人
message PeopleInfo {
    reserved 2;

    string name = 1; // 姓名
    // int32 age = 2; // 年龄
    int32 birthday = 4; // 生日

    message Phone {
        string number = 1; // 电话号码
    }
    repeated Phone phone = 3; // 电话
}

// 通讯录
message Contacts {
    repeated PeopleInfo contacts = 1;
}
  • 编译 .proto 文件后,还需要重新编译下 service.cc,让 service 程序保持使用新生成的 pb C++ 文件。
0 复制代码
(base) ubuntu@Ubuntu22:~/Learn/protubuf-learn/update/service$ make
g++ -o service service.cc contacts.pb.cc -std=c++11 -lprotobuf
(base) ubuntu@Ubuntu22:~/Learn/protubuf-learn/update/service$ ./service ../contacts.bin
-------------新增联系人-------------
请输入联系人姓名: 王五
请输入联系人生日: 1112
请输入联系人电话1(只输入回车完成电话新增): 32234456
请输入联系人电话2(只输入回车完成电话新增): 
-----------添加联系人成功-----------
(base) ubuntu@Ubuntu22:~/Learn/protubuf-learn/update/service$ cd ../client/
(base) ubuntu@Ubuntu22:~/Learn/protubuf-learn/update/client$ ./client ../contacts.bin
------------联系人1------------
姓名:张三
年龄:20
电话1: 23211111
电话2: 23233233
------------联系人2------------
姓名:李四
年龄:1221
电话1: 897812
------------联系人3------------
姓名:王五
年龄:0
电话1: 32234456
  • 根据实验结果,发现 '王五' 的年龄为 0,这是由于新增时未设置年龄,通过 client 程序反序列化时,给年龄字段设置了默认值 0。这个结果显然是我们想看到的。
  • 还要解释一下 '李四' 的年龄依旧使用了之前设置的生日字段 '1221',这是因为在新增 '李四' 的时候,生日字段的字段编号依旧为 2,并且已经被序列化到文件中了。最后再读取的时候,字段编号依旧为 2。
  • 还要再说一下的是:因为使用了 reserved 关键字,ProtoBuf在编译阶段就会拒绝我们使用已经保留的字段编号。到此实验结束,也印证了我们的结论。

根据以上的例子,有的同学可能还有一个疑问:如果使用了 reserved 2 了,那么 service 给 '王五' 设置的生日 '1112',client 就没法读到了吗? 答案是可以的。继续学习下面的未知字段即可揭晓答案。


3.8.3 未知字段(通讯录3.1)

  • 在通讯录 3.0 版本中,我们向 service 目录下的 contacts.proto 新增了'生日'字段,但对于 client 相关的代码并没有任何改动。验证后发现 新代码序列化的消息(service)也可以被旧代码(client)解析。并且这里要说的是,新增的 '生日'字段在旧程序(client)中其实并没有丢失,而是会作为旧程序的未知字段
    • 未知字段:解析结构良好的 protocol buffer 已序列化数据中的未识别字段的表示方式。例如,当旧程序解析带有新字段的数据时,这些新字段就会成为旧程序的未知字段。
    • 本来,proto3 在解析消息时总是会丢弃未知字段,但在 3.5 版本中重新引入了对未知字段的保留机制。所以在 3.5 或更高版本中,未知字段在反序列化时会被保留,同时也会包含在序列化的结果中。

3.8.3.1 未知字段从哪获取

1. 了解相关类关系图

2. MessageLite 类介绍(了解)

  • MessageLite 从名字看是轻量级的 message,仅仅提供序列化、反序列化功能。
  • 类定义在 google 提供的 message_lite.h 中。

3. Message 类介绍(了解)

  • 我们自定义的message类,都是继承自Message
  • Message 最重要的两个接口 GetDescriptor/GetReflection,可以获取该类型对应的 Descriptor 对象指针 和 Reflection 对象指针。
  • 类定义在 google 提供的 message.h 中。
cpp 复制代码
//google::protobuf::Message 部分代码展示
const Descriptor* GetDescriptor() const;
const Reflection* GetReflection() const;

4. Descriptor 类介绍(了解)

  • Descriptor:是对 message 类型定义的描述,包括 message 的名字、所有字段的描述、原始的 proto 文件内容等。
  • 类定义在 google 提供的 descriptor.h 中。
cpp 复制代码
// 部分代码展示
class PROTOBUF_EXPORT Descriptor : private internal::SymbolBase {
 	string& name () const
 	int field_count() const;
 	const FieldDescriptor* field(int index) const;
 	const FieldDescriptor* FindFieldByNumber(int number) const;
 	const FieldDescriptor* FindFieldByName(const std::string& name) const;
 	const FieldDescriptor* FindFieldByLowercaseName(
 		const std::string& lowercase_name) const;
 	const FieldDescriptor* FindFieldByCamelcaseName(
 		const std::string& camelcase_name) const;
 	int enum_type_count() const;
 	const EnumDescriptor* enum_type(int index) const;
 	const EnumDescriptor* FindEnumTypeByName(const std::string& name) const;
 	const EnumValueDescriptor* FindEnumValueByName(const std::string& name) const;
}

5. Reflection 类介绍(了解)

  • Reflection 接口类,主要提供了动态读写消息字段的接口,对消息对象的自动读写主要通过该类完成。
  • 提供方法来动态访问/修改 message 中的字段,对每种类型,Reflection 都提供了一个单独的接口用于读写字段对应的值。
    • 针对所有不同的 field 类型 FieldDescriptor::TYPE_*,需要使用不同的 Get*()/Set* ()/Add*() 接口;
    • repeated 类型需要使用 GetRepeated*()/SetRepeated*() 接口,不可以和非 repeated 类型接口混用;
    • message 对象只可以被由它自身的 reflectionmessage.GetReflection()) 来操作;
  • 类中还包含了访问/修改未知字段的方法。
  • 类定义在 google 提供的 message.h 中。
cpp 复制代码
// 部分代码展示
class PROTOBUF_EXPORT Reflection final {
 	const UnknownFieldSet& GetUnknownFields(const Message& message) const;
 	UnknownFieldSet* MutableUnknownFields(Message* message) const;
 	bool HasField(const Message& message, const FieldDescriptor* field) const;
 	int FieldSize(const Message& message, const FieldDescriptor* field) const;
 	void ClearField(Message* message, const FieldDescriptor* field) const;
 	bool HasOneof(const Message& message,
 				const OneofDescriptor* oneof_descriptor) const;
 	void ClearOneof(Message* message,
 				const OneofDescriptor* oneof_descriptor) const;
 	const FieldDescriptor* GetOneofFieldDescriptor(
 		const Message& message, const OneofDescriptor* oneof_descriptor) const;
 	
 	// Singular field getters ------------------------------------------
 	// These get the value of a non-repeated field. They return the default
 	// value for fields that aren't set.
 	int32_t GetInt32(const Message& message, const FieldDescriptor* field) const;
 	int64_t GetInt64(const Message& message, const FieldDescriptor* field) const;
 	uint32_t GetUInt32(const Message& message,
 				const FieldDescriptor* field) const;
 	uint64_t GetUInt64(const Message& message,
 				const FieldDescriptor* field) const;
 	float GetFloat(const Message& message, const FieldDescriptor* field) const;
 	double GetDouble(const Message& message, const FieldDescriptor* field) const;
 	bool GetBool(const Message& message, const FieldDescriptor* field) const;
 	std::string GetString(const Message& message,
 				const FieldDescriptor* field) const;
 	const EnumValueDescriptor* GetEnum(const Message& message,
 				const FieldDescriptor* field) const;
 	int GetEnumValue(const Message& message, const FieldDescriptor* field) const;
 	const Message& GetMessage(const Message& message,
 				const FieldDescriptor* field,
 	MessageFactory* factory = nullptr) const;
 	
 	// Singular field mutators -----------------------------------------
 	// These mutate the value of a non-repeated field.
 	void SetInt32(Message* message, const FieldDescriptor* field,
 				int32_t value) const;
 	void SetInt64(Message* message, const FieldDescriptor* field,
 				int64_t value) const;
 	void SetUInt32(Message* message, const FieldDescriptor* field,
 				uint32_t value) const;
 	void SetUInt64(Message* message, const FieldDescriptor* field,
 				uint64_t value) const;
 	void SetFloat(Message* message, const FieldDescriptor* field,
 				float value) const;
 	void SetDouble(Message* message, const FieldDescriptor* field,
 				double value) const;
 	void SetBool(Message* message, const FieldDescriptor* field,
 				bool value) const;
 	void SetString(Message* message, const FieldDescriptor* field,
 				std::string value) const;
 	void SetEnum(Message* message, const FieldDescriptor* field,
 				const EnumValueDescriptor* value) const;
 	void SetEnumValue(Message* message, const FieldDescriptor* field,
 				int value) const;
 	Message* MutableMessage(Message* message, const FieldDescriptor* field,
 				MessageFactory* factory = nullptr) const;
 	PROTOBUF_NODISCARD Message* ReleaseMessage(
 				Message* message, const FieldDescriptor* field,
 				MessageFactory* factory = nullptr) const;
 
 	// Repeated field getters ------------------------------------------
 	// These get the value of one element of a repeated field.
 	int32_t GetRepeatedInt32(const Message& message, 
 			const FieldDescriptor* field, int index) const;
 	int64_t GetRepeatedInt64(const Message& message, 
 			const FieldDescriptor* field, int index) const;
 	uint32_t GetRepeatedUInt32(const Message& message,
 			const FieldDescriptor* field, int index) const;
 	uint64_t GetRepeatedUInt64(const Message& message,
 			const FieldDescriptor* field, int index) const;
 	float GetRepeatedFloat(const Message& message, 
 			const FieldDescriptor* field, int index) const;

 	double GetRepeatedDouble(const Message& message, 
 			const FieldDescriptor* field, int index) const;
 	bool GetRepeatedBool(const Message& message, 
 			const FieldDescriptor* field, int index) const;
 	std::string GetRepeatedString(const Message& message,
 			const FieldDescriptor* field, int index) const;
 	const EnumValueDescriptor* GetRepeatedEnum(const Message& message,
 			const FieldDescriptor* field, int index) const;
 	int GetRepeatedEnumValue(const Message& message, 
 			const FieldDescriptor* field, int index) const;
 	const Message& GetRepeatedMessage(const Message& message,
 			const FieldDescriptor* field, int index) const;
 	const std::string& GetRepeatedStringReference(const Message& message,
 			const FieldDescriptor* field, int index, std::string* scratch) const;
 
 	// Repeated field mutators -----------------------------------------
 	// These mutate the value of one element of a repeated field.
 	void SetRepeatedInt32(Message* message, const FieldDescriptor* field,
 			int index, int32_t value) const;
 	void SetRepeatedInt64(Message* message, const FieldDescriptor* field,
 			int index, int64_t value) const;
 	void SetRepeatedUInt32(Message* message, const FieldDescriptor* field,
 			int index, uint32_t value) const;
 	void SetRepeatedUInt64(Message* message, const FieldDescriptor* field,
 			int index, uint64_t value) const;
 	void SetRepeatedFloat(Message* message, const FieldDescriptor* field,
 			int index, float value) const;
 	void SetRepeatedDouble(Message* message, const FieldDescriptor* field,
 			int index, double value) const;
 	void SetRepeatedBool(Message* message, const FieldDescriptor* field,
 			int index, bool value) const;
 	void SetRepeatedString(Message* message, const FieldDescriptor* field,
 			int index, std::string value) const;
 	void SetRepeatedEnum(Message* message, const FieldDescriptor* field,
 			int index, const EnumValueDescriptor* value) const;
 	void SetRepeatedEnumValue(Message* message, const FieldDescriptor* field,
 			int index, int value) const;
 	Message* MutableRepeatedMessage(Message* message,
 			const FieldDescriptor* field, int index) const;

 	// Repeated field adders -------------------------------------------
 	// These add an element to a repeated field.
 	void AddInt32(Message* message, const FieldDescriptor* field,
 			int32_t value) const;
 	void AddInt64(Message* message, const FieldDescriptor* field,
 			int64_t value) const;
 	void AddUInt32(Message* message, const FieldDescriptor* field,
 			uint32_t value) const;
 	void AddUInt64(Message* message, const FieldDescriptor* field,
 			uint64_t value) const;
 	void AddFloat(Message* message, const FieldDescriptor* field,
 			float value) const;
 	void AddDouble(Message* message, const FieldDescriptor* field,
 			double value) const;
 	void AddBool(Message* message, const FieldDescriptor* field,
 			bool value) const;
 	void AddString(Message* message, const FieldDescriptor* field,
 			std::string value) const;
 	void AddEnum(Message* message, const FieldDescriptor* field,
 			const EnumValueDescriptor* value) const;
 	void AddEnumValue(Message* message, const FieldDescriptor* field,
 			int value) const;
 	Message* AddMessage(Message* message, const FieldDescriptor* field,
 			MessageFactory* factory = nullptr) const;
 	const FieldDescriptor* FindKnownExtensionByName(
 			const std::string& name) const;
 	const FieldDescriptor* FindKnownExtensionByNumber(int number) const;
 	bool SupportsUnknownEnumValues() const;
};

6. UnknownFieldSet 类介绍(重要)

  • UnknownFieldSet 包含在分析消息时遇到但未由其类型定义的所有字段。
  • 若要将 UnknownFieldSet 附加到任何消息,请调用Reflection::GetUnknownFields()
  • 类定义在 unknown_field_set.h 中。
cpp 复制代码
class PROTOBUF_EXPORT UnknownFieldSet {
 	inline void Clear();
 	void ClearAndFreeMemory();
 	inline bool empty() const;
 	inline int field_count() const;
 	inline const UnknownField& field(int index) const;

 	inline UnknownField* mutable_field(int index);
 	
 	// Adding fields ---------------------------------------------------
 	void AddVarint(int number, uint64_t value);
 	void AddFixed32(int number, uint32_t value);
 	void AddFixed64(int number, uint64_t value);
 	void AddLengthDelimited(int number, const std::string& value);
 	std::string* AddLengthDelimited(int number);
 	UnknownFieldSet* AddGroup(int number);
 	
 	// Parsing helpers -------------------------------------------------
 	// These work exactly like the similarly-named methods of Message.
 	bool MergeFromCodedStream(io::CodedInputStream* input);
 	bool ParseFromCodedStream(io::CodedInputStream* input);
 	bool ParseFromZeroCopyStream(io::ZeroCopyInputStream* input);
 	bool ParseFromArray(const void* data, int size);
 	inline bool ParseFromString(const std::string& data) {
 		return ParseFromArray(data.data(), static_cast<int>(data.size()));
 	}
 
 	// Serialization.
 	bool SerializeToString(std::string* output) const;
 	bool SerializeToCodedStream(io::CodedOutputStream* output) const;
 	static const UnknownFieldSet& default_instance();
};

7. UnknownField 类介绍(重要)

  • 表示未知字段集中的一个字段。
  • 类定义在 unknown_field_set.h 中。
cpp 复制代码
class PROTOBUF_EXPORT UnknownField {
public:
 	enum Type {
 		TYPE_VARINT,
 		TYPE_FIXED32,
 		TYPE_FIXED64,
 		TYPE_LENGTH_DELIMITED,
 		TYPE_GROUP
 	};
 	inline int number() const;	// 可以拿到未知字段编号
 	inline Type type() const;	// 可以拿到未知字段类型

 	// Accessors -------------------------------------------------------
 	// Each method works only for UnknownFields of the corresponding type.
 	inline uint64_t varint() const;
 	inline uint32_t fixed32() const;
 	inline uint64_t fixed64() const;
 	inline const std::string& length_delimited() const;
 	inline const UnknownFieldSet& group() const;
 	inline void set_varint(uint64_t value);
 	inline void set_fixed32(uint32_t value);
 	inline void set_fixed64(uint64_t value);
 	inline void set_length_delimited(const std::string& value);
 	inline std::string* mutable_length_delimited();
 	inline UnknownFieldSet* mutable_group();
};
  • 这几个枚举类型,和取值方法一一对应:

3.8.3.2 升级通讯录 3.1 版本---验证未知字段

1. 更新 client.cc (通讯录 3.1),在这个版本中,需要打印出未知字段的内容。更新的代码如下:

cpp 复制代码
#include <iostream>
#include <fstream>
#include "contacts.pb.h"

using namespace std;
using namespace c_contacts;
using namespace google::protobuf;
/**
 * 打印联系人列表
 */
void PrintfContacts(const Contacts &contacts)
{
    for (int i = 0; i < contacts.contacts_size(); ++i)
    {
        const PeopleInfo &people = contacts.contacts(i);
        cout << "------------联系人" << i + 1 << "------------" << endl;
        cout << "姓名:" << people.name() << endl;
        cout << "年龄:" << people.age() << endl;
        int j = 1;
        for (const PeopleInfo_Phone &phone : people.phone())
        {
            cout << "电话" << j++ << ": " << phone.number() << endl;
        }

		// 下面是新增内容
        const Reflection* reflection = PeopleInfo::GetReflection();
        const UnknownFieldSet& set = reflection->GetUnknownFields(people);
        for (int j = 0; j < set.field_count(); j++){
            const UnknownField& unknow_field = set.field(j);
            cout << "未知字段" << j + 1 << ":"
                << "编号:" << unknow_field.number();
            switch (unknow_field.type()){
                case UnknownField::Type::TYPE_VARINT:
                    cout << " 值:" << unknow_field.varint() << endl;
                    break;
                case UnknownField::Type::TYPE_LENGTH_DELIMITED:
                    cout << " 值:" << unknow_field.length_delimited() << endl;
                    break;
                case UnknownField::Type::TYPE_FIXED32:
                    cout << " 值:" << unknow_field.fixed32() << endl;
                    break;
                case UnknownField::Type::TYPE_FIXED64:
                    cout << " 值:" << unknow_field.fixed64() << endl;
                    break;
                default:
                    break;
            }
        }
    }
}

int main(int argc, char *argv[])
{

    GOOGLE_PROTOBUF_VERIFY_VERSION;
    if (argc != 2)
    {
        cerr << "Usage: " << argv[0] << "CONTACTS_FILE" << endl;
        return -1;
    }
    // 以二进制方式读取 contacts
    Contacts contacts;
    fstream input(argv[1], ios::in | ios::binary);
    if (!contacts.ParseFromIstream(&input))
    {
        cerr << "Failed to parse contacts." << endl;
        input.close();
        return -1;
    }

    // 打印 contacts
    PrintfContacts(contacts);
    input.close();
    google::protobuf::ShutdownProtobufLibrary();
    return 0;
}

2. 其他文件均不用做任何修改,重新编译 client.cc,进行一次读操作可得如下结果:

0 复制代码
(base) ubuntu@Ubuntu22:~/Learn/protubuf-learn/update/client$ ./client ../contacts.bin
------------联系人1------------
姓名:张三
年龄:20
电话1: 23211111
电话2: 23233233
------------联系人2------------
姓名:李四
年龄:1221
电话1: 897812
------------联系人3------------
姓名:王五
年龄:0
电话1: 32234456
未知字段1:编号:4 值:1112

3.8.4 前后兼容性


根据上述的例子可以得出,pb 是具有向前兼容的。为了叙述方便,把增加了"生日"属性的 service 称为"新模块";未做变动的 client 称为 "老模块"。

  • 向前兼容:老模块能够正确识别新模块生成或发出的协议。这时新增加的"生日"属性会被当作知字段(pb 3.5版本及之后)。
  • 向后兼容:新模块也能够正确识别老模块生成或发出的协议。

前后兼容的作用:当我们维护一个很庞大的分布式系统时,由于你无法同时 升级所有 模块,为了保证在升级过程中,整个系统能够尽可能不受影响,就需要尽量保证通讯协议的"向后兼容"或"向前兼容"。


3.9 选项 option

.proto 文件中可以声明许多选项,使用 option 标注。选项能影响 protoc 编译器的某些处理方式。


3.9.1 选项分类


选项的完整列表在google/protobuf/descriptor.proto中定义。部分代码:

proto 复制代码
syntax = "proto2"; // descriptor.proto 使用 proto2 语法版本

message FileOptions { ... } // 文件选项 定义在 FileOptions 消息中

message MessageOptions { ... } // 消息类型选项 定义在 MessageOptions 消息中

message FieldOptions { ... } // 消息字段选项 定义在 FieldOptions 消息中

message OneofOptions { ... } // oneof字段选项 定义在 OneofOptions 消息中

message EnumOptions { ... } // 枚举类型选项 定义在 EnumOptions 消息中

message EnumValueOptions { .. } // 枚举值选项 定义在 EnumValueOptions 消息中

message ServiceOptions { ... } // 服务选项 定义在 ServiceOptions 消息中

message MethodOptions { ... } // 服务方法选项 定义在 MethodOptions 消息中

...
  • 由此可见,选项分为 文件级、消息级、字段级 等等, 但并没有一种选项能作用于所有的类型。

3.9.2 常用选项列举


1. optimize_for

  • 该选项为文件选项,可以设置 protoc 编译器的优化级别,分别为 SPEED CODE_SIZELITE_RUNTIME 。受该选项影响,设置不同的优化级别,编译 ·proto 文件后生成的代码内容不同。
    • SPEEDprotoc 编译器将生成的代码是高度优化的,代码运行效率高,但是由此生成的代码编译后会占用更多的空间。 SPEED 是默认选项。
    • CODE_SIZEproto 编译器将生成最少的类,会占用更少的空间,是依赖基于反射的代码来实现序列化、反序列化和各种其他操作。但和 SPEED 恰恰相反,它的代码运行效率较低。这种方式适合用在包含大量的 .proto 文件,但并不盲目追求速度的应用中。
    • LITE_RUNTIME:生成的代码执行效率高,同时生成代码编译后的所占用的空间也是非常少。这是以牺牲Protocol Buffer提供的反射功能为代价的,仅仅提供 encoding+序列化 功能,所以我们在链接 BP 库时仅需链接 libprotobuf-lite,而非 libprotobuf。这种模式通常用于资源有限的平台,例如移动手机平台中。
0 复制代码
option optimize_for = LITE_RUNTIME;

2. allow_alias

  • 允许将相同的常量值分配给不同的枚举常量,用来定义别名。该选项为枚举选项。举个例子:
java 复制代码
enum PhoneType {
 	option allow_alias = true;
 	MP = 0;
 	TEL = 1;
 	LANDLINE = 1; // 若不加 option allow_alias = true; 这一行会编译报错
}

相关推荐
下位子2 小时前
『OpenGL学习滤镜相机』- Day4: 纹理贴图基础
android·opengl
Cx330❀2 小时前
《C++ 搜索二叉树》深入理解 C++ 搜索二叉树:特性、实现与应用
java·开发语言·数据结构·c++·算法·面试
2501_915909062 小时前
iOS 发布 App 全流程指南,从签名打包到开心上架(Appuploader)跨平台免 Mac 上传实战
android·macos·ios·小程序·uni-app·cocoa·iphone
Kapaseker2 小时前
在 Compose 中使用 SurfaceView
android·kotlin
爱吃烤鸡翅的酸菜鱼2 小时前
深度解析《AI+Java编程入门》:一本为零基础重构的Java学习路径
java·人工智能·后端·ai
SimonKing2 小时前
被卖的Hutool出AI模块了!它如何让Java调用大模型变得如此简单?
java·后端·程序员
不染尘.3 小时前
2025_11_5_刷题
开发语言·c++·vscode·算法·贪心算法·动态规划
不穿格子的程序员3 小时前
从零开始刷算法-栈-字符串解码
java·开发语言
你不是我我3 小时前
【Java 开发日记】设计模式了解吗,知道什么是饿汉式和懒汉式吗?
android·java·开发语言