ProtoBuf - 3

proto3 语法详解

在语法详解部分,依旧使用项目推进的方式来演示。这个部分会对通讯录进行多次升级,使用 2.x 表示升级的版本,最终将会介绍如下内容:

  • 不再打印联系人的序列化结果,而是将通讯录序列化后并写入文件中。
  • 从文件中将通讯录解析出来,并进行打印。
  • 新增联系人属性,共包括:姓名、年龄、电话信息、地址、其他联系方式、备注。

1. 字段规则

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

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

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

cpp 复制代码
syntax = "proto3";
package contacts;
 
message PeopleInfo {
  string name = 1;
  int32 age = 2;
  repeated string phone_numbers = 3;
}

2. 消息类型的定义与使用

2.1 定义

在单个.proto文件中可以定义多个 "消息" 体,且支持定义嵌套类型的消息(任意多层)。每个消息中的字段编号可以重复。(这个 message 就相当于 C++ 的类)

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

cpp 复制代码
message Phone {
  string number = 1;
}
 
message PeopleInfo {
  string name = 1;
  int32 age = 2;
2.2 使用
  • 消息类型可作为字段类型使用

contacts.proto

cpp 复制代码
syntax = "proto3";
package contacts;
 
message PeopleInfo {
  string name = 1;
  int32 age = 2;
 
  message Phone {
    string number = 1;
  }
  repeated Phone phone = 3;
}
cpp 复制代码
package contacts;
 
import "phone.proto";  // 使用import将phone.proto文件导入进来!!!
 
message PeopleInfo {
  string name = 1;
  int32 age = 2;
 
  // 引入文件声明了package,使用消息时,需要用"命名空间.消息类"格式导入
  repeated phone.Phone phone = 3;
}

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

2.3 创建通讯录 2.0 版本

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

cpp 复制代码
syntax = "proto3";
package contacts;
 
// 联系人
message PeopleInfo {
  string name = 1;     // 姓名
  int32 age = 2;       // 年龄
 
  message Phone {
    string number = 1; // 电话号码
  }
  repeated Phone phone = 3; // 电话
}
 
// 通讯录
message Contacts {
 repeated PeopleInfo contacts = 1;
}

接着进行一次编译:

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

编译后生成的contacts.pb.hcontacts.pb.cc会将在快速上手的生成文件覆盖掉。

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

cpp 复制代码
void set_allocated_number(std::string* number);
 
// ==== PeopleInfo ====
 
class PeopleInfo final : public ::PROTOBUF_NAMESPACE_ID::Message {
 public:
  using CopyFrom = ::PROTOBUF_NAMESPACE_ID::Message::CopyFrom;
  using MergeFrom = ::PROTOBUF_NAMESPACE_ID::Message::MergeFrom;
  using Proto = PeopleInfo;
  void MergeFrom(const PeopleInfo& from) {
    ::PROTOBUF_NAMESPACE_ID::internal::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();
  ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField<PeopleInfo_Phone>*
  mutable_phone();
  const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField<PeopleInfo_Phone>&
  phone() const;
  ::PeopleInfo_Phone* mutable_phone(int index);
  ::PeopleInfo_Phone* add_phone();
  const ::PeopleInfo_Phone& phone(int index) const;
 
};
// ==== Contacts ====
 
class Contacts final : public ::PROTOBUF_NAMESPACE_ID::Message {
 public:
  using CopyFrom = ::PROTOBUF_NAMESPACE_ID::Message::CopyFrom;
  using MergeFrom = ::PROTOBUF_NAMESPACE_ID::Message::MergeFrom;
  using Proto = Contacts;
 
  static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() {
    return "Contacts";
  }
 
  // repeated PeopleInfo contacts = 1;
  int contacts_size() const;
  void clear_contacts();
  ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField<PeopleInfo>*
  mutable_contacts();
  const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField<PeopleInfo>&
  contacts() const;
  ::PeopleInfo* mutable_contacts(int index);
  ::PeopleInfo* add_contacts();
  const ::PeopleInfo& contacts(int index) const;
 
};

上述的例子中:

  • 每个字段都有一个clear_方法,可以将字段重新设置回empty状态。
  • 每个字段都有设置和获取的方法,获取方法的方法名称都与小写字段名完全相同。但如果是消息类型的字段,可以直接对方法返回的内容进行访问,返回值为消息类型的指针,这类方法会为我们开辟好空间,可以直接设置方法的mutable_方法。
  • 对于使用repeated修饰的字段,也就是数组类型,pb为我们提供了add_方法来新增一个值,并且提供了_size方法来判断数组存放元素的个数。
2.3.1 通讯录 2.0 的写入实现

write.cc(通讯录 2.0)

cpp 复制代码
#include <iostream>
#include "contacts.pb.h"
#include <fstream>
using namespace std;
using namespace contacts;
 
// 添加联系人
void AddPeopleInfo(PeopleInfo* people_info_ptr) {
  cout << "-------新增联系人-------" << endl;
  cout << "请输入联系人姓名:";
  string name;
  cin >> name;
  people_info_ptr->set_name(name);
 
  cout << "请输入联系人年龄:";
  int age;
  cin >> age;
  people_info_ptr->set_age(age);
 
  while (true) {
    cout << "请输入联系人电话(只输入回车完成电话新增):";
    string number;
    cin >> number;
    if (number.empty()) {
      break;
    }
    PeopleInfo_Phone* phone_ptr = people_info_ptr->add_phone();
    phone_ptr->set_number(number);
  }
}
 
int main(int argc, char* argv[]) {
  // 调用GOOGLE_PROTOBUF_VERIFY_VERSION,检查protobuf库版本是否与编译版本兼容。
  // 这个宏必须在调用任何其他库函数之前调用,但是在调用ParseFromIstream之后可以安全地释放。
  GOOGLE_PROTOBUF_VERIFY_VERSION;
 
  if (argc != 2) {
    cerr << "Usage: " << argv[0] << " CONTACTS_FILE" << endl;
    return -1;
  }
 
  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;
    return -1;
  }
 
  // 添加一个联系人
  AddPeopleInfo(contacts.add_contacts());
 
  // 将序列化后的数据写入文件
  fstream output(argv[1], ios::out | ios::trunc | ios::binary);
  if (!contacts.SerializeToOstream(&output)) {
    cerr << "Failed to write contacts." << endl;
    return -1;
  }
 
  // 可选:删除所有已分配的protobuf对象。
  // 对于大多数程序来说,这不是必要的,因为进程退出时会自动释放内存。
  // 但是在某些情况下,比如在长时间运行的服务器进程中,可能需要手动释放内存以避免内存泄漏。
  google::protobuf::ShutdownProtobufLibrary();
 
  return 0;
}

makefile

bash 复制代码
write:write.cc contacts.pb.cc
  g++ -o $@ $^ -std=c++11 -lprotobuf
 
.PHONY:clean
clean:
  rm -f write

make之后,运行write

bash 复制代码
g++ -o write write.cc contacts.pb.cc -std=c++11 -lprotobuf
hb@13-159-156-152:~/project/protobuf/contacts1$ ./write contacts.bin
contacts.bin: File not found. Creating a new file.
-------新增联系人-------
请输入联系人姓名:张三
请输入联系人年龄:20
请输入联系人电话(只输入回车完成电话新增):13111111111
请输入联系人电话(只输入回车完成电话新增):15111111111
请输入联系人电话(只输入回车完成电话新增):

查看二进制文件

cpp 复制代码
hb@13-159-156-152:~/project/protobuf/contacts1$ xxd contacts.bin
00000000: 0a20 0a06 e5bc a0e4 b889 1014 1a0b 3133  . ............13
00000010: 3131 3131 3131 3131 3131 311a0b 3135 3131  1111111111..1511
00000020: 3131 3131 3131 3131 3131                 1111111111

解释:xxd是 Linux 下的一个 "二进制" 文件查看工具,它可以将 "二进制" 文件转换为 ASCII、八进制、十进制、十六进制格式进行查看。

  • 十六进制:十六个数字(0~f)进行编码和对应的 ASCII 字符
2.3.2 通讯录 2.0 的读取实现

read.cc(通讯录 2.0)

cpp 复制代码
#include <iostream>
#include "contacts.pb.h"
#include <fstream>
using namespace std;
using namespace contacts;
 
// 打印联系人列表
void PrintContacts(const Contacts& contacts) {
  for (int i = 0; i < contacts.contacts_size(); ++i) {
    const PeopleInfo& people_info = contacts.contacts(i);
    cout << "-------联系人" << i+1 << "-------" << endl;
    cout << "姓名:" << people_info.name() << endl;
    cout << "年龄:" << people_info.age() << endl;
    cout << "电话:" << endl;
    for (int j = 0; j < people_info.phone_size(); ++j) {
      const PeopleInfo_Phone& phone = people_info.phone(j);
      cout << "  " << 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;
 
  fstream input(argv[1], ios::in | ios::binary);
  if (!input) {
    cerr << argv[1] << ": File not found." << endl;
    return -1;
  } else if (!contacts.ParseFromIstream(&input)) {
    cerr << "Failed to parse contacts." << endl;
    return -1;
  }
 
  PrintContacts(contacts);
 
  google::protobuf::ShutdownProtobufLibrary();
  return 0;
}

makefile

bash 复制代码
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

bash 复制代码
hb@13-159-156-152:~/project/protobuf/contacts1$ 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
hb@13-159-156-152:~/project/protobuf/contacts1$ ./read contacts.bin
-------联系人1-------
姓名:张三
年龄:20
电话:
  13111111111
  15111111111
另一种调试工具

我们可以用protoc命令,来查看protobuf为我们提供的所有命令option,执行protoc --help提供一个选项,消息--decode选项,表示从标准输入中读取定义的二进制消息,并将其以文本格式写入标准输出(消歧义:decodeprotobuf文件 / 程序的二进制中,是 "解析")。

bash 复制代码
hb@13-159-156-152:~/project/protobuf/contacts1$ protoc --decode contacts.Contacts contacts.proto < contacts.bin
contacts {
  name: "\345\274\240\344\270\211"  // 在这里是将utf-8汉字转为八进制格式输出了
  age: 20
  phone {
    number: "13111111111"
  }
  phone {
    number: "15111111111"
  }
}

3. enum 类型

3.1 定义规则

在 proto 文件中定义枚举类型并使用,枚举类型的书写规范为:

  • 枚举类型名称 :使用驼峰命名法,首字母大写。例如:PhoneType
  • 常量值名称 :全大写字母,多个单词之间用 _ 连接。例如:ENM_CONST = 0;

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

cpp 复制代码
enum PhoneType {
  MP = 0; // 移动电话
  TEL = 1; // 固定电话
}

要注意枚举类型的定义有以下几种规则:

  1. 0 值常量必须存在 ,且要作为第一个元素。这是为了与 proto2 的语义兼容:第一个元素为默认值,且值为 0。
  2. 枚举类型可以在消息外定义,也可以在消息体内定义(嵌套)。
  3. 枚举的常量值在 32 位整数的范围内,但因负值无效故而不建议使用(与编码规则有关)。
3.2 定义时注意

将两个 "具有相同枚举值名称" 的枚举类型放在单个.proto 文件下测试时,编译器会报错:某某某常量已被定义。所以这里要注意:

  • 同级(同层)的枚举类型,各个枚举类型中的常量不能重名。
  • 单个.proto 文件下,最外层枚举类型和嵌套枚举类型,不算同级。
  • 多个.proto 文件下,若一个文件引入了其他文件,且每个文件都未声明 package,每个 proto 文件中的枚举类型都在最外层,算同级。
  • 多个.proto 文件下,若一个文件引入了其他文件,且每个文件都声明了 package,不算同级。
情况 1:同级枚举类型包含相同枚举名称
cpp 复制代码
// 同级枚举类型包含相同枚举名称
enum PhoneType {
  MP = 0; // 移动电话
  TEL = 1; // 固定电话
}

enum PhoneTypeCopy {
  MP = 0; // 移动电话 // 编译后报错:MP 已经定义
}
情况 2:不同级枚举类型包含相同枚举名称
cpp 复制代码
// 不同级枚举类型包含相同枚举名称
enum PhoneType {
  MP = 0; // 移动电话 // 用法正确
}

message Phone {
  string number = 1; // 电话号码
  enum PhoneType {
    MP = 0; // 移动电话
    TEL = 1; // 固定电话
  }
  PhoneType type = 2; // 类型
}
情况 3:多文件下未声明 package
cpp 复制代码
// phone1.proto
import "phone2.proto"
enum PhoneType {
  MP = 0; // 移动电话 // 编译后报错:MP 已经定义
  TEL = 1; // 固定电话
}

// phone2.proto
enum PhoneTypeCopy {
  MP = 0; // 移动电话
}
情况 4:多文件下声明了 package
cpp 复制代码
// phone1.proto
import "phone2.proto"
package phone1;
enum PhoneType {
  MP = 0; // 移动电话 // 用法正确
  TEL = 1; // 固定电话
}

// phone2.proto
package phone2;
enum PhoneTypeCopy {
  MP = 0; // 移动电话
}
3.3 升级通讯录至 2.1 版本

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

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

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

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

  repeated Phone phone = 3; // 电话
}

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

执行编译命令:protoc -cpp_out=. contacts.proto

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

cpp 复制代码
// 枚举 PeopleInfo_Phone_PhoneType
enum PeopleInfo_Phone_PhoneType {
  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 bool PhoneType_IsValid(int value) {
    return PeopleInfo_Phone_PhoneType_IsValid(value);
  }
  static const std::string& PhoneType_Name(PhoneType value) { ... }
  static bool PhoneType_Parse(const std::string& name, PhoneType* value) { ... }

  // 字段 type
  void clear_type();
  ::contacts::PeopleInfo_Phone_PhoneType type() const;
  void set_type(::contacts::PeopleInfo_Phone_PhoneType value);
};

上述代码中:

  • 对于.proto 文件中定义的枚举类型,编译生成的代码中会含有与之对应的枚举类型、校验枚举值是否有效的方法_IsValid、以及获取枚举值名称的方法_Name
  • 对于使用了枚举类型的字段,包含设置和获取字段的方法,已经清空字段的方法clear_
更新 write.cc(通讯录 2.1):
cpp 复制代码
#include <iostream>
#include <fstream>
#include "contacts.pb.h"
using namespace std;
using namespace contacts;

/*
 * 添加联系人
 */
void AddPeopleInfo(PeopleInfo* people_info_ptr)
{
    cout << "----------添加联系人----------" << endl;
    cout << "请输入联系人姓名:";
    string name;
    cin >> name;
    people_info_ptr->set_name(name);

    cout << "请输入联系人年龄:";
    int age;
    cin >> age;
    people_info_ptr->set_age(age);

    cout << "请输入联系人电话(输入-1 结束):" << endl;
    while (1) {
        cout << "请输入联系人电话:";
        string number;
        cin >> number;
        if (number == "-1") {
            break;
        }
        PeopleInfo::Phone* phone = people_info_ptr->add_phone();
        phone->set_number(number);

        cout << "请选择电话类型(1.移动电话  2.固定电话):";
        int type;
        cin >> type;
        switch (type) {
            case 1:
                phone->set_type(PeopleInfo::Phone::PhoneType::MP);
                break;
            case 2:
                phone->set_type(PeopleInfo::Phone::PhoneType::TEL);
                break;
            default:
                cout << "非法选项,使用默认值!" << endl;
                break;
        }
    }
    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;

    // 先读取已存在的通讯录
    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;
        return -1;
    }

    // 添加一个联系人
    AddPeopleInfo(contacts.add_contacts());

    // 再把通讯录写入到文件中
    fstream output(argv[1], ios::out | ios::trunc | ios::binary);
    if (!contacts.SerializeToOstream(&output)) {
        cerr << "Failed to write contacts." << endl;
        return -1;
    }

    output.close();
    input.close();
    return 0;
}
更新 read.cc(通讯录 2.1)
cpp 复制代码
#include <iostream>
#include <fstream>
#include "contacts.pb.h"
using namespace std;
using namespace contacts;

/*
 * 打印联系人列表
 */
void PrintContacts(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;

        for (int j = 0; j < people.phone_size(); ++j) {
            const PeopleInfo::Phone& phone = people.phone(j);
            cout << "电话" << j+1 << ":" << phone.number() << "(" 
                 << PeopleInfo::Phone::PhoneType_Name(phone.type()) 
                 << ")" << 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;
    fstream input(argv[1], ios::in | ios::binary);
    if (!contacts.ParseFromIstream(&input)) {
        cerr << "Failed to parse contacts." << endl;
        return -1;
    }

    // 打印通讯录
    PrintContacts(contacts);

    input.close();
    google::protobuf::ShutdownProtobufLibrary();
    return 0;
}

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

bash 复制代码
# 编译 write 程序
g++ write.cc contacts.pb.cc -o write_contacts -lprotobuf
# 运行 write 程序
./write_contacts test.data

运行输出:

bash 复制代码
----------添加联系人----------
请输入联系人姓名:李四
请输入联系人年龄:25
请输入联系人电话(输入-1 结束):
请输入联系人电话:123456
请选择电话类型(1.移动电话  2.固定电话):1
请输入联系人电话:-1
----------添加联系人成功----------
bash 复制代码
# 编译 read 程序
g++ read.cc contacts.pb.cc -o read_contacts -lprotobuf
# 运行 read 程序
./read_contacts test.data

运行输出:

bash 复制代码
----------联系人1----------
姓名:李四
年龄:25
电话1:123456(MP)

注:这里打印MP是因为设置了默认值,导致取了枚举的第一个值。

4. Any 类型

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

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

bash 复制代码
hyb@192-159-150-152:~/project/protobuf/contact$ cd /usr/local/protobuf/include/google/protobuf/
hyb@192-159-150-152:/usr/local/protobuf/include/google/protobuf$ ls
any.pb.h                  descriptor.proto          map_field.h
any.pb.cc                 descriptor.pb.h           map_field.hpp
any.proto                 descriptor.pb.cc          map_field.inl
api.pb.h                  descriptor_set.h          map_field_lite.h
api.pb.cc                 descriptor_set.inl        map.h
api.proto                 field_access_listener.h   map_type_handler.h
struct.pb.h               field_mask.pb.h           message.h
struct.proto              field_mask.pb.cc          message_lite.h
arena.h                   field_mask.proto          metadata.h
arena_impl.h              generated_enum_reflection.h metadata_lite.h
arena_port.h              generated_enum_util.h     parse_context.h
test_arith.h              generated_message_bases.h  port_def.inc
test_arith.pb.h           generated_message_reflection.h port.h
test_arith.proto          generated_message_table_driven.h port_undef.inc
test_simple.h             generated_message_table_driven_impl.h reflection.h
test_simple.pb.h          generated_message_util.h   reflection_internal.h
test_simple.proto         has_bits.h                 reflection_ops.h
compiler                  implicit_weak_message.h   repeated_field.h
type.pb.h                 inline_string_field.h     repeated_ptr_field.h
descriptor_database.h     io
descriptor.h              map_entry.h
unknown_field_set.h
util
duration.pb.h
duration.pb.cc
duration.proto
wire_format.h
wire_format_lite.h
wrappers.pb.h
wrappers.pb.cc
wrappers.proto
dynamic_message.h
empty.pb.h
empty.pb.cc
empty.proto
4.1 升级通讯录至 2.2 版本

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

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

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

import "google/protobuf/any.proto"; // 引入 any.proto 文件

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

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

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

  repeated Phone phone = 3; // 电话
  google.protobuf.Any data = 4;
}

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

执行编译命令:protoc -cpp_out=. contacts.proto

4.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();
  ~Address();

  // string home_address = 1;
  void clear_home_address();
  const std::string& home_address() const;
  template <typename Arg0 = const std::string&, typename... ArgT>
  void set_home_address(Arg0&& arg0, ArgT&&... args);
  void set_allocated_home_address(std::string* home_address);
  PROTOBUF_NODISCARD std::string* release_home_address();

  // string unit_address = 2;
  void clear_unit_address();
  const std::string& unit_address() const;
  template <typename Arg0 = const std::string&, typename... ArgT>
  void set_unit_address(Arg0&& arg0, ArgT&&... args);
  void set_allocated_unit_address(std::string* unit_address);
  PROTOBUF_NODISCARD std::string* release_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 GetTypeName().empty() || any_metadata_.Is<T>();
  }
};

解释

  • 使用 PackFrom() 方法可以将任意消息类型转为 Any 类型。
  • 使用 UnpackTo() 方法可以将 Any 类型转回之前设置的任意消息类型。
  • 使用 Is<T>() 方法可以用来判断所存放的消息类型是否为 typename T
更新 write.cc(通讯录 2.2)
cpp 复制代码
#include <iostream>
#include <fstream>
#include "contacts.pb.h"
using namespace std;
using namespace contacts;

/*
 * 添加联系人
 */
void AddPeopleInfo(PeopleInfo* people_info_ptr)
{
    cout << "----------添加联系人----------" << endl;
    cout << "请输入联系人姓名:";
    string name;
    cin >> name;
    people_info_ptr->set_name(name);

    cout << "请输入联系人年龄:";
    int age;
    cin >> age;
    people_info_ptr->set_age(age);

    for (int i = 1; ; i++) {
        cout << "请输入联系人电话" << i << "(输入-1 结束):";
        string number;
        cin >> number;
        if (number.empty()) {
            break;
        }
        PeopleInfo::Phone* phone = people_info_ptr->add_phone();
        phone->set_number(number);

        cout << "请选择电话类型(1.移动电话  2.固定电话):";
        int type;
        cin >> type;
        switch (type) {
            case 1:
                phone->set_type(PeopleInfo::Phone::PhoneType::MP);
                break;
            case 2:
                phone->set_type(PeopleInfo::Phone::PhoneType::TEL);
                break;
            default:
                cout << "非法选项,使用默认值!" << endl;
                break;
        }
    }

    Address address;
    cout << "请输入联系人家庭地址:";
    string home_address;
    getline(cin, home_address);
    address.set_home_address(home_address);

    cout << "请输入联系人单位地址:";
    string unit_address;
    getline(cin, unit_address);
    address.set_unit_address(unit_address);

    google::protobuf::Any* data = people_info_ptr->mutable_data();
    data->PackFrom(address);

    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;

    // 先读取已存在的通讯录
    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;
        return -1;
    }

    // 添加一个联系人
    AddPeopleInfo(contacts.add_contacts());

    // 再把通讯录写入到文件中
    fstream output(argv[1], ios::out | ios::trunc | ios::binary);
    if (!contacts.SerializeToOstream(&output)) {
        cerr << "Failed to write contacts." << endl;
        return -1;
    }

    output.close();
    input.close();
    return 0;
}
更新 read.cc(通讯录 2.2)
cpp 复制代码
#include <iostream>
#include <fstream>
#include "contacts.pb.h"
using namespace std;
using namespace contacts;

/*
 * 打印联系人列表
 */
void PrintContacts(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;

        for (int j = 0; j < people.phone_size(); ++j) {
            const PeopleInfo::Phone& phone = people.phone(j);
            cout << "电话" << j+1 << ":" << phone.number() << "(" 
                 << PeopleInfo::Phone::PhoneType_Name(phone.type()) 
                 << ")" << endl;
        }

        if (people.has_data() && people.data().Is<Address>()) {
            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(int argc, char* argv[])
{
    GOOGLE_PROTOBUF_VERIFY_VERSION;

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

    // 读取通讯录
    Contacts contacts;
    fstream input(argv[1], ios::in | ios::binary);
    if (!contacts.ParseFromIstream(&input)) {
        cerr << "Failed to parse contacts." << endl;
        return -1;
    }

    // 打印通讯录
    PrintContacts(contacts);

    input.close();
    google::protobuf::ShutdownProtobufLibrary();
    return 0;
}

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

bash 复制代码
# 编译 write 程序
g++ write.cc contacts.pb.cc -o write_contacts -lprotobuf
# 运行 write 程序
./write_contacts test.data

运行输出:

bash 复制代码
----------添加联系人----------
请输入联系人姓名:王五
请输入联系人年龄:40
请输入联系人电话1(输入-1 结束):123456
请选择电话类型(1.移动电话  2.固定电话):1
请输入联系人电话2(输入-1 结束):654321
请选择电话类型(1.移动电话  2.固定电话):2
请输入联系人电话3(输入-1 结束):-1
请输入联系人家庭地址:陕西省西安市
请输入联系人单位地址:陕西省西安市
----------添加联系人成功----------
bash 复制代码
# 编译 read 程序
g++ read.cc contacts.pb.cc -o read_contacts -lprotobuf
# 运行 read 程序
./read_contacts test.data

运行输出:

bash 复制代码
----------联系人1----------
姓名:王五
年龄:40
电话1:123456(MP)
电话2:654321(TEL)
家庭地址:陕西省西安市
单位地址:陕西省西安市

5. oneof 类型

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

5.1 升级通讯录至 2.3 版本

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

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

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

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

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

注意:

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

执行编译命令:protoc -cpp_out=. contacts.proto

5.2 contacts.pb.h更新的部分代码展示:
cpp 复制代码
// 类 PeopleInfo
class PeopleInfo final : public ::PROTOBUF_NAMESPACE_ID::Message {
 public:
  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 Arg0 = const std::string&, typename... ArgT>
  void set_qq(Arg0&& 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 Arg0 = const std::string&, typename... ArgT>
  void set_weixin(Arg0&& 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方法。
更新 write.cc(通讯录 2.3),更新内容如下:
cpp 复制代码
#include <iostream>
#include <fstream>
#include "contacts.pb.h"
using namespace std;
using namespace 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 << "请选择电话类型(1.移动电话  2.固定电话):";
        int type;
        cin >> type;
        cin.ignore(256, '\n');
        switch (type) {
            case 1:
                phone->set_type(PeopleInfo::Phone::PhoneType::MP);
                break;
            case 2:
                phone->set_type(PeopleInfo::Phone::PhoneType::TEL);
                break;
            default:
                cout << "非法选项,使用默认值!" << endl;
                break;
        }
    }

    Address address;
    cout << "请输入联系人家庭地址:";
    string home_address;
    getline(cin, home_address);
    address.set_home_address(home_address);

    cout << "请输入联系人单位地址:";
    string unit_address;
    getline(cin, unit_address);
    address.set_unit_address(unit_address);

    google::protobuf::Any* data = people_info_ptr->mutable_data();
    data->PackFrom(address);

    cout << "请选择一个其他联系方式(1.QQ号  2.微信号):";
    int other_contact;
    cin >> other_contact;
    cin.ignore(256, '\n');
    if (other_contact == 1) {
        cout << "请输入QQ号:";
        string qq;
        getline(cin, qq);
        people_info_ptr->set_qq(qq);
    } else if (other_contact == 2) {
        cout << "请输入微信号:";
        string weixin;
        getline(cin, weixin);
        people_info_ptr->set_weixin(weixin);
    } else {
        cout << "非法选择,该设置失效!" << endl;
    }

    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;

    // 先读取已存在的通讯录
    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;
        return -1;
    }

    // 添加一个联系人
    AddPeopleInfo(contacts.add_contacts());

    // 再把通讯录写入到文件中
    fstream output(argv[1], ios::out | ios::trunc | ios::binary);
    if (!contacts.SerializeToOstream(&output)) {
        cerr << "Failed to write contacts." << endl;
        return -1;
    }

    output.close();
    input.close();
    return 0;
}
更新 read.cc(通讯录 2.3),更新内容如下:
cpp 复制代码
#include <iostream>
#include <fstream>
#include "contacts.pb.h"
using namespace std;
using namespace contacts;

/*
 * 打印联系人列表
 */
void PrintContacts(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;

        for (int j = 0; j < people.phone_size(); ++j) {
            const PeopleInfo::Phone& phone = people.phone(j);
            cout << "电话" << j+1 << ":" << phone.number() << "(" 
                 << PeopleInfo::Phone::PhoneType_Name(phone.type()) 
                 << ")" << endl;
        }

        if (people.has_data() && people.data().Is<Address>()) {
            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;
            }
        }

        switch (people.other_contact_case()) {
            case PeopleInfo::OtherContactCase::kQq:
                cout << "QQ号:" << people.qq() << endl;
                break;
            case PeopleInfo::OtherContactCase::kWeixin:
                cout << "微信号:" << people.weixin() << endl;
                break;
            case PeopleInfo::OtherContactCase::OTHER_CONTACT_NOT_SET:
                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;
    fstream input(argv[1], ios::in | ios::binary);
    if (!contacts.ParseFromIstream(&input)) {
        cerr << "Failed to parse contacts." << endl;
        return -1;
    }

    // 打印通讯录
    PrintContacts(contacts);

    input.close();
    google::protobuf::ShutdownProtobufLibrary();
    return 0;
}

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

bash 复制代码
# 编译 write 程序
g++ write.cc contacts.pb.cc -o write_contacts -lprotobuf
# 运行 write 程序
./write_contacts test.data

运行输出:

bash 复制代码
----------新增联系人----------
请输入联系人姓名:赵六
请输入联系人年龄:38
请输入联系人电话1(只输入回车完成电话新增):171
请选择电话类型(1.移动电话  2.固定电话):1
请输入联系人电话2(只输入回车完成电话新增):
请输入联系人家庭地址:北京市
请输入联系人单位地址:北京市
请选择一个其他联系方式(1.QQ号  2.微信号):2
请输入微信号:guo_liu
----------添加联系人成功----------
cpp 复制代码
# 编译 read 程序
g++ read.cc contacts.pb.cc -o read_contacts -lprotobuf
# 运行 read 程序
./read_contacts test.data

运行输出:

bash 复制代码
----------联系人1----------
姓名:赵六
年龄:38
电话1:171(MP)
家庭地址:北京市
单位地址:北京市
微信号:guo_liu

6. map 类型

语法支持创建一个关联映射字段,也就是可以使用 map 类型去声明字段类型,格式为:map<key_type, value_type> map_field = N;

要注意的是:

  • key_type 是除了 float 和 bytes 类型以外的任意标量类型。value_type 可以是任意类型。
  • map 字段不可以用repeated修饰。
  • map 中存入的元素是无序的。
6.1 升级通讯录至 2.4 版本

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

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

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

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; // 备注
}

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

执行编译命令:protoc -cpp_out=. contacts.proto

6.2 contacts.pb.h更新的部分代码展示:
cpp 复制代码
// 类 PeopleInfo
class PeopleInfo final : public ::PROTOBUF_NAMESPACE_ID::Message {
 public:
  // 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 类型的字段:

  • 清空 map:clear_方法。
  • 设置和获取:获取方法的方法名称与小写字段名完全相同;设置方法为mutable_方法,返回值为 Map 类型的指针,这类方法会为我们开辟好空间,可以直接对这块空间的内容进行修改。
更新 write.cc(通讯录 2.4),更新内容如下:
cpp 复制代码
#include <iostream>
#include <fstream>
#include "contacts.pb.h"
using namespace std;
using namespace 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 << "选择此电话类型(1、移动电话  2、固定电话):";
        int type;
        cin >> type;
        cin.ignore(256, '\n');
        switch (type) {
            case 1:
                phone->set_type(PeopleInfo::Phone::PhoneType::MP);
                break;
            case 2:
                phone->set_type(PeopleInfo::Phone::PhoneType::TEL);
                break;
            default:
                cout << "非法选择,使用默认值!" << endl;
                break;
        }
    }

    Address address;
    cout << "请输入联系人家庭地址:";
    string home_address;
    getline(cin, home_address);
    address.set_home_address(home_address);

    cout << "请输入联系人单位地址:";
    string unit_address;
    getline(cin, unit_address);
    address.set_unit_address(unit_address);

    google::protobuf::Any* data = people_info_ptr->mutable_data();
    data->PackFrom(address);

    cout << "选择添加一个其他联系方式(1、qq号  2、微信号):";
    int other_contact;
    cin >> other_contact;
    cin.ignore(256, '\n');
    if (1 == other_contact) {
        cout << "请输入qq号:";
        string qq;
        getline(cin, qq);
        people_info_ptr->set_qq(qq);
    } else if (2 == other_contact) {
        cout << "请输入微信号:";
        string weixin;
        getline(cin, weixin);
        people_info_ptr->set_weixin(weixin);
    } else {
        cout << "非法选择,该设置失效!" << endl;
    }

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

    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;

    // 先读取已存在的通讯录
    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;
        return -1;
    }

    // 添加一个联系人
    AddPeopleInfo(contacts.add_contacts());

    // 再把通讯录写入到文件中
    fstream output(argv[1], ios::out | ios::trunc | ios::binary);
    if (!contacts.SerializeToOstream(&output)) {
        cerr << "Failed to write contacts." << endl;
        return -1;
    }

    output.close();
    input.close();
    return 0;
}
更新 read.cc(通讯录 2.4),更新内容如下:
cpp 复制代码
#include <iostream>
#include <fstream>
#include "contacts.pb.h"
using namespace std;
using namespace contacts;

/*
 * 打印联系人列表
 */
void PrintContacts(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 auto& phone : people.phone()) {
            cout << "电话" << j++ << ":" << phone.number() << "(" 
                 << PeopleInfo::Phone::PhoneType_Name(phone.type()) 
                 << ")" << endl;
        }

        if (people.has_data() && people.data().Is<Address>()) {
            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;
            }
        }

        switch (people.other_contact_case()) {
            case PeopleInfo::OtherContactCase::kQq:
                cout << "qq号:" << people.qq() << endl;
                break;
            case PeopleInfo::OtherContactCase::kWeixin:
                cout << "微信号:" << people.weixin() << endl;
                break;
            case PeopleInfo::OtherContactCase::OTHER_CONTACT_NOT_SET:
                break;
        }

        if (people.remark_size() > 0) {
            cout << "备注信息:" << endl;
            for (auto it = people.remark().cbegin(); it != people.remark().cend(); ++it) {
                cout << "\t" << it->first << ":" << it->second << 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;
    fstream input(argv[1], ios::in | ios::binary);
    if (!contacts.ParseFromIstream(&input)) {
        cerr << "Failed to parse contacts." << endl;
        return -1;
    }

    // 打印通讯录
    PrintContacts(contacts);

    input.close();
    google::protobuf::ShutdownProtobufLibrary();
    return 0;
}

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

bash 复制代码
# 编译 write 程序
g++ write.cc contacts.pb.cc -o write_contacts -lprotobuf
# 运行 write 程序
./write_contacts test.data

运行输出:

bash 复制代码
----------新增联系人----------
请输入联系人姓名:赵七
请输入联系人年龄:28
请输入联系人电话1(只输入回车完成电话新增):123123
选择此电话类型(1、移动电话  2、固定电话):2
请输入联系人电话2(只输入回车完成电话新增):
请输入联系人家庭地址:海南海口
请输入联系人单位地址:海南海口
选择添加一个其他联系方式(1、qq号  2、微信号):1
请输入qq号:123123
请输入备注1标题(只输入回车完成备注新增):日程
请输入备注1内容:10月1一起出去玩
请输入备注2标题(只输入回车完成备注新增):
----------添加联系人成功----------
bash 复制代码
# 编译 read 程序
g++ read.cc contacts.pb.cc -o read_contacts -lprotobuf
# 运行 read 程序
./read_contacts test.data

运行输出:

bash 复制代码
----------联系人1----------
姓名:赵七
年龄:28
电话1:123123(TEL)
家庭地址:海南海口
单位地址:海南海口
qq号:123123
备注信息:
	日程:10月1一起出去玩

到这里,我们的通讯录 2.x 就更新了全部内容啦。在这个过程中我们将通讯录升级到了 2.4 版本,同时对 Protobuf 的内容又更深的了解了一些。不过,也只是正常使用还是完全不够的。通过接下来的学习,我们就能更进一步了解到 Protobuf 深入的内容。

7. 默认值

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

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

需要注意的是:

proto3 对于标量数据类型,并没有生成 has_ 方法!

这就会造成一个问题:

我们不确定在这个二进制序列消息中,有没有一个字段是 c = 0?

8. 更新消息

8.1 更新规则

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

不要修改已存在字段的字段编号

如果删除了某个字段,不要再使用该字段的字段编号。可以给该字段的字段编号进行(reserved),以保证该字段不会被重新使用。不过建议删除/注释字段,保留字段编号。

保留字段的概念就在下面!

int32uint32int64uint64bool是完全兼容的。可以从这些类型中的一个改为另一个(例如,将int32类型改成int64类型),它们的解析方式是一样的。

仅当 "大数值类型→小数值类型" 且数值超出小类型取值范围时,才会发生截断(不安全);同范围 / 小→大类型转换不会截断(安全),bool 与整数互转也遵循此规则

sint32sint64相互兼容(截断),但与其他的整数类型不兼容。

stringbytes是兼容的(如果bytes是有效的 UTF-8 字节)。

因为 Protobuf 中 string 类型的语义是 "UTF-8 编码的字符串" ,所以只有当 bytes 存储的二进制数据是合法的 UTF-8 字节序列 时,bytesstring 才能正确解析为字符串;反之,stringbytes 时,会自动将 UTF-8 字符串编码为对应的字节流(天然是合法的 bytes)。

具体来说:

  • 当 bytes → string 时:Protobuf 会把 bytes 的二进制数据当作 UTF-8 编码来解码 。如果 bytes 不是有效的 UTF-8(比如包含乱码字节),解码会失败(不同语言表现不同:C++ 可能返回乱码,Java 可能抛异常)。示例:bytes 存的是 "abc" 的 UTF-8 字节 [0x61, 0x62, 0x63],转 string 后是 "abc";但如果 bytes 存的是无效 UTF-8 字节 [0xFF],转 string 会解析失败。

  • 当 string → bytes 时:Protobuf 会把 string 的 UTF-8 编码直接转成字节流,所以得到的 bytes 天然是合法的 UTF-8 字节,不存在兼容性问题。

简言之,string 和 bytes 的兼容是基于 "string 是 UTF-8 编码的字节序列" 这一语义 ,所以要求 bytes 必须是有效的 UTF-8,才能和 string 互相正确解析。

fixed32sfixed32兼容,fixed64sfixed64兼容。

enumint32uint32int64uint64兼容(注意值不匹配会被截断)。但要注意当反序列化消息时,根据语言采用不同的处理方式:例如,未知的proto3枚举类型会保存在消息中,但当被序列化后,该值会被转换为对应的整型数值;但如果被识别为其类型,会使用其值。

enum 的本质是 "带名字的整数" --- Protobuf 里的 enum 底层就是32 位整数 (默认基于 int32 实现)

proto3 不会丢弃 "未知的 enum 值",而是以整数形式保留,不同语言处理方式略有差异

  1. "未知的 proto3 枚举类型会保存在消息中"

假设服务 A 的 proto 定义:

cpp 复制代码
enum PhoneType {
  MP = 0;
  TEL = 1;
}

服务 B 的 proto 新增了枚举值:

cpp 复制代码
enum PhoneType {
  MP = 0;
  TEL = 1;
  WORK = 2; // 服务A不认识的新值
}

当服务 B 把 WORK(值为 2)序列化发给服务 A 时:

  • 服务 A 不认识 "2" 对应的枚举名,但不会丢弃这个值,而是把 "2" 以整数形式保存在消息的 "未知字段" 里;(后面马上就会谈到!)
  • 服务 A 再把这个消息序列化发出去时,"2" 会被原样保留(不会丢失)。
  1. "序列化后转换为对应的整型数值;被识别为其类型则用其值"
  • 场景 1(服务 A 解析服务 B 的消息):服务 A 不认识 WORK=2,所以解析时只会拿到整数 2(而非枚举名),但这个 2 会被保留;
  • 场景 2(服务 B 解析自己的消息):服务 B 认识 WORK=2,所以解析时会直接拿到枚举名 WORK(而非单纯的整数 2);
  • 场景 3(服务 A 把含 2 的消息发给服务 B):服务 B 能识别 2 对应 WORK,所以会解析为枚举名 WORK。

oneof

  1. 将非 oneof 字段更新为 oneof 类型成员之一是安全和二进制兼容的。(原本独立的普通字段(不在任何 oneof 里),把它移到一个新创建的 oneof 里作为成员,对旧代码(没更新 proto 的代码)完全无影响,二进制数据也能正常解析。)

旧 proto(v1)

cpp 复制代码
message PeopleInfo {
  string name = 1;
  string qq = 2; // 普通字段,非oneof
}

新 proto(v2)

cpp 复制代码
message PeopleInfo {
  string name = 1;
  oneof other_contact { // 新增oneof
    string qq = 2; // 把普通字段移进新oneof
  }
}

为什么安全呢?

  • 旧代码(基于 v1):依然能正常读写 qq 字段,感知不到 oneof 的存在,二进制数据解析和原来完全一致;
  • 新代码(基于 v2):把 qq 当作 oneof 成员使用,设置 / 读取逻辑也完全兼容旧数据(旧数据里的 qq 值会被新代码识别为 oneof 的当前值);
  • 二进制层面:qq 的字段编号还是 2,编码 / 解码规则没变,完全兼容。
  1. 若肯定没有代码设置该字段值,将该字段移入一个新 oneof 类型也是可行的。(如果某个普通字段从未被任何代码赋值过(字段值一直是默认值,比如 string 默认空、int 默认 0),把它移到新 oneof 里也没问题;但如果该字段已有真实值,这么做就可能丢数据。)

旧 proto(v1)

cpp 复制代码
message PeopleInfo {
  string name = 1;
  string weixin = 3; // 普通字段,从未被任何代码设置过值
}

新 proto(v2)

cpp 复制代码
message PeopleInfo {
  string name = 1;
  oneof other_contact {
    string qq = 2;
    string weixin = 3; // 移进新oneof(从未被赋值过)
  }
}

为什么可行?

因为该字段原本就没有真实值(只有默认值),移进 oneof 后,旧代码读取时依然是默认值,新代码也不会读到无效数据;但如果 weixin 字段已有真实值(比如旧代码设置过 weixin="abc"),移进 oneof 后,旧代码仍能读,但新代码若设置了 qq,会清空 weixin,导致旧数据丢失。

  1. 将同字段移入已存在的 oneof 类型是不安全的。(如果一个普通字段,要移到已经存在的 oneof 里(这个 oneof 已有其他字段,且可能被设置过值),会导致数据冲突、丢失,二进制兼容被破坏。)

旧 proto(v1)

cpp 复制代码
message PeopleInfo {
  string name = 1;
  string qq = 2; // 普通字段
  oneof other_contact {
    string weixin = 3; // 已存在的oneof,可能被设置过值
  }
}

新 proto(v2)

cpp 复制代码
message PeopleInfo {
  string name = 1;
  oneof other_contact {
    string weixin = 3;
    string qq = 2; // 把普通字段移进已存在的oneof
  }
}

为什么不安全?

假设旧代码同时设置了 qq="123" 和 weixin="456":

  • 旧 proto(v1):两个字段是独立的,序列化后会同时保存 qq=123 和 weixin=456;
  • 新 proto(v2):qq 和 weixin 互斥,反序列化时只会保留最后设置的那个值(比如先设 qq、后设 weixin,就只剩 weixin),导致另一个值丢失;
  • 更严重的是:旧代码写的 "同时有 qq 和 weixin 值" 的二进制数据,被新代码解析后会丢失一个值,数据一致性被破坏,这就是 "不安全" 的核心。
8.2 保留字reserved

如果通过删除或注释字段来更新消息类型,未来的用户在添加新字段时,有可能会使用以前已经使用,但已经被删除或注释掉的字段编号。将使用.proto 的旧版本的程序序列发消息时,就会出现解析错误等。我们会发现解决该问题的一种方法是:使用reserved将字段的编号或名称设置为保留项。当我们使用这些编号或名称时,protocol buffer 的编译器将会把这些编号或名称不可用。举个例子:

cpp 复制代码
message Message {
  reserved 16, 17;
  reserved 100 to 199;
  reserved "foo", "bar";
  // string foo = 16; // 被删除,字段编号和名称被reserved
  // string bar = 17; // 被删除,字段编号和名称被reserved
  int32 field1 = 1; // field1 foo2
  int32 field2 = 2; // field2 bar2
  int32 field3 = 182; // field3 name
}

reserved 后可跟 字段编号字段编号范围字段名称,但有严格的语法限制:

类型 格式示例 说明
单个字段编号 reserved 2; 保留编号 2,不能再用于任何字段
编号范围 reserved 10 to 20; 保留 10~20(含)的所有编号,范围用 to(小写)
多个编号 / 范围 reserved 5, 8 to 15; 同时保留单个编号 5 + 范围 8~15
字段名称 reserved "name", "age"; 保留名称 "name" 和 "age",不能再用作字段名

注意:

  • 编号和名称不能写在同一条 reserved 语句里 :必须分开写!
    ❌ 错误:reserved 2, "age";(同时写编号和名称)
    ✅ 正确:

    cpp 复制代码
    reserved 2; // 单独写编号
    reserved "age"; // 单独写名称
  • 编号范围的 to 必须小写 :不能用 TO/To,否则编译器报错。

  • 编号必须是正整数:且符合 Protobuf 字段编号规则(1~2²⁹-1,其中 19000~19999 是保留段)。

  • 名称必须用双引号包裹:单引号、无引号都不行,且区分大小写(比如 "Age" 和 "age" 是两个不同名称)。

  • reserved 作用域是当前 message:只约束当前消息内的字段,嵌套消息 / 其他消息不受影响。

8.2.1 创建通讯 3.0 版本 ------ 验证删除字段造成的数据损坏

现有两个服务,他们各自使用一份通讯录.proto 文件,内容约定好了是一模一样的。

  • 服务(service):负责序列化通讯录对象,并写入文件中。
  • 服务 2(client):负责读取文件中的数据,解析并打印。

服务之间的通讯.proto 文件是一致的。现在,服务 1 删除了某个字段,并新增了一个字段,新增的字段使用了被删除字段的字段编号。并将新的序列化对象写进了文件。但 client 并没有更新自己的 proto 文件。根据推论,可能会出现数据损坏的现象,接下来就让我们来验证这个推论。

新增两个目录:serviceclient,分别存放两个服务的代码。

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

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

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

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

message Contacts {
  repeated PeopleInfo contacts = 1;
}

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

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

// 联系人
message PeopleInfo {
  string name = 1; // 姓名
  int32 age = 2; // 年龄
  int32 birthday = 4; // 出生日期(新增字段)

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

message Contacts {
  repeated PeopleInfo contacts = 1;
}

分别对两个文件进行编译,自行操作。

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

cpp 复制代码
#include <iostream>
#include <fstream>
#include "contacts.pb.h"
using namespace std;
using namespace 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 << "(输入-1完成电话新增):";
    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;
    return -1;
  }
  input.close();

  // 添加一个联系人
  AddPeopleInfo(contacts.add_contacts());

  // 再把通讯录写入到文件中
  fstream output(argv[1], ios::out | ios::trunc | ios::binary);
  if (!contacts.SerializeToOstream(&output)) {
    cerr << "Failed to write contacts." << endl;
    return -1;
  }

  output.close();
  google::protobuf::ShutdownProtobufLibrary();
  return 0;
}

service 目录下新增Makefile

bash 复制代码
service:service.cc contacts.pb.cc
	g++ $^ -o $@ -lprotobuf
.PHONY:clean
clean:
	rm -f service

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

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

// 打印联系人列表
void PrintContacts(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;

    for (const auto& phone : people.phone()) {
      cout << "电话:" << 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;
  fstream input(argv[1], ios::in | ios::binary);
  if (!contacts.ParseFromIstream(&input)) {
    cerr << "Failed to parse contacts." << endl;
    return -1;
  }
  input.close();

  // 打印通讯录
  PrintContacts(contacts);

  google::protobuf::ShutdownProtobufLibrary();
  return 0;
}

client 目录下新增Makefile

cpp 复制代码
client:client.cc contacts.pb.cc
	g++ $^ -o $@ -lprotobuf
.PHONY:clean
clean:
	rm -f client

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

bash 复制代码
hyb@192-159-150-152:~/project/protobuf/update/service$ ./service ../contacts.bin
----------新增联系人----------
请输入联系人姓名:张珊
请输入联系人年龄:22
请输入联系人电话1(输入-1完成电话新增):131
请输入联系人电话2(输入-1完成电话新增):
----------添加联系人成功----------

hyb@192-159-150-152:~/project/protobuf/update/client$ ./client ../contacts.bin
----------联系人1----------
姓名:张珊
年龄:22
电话:131

确定无误后,对service目录下的contact.proto文件进行更新:删除age字段,新增birthday字段,新增的字段使用被删除字段的字段编号。更新后的contact.proto(通讯录 3.0)内容如下:

cpp 复制代码
syntax = "proto3";
package 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 contacts;

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

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

  for (int i = 1; ; i++) {
    cout << "请输入联系人电话" << i << "(输入-1完成电话新增):";
    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[])
{
  // ...(main函数内容与之前一致,省略)
}

我们对client相关的代码都不进行更新。再进行一次读写(对service.cc编译过程,自行操作)。

cpp 复制代码
hyb@192-159-150-152:~/project/protobuf/update/service$ ./service ../contacts.bin
----------新增联系人----------
请输入联系人姓名:李四
请输入联系人出生日期:1990-1-1
请输入联系人电话1(输入-1完成电话新增):151
请输入联系人电话2(输入-1完成电话新增):
----------添加联系人成功----------

hyb@192-159-150-152:~/project/protobuf/update/client$ ./client ../contacts.bin
----------联系人1----------
姓名:张珊
年龄:22
电话:131
----------联系人2----------
姓名:李四
年龄:199011
电话:151

上面的结果中,我们发现 "李四" 的年龄变成了 "199011",这明显是不正确的。这是因为service给 "出生日期" 字段设置的值 "1990-1-1" 被client识别成了 "年龄" 字段的值,所以出现了乱码或者错误的字段值。要避免这个问题就需要不要使用被删除的字段编号,不要重复使用字段编号

正确的service目录下的contact.proto写法应该(给被删除字段编号)被设置为:

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

// 联系人
message PeopleInfo {
  reserved 2;
  string name = 1; // 姓名
  int32 birthday = 4; // 出生日期
  message Phone {
    string number = 1; // 电话号码
  }
  repeated Phone phone = 3; // 电话
}

message Contacts {
  repeated PeopleInfo contacts = 1;
}

proto文件修改后,还需要更新一下对应的service.cc程序,再进行一次读写,这样就不会出现上述的错误了。

cpp 复制代码
hyb@192-159-150-152:~/project/protobuf/update/service$ ./service ../contacts.bin
----------新增联系人----------
请输入联系人姓名:王五
请输入联系人出生日期:1112
请输入联系人电话1(输入-1完成电话新增):110
请输入联系人电话2(输入-1完成电话新增):
----------添加联系人成功----------

hyb@192-159-150-152:~/project/protobuf/update/client$ ./client ../contacts.bin
----------联系人1----------
姓名:张珊
年龄:22
电话:131
----------联系人2----------
姓名:李四
年龄:199011
电话:151
----------联系人3----------
姓名:王五
年龄:0
电话:110

这样一来,"王五" 的 "年龄" 字段使用了proto3的默认值 "0",这就是我们想要的结果。这也印证了我们的推论。

根据上面的例子,我们可能还有一个疑问:如果使用了reserved 2 了那么service给王五设置的生日"1112",client就没有办法读到了吗?

答案是可以的,继续学习下面的知识我们就可以知道啦。

8.3 未知字段

在通讯录 3.0 版本中,我们向service目录下的contacts.proto新增了'生日'字段,但对于client相关的代码并没有任何改动。验证后发现 新代码序列化的消息(service)也可以被旧代码(client)解析。并且这里要说的是,新增的'生日'字段在旧程序(client)中其实并没有丢失,而是会作为旧程序的未知字段。

  • 未知字段:解析结构良好的 protocol buffer 已序列化数据中的未识别字段的表示方式。例如,当旧程序解析带有新字段的数据时,这些新字段就会成为旧程序的未知字段。
  • 本来,proto3在解析消息时总是会丢弃未知字段,但在 3.5 版本中重新引入了对未知字段的保留机制。所以在 3.5 或更高版本中,未知字段在反序列化时会被保留,同时也会包含在序列化的结果中。(也就是3.5版本之前会被丢弃,之后会被保留)
8.3.1 未知字段从哪获取

了解相关类关系图

针对未知字段从哪里获取,不再是像我们之前定义的常规字一样的使用 Get 方式来获取未知字段,而是通过一些复杂的手段才可以拿得到的!

其实上图中,UnknownField 就是表示未知字段的类!

MessageLite 类介绍(了解)

  • MessageLite 从名字看是轻量级的 message仅仅提供序列化、反序列化功能。

  • 类定义在 google 提供的 message_lite.h 中。

Message 类介绍(了解)

  • 我们自己定义的 message 都是继承自 Message

  • Message 最重要的两个接口 GetDescriptor()/GetReflection(),可以获取该类型对应的 Descriptor 对象指针和 Reflection 对象指针。

  • 类定义在 google 提供的 message.h 中。

cpp 复制代码
const Descriptor* GetDescriptor() const;
const Reflection* GetReflection() const;

Descriptor 类介绍(了解)

  • Descriptor 是对 google 的 message 类型定义的描述,包括 message 的名字、所有字段的描述、原始的 .proto 文件内容。

  • 类定义在 google 提供的 descriptor.h 中。

cpp 复制代码
class PROTOBUF_EXPORT Descriptor : private internal::SymbolBase {
 public:
  const string& name() const;
  int field_count() const;
  const FieldDescriptor* field(int index) const;
  const FieldDescriptor* FindFieldByNumber(int number) const;
  const FieldDescriptor* FindFieldByName(const string& name) const;
  const FieldDescriptor* FindFieldByCamelCaseName(const string& name) const;
  const FieldDescriptor* field(int index) const;
  const FieldDescriptor* FindFieldByNumber(int number) const;
  const EnumDescriptor* enum_type(int index) const;
  const EnumDescriptor* FindEnumTypeByName(const string& name) const;
  const EnumValueDescriptor* FindEnumValueByName(const string& name) const;
};

Reflection 类介绍(了解)

  • Reflection 接口类,主要提供动态访问消息字段的接口,对消息对象的自动写主要通过这类完成。

  • 提供方法动态访问 / 修改 message 中的字段,对每种类型,Reflection 都提供了一个单独的接口用于读 / 写对应的值。

  • 对所有不同的标量类型 FieldDescriptor::TYPE_*,需要使用不同的 Get*()/Set*()/Add*() 接口;

  • repeated 类型字段,需要使用 GetRepeated*()/SetRepeated*() 接口,不可以和 Repeated 类型接口混用;

  • message 对象可以通过它自身的 reflection (message.GetReflection()) 来操作类中包含了访问 / 修改未知字段的方法。

  • 类定义在 google 提供的 message.h 中。

也就是读写字段的相关方法!

cpp 复制代码
class PROTOBUF_EXPORT Reflection final {
 public:
  const UnknownFieldSet& GetUnknownFields(const Message& message) const;
  void SetUnknownFields(Message* message, const UnknownFieldSet& fields);
  bool HasField(const Message& message, const FieldDescriptor* field) const;
  void ClearField(Message* message, const FieldDescriptor* field) const;
  void SetField(Message* message, const FieldDescriptor* field,
                const Message& value) 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
  int32 GetInt32(const Message& message, const FieldDescriptor* field) const;
  int64 GetInt64(const Message& message, const FieldDescriptor* field) const;
  uint32 GetUInt32(const Message& message, const FieldDescriptor* field) const;
  uint64 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,
                            const MessageFactory* factory = nullptr) const;

  // Singular field mutators
  void SetInt32(Message* message, const FieldDescriptor* field,
                int32 value) const;
  void SetInt64(Message* message, const FieldDescriptor* field,
                int64 value) const;
  void SetUInt32(Message* message, const FieldDescriptor* field,
                 uint32 value) const;
  void SetUInt64(Message* message, const FieldDescriptor* field,
                 uint64 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,
                 const 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,
                          const MessageFactory* factory = nullptr) const;
  Message* ReleaseMessage(Message* message, const FieldDescriptor* field,
                          const MessageFactory* factory = nullptr) const;

  // Repeated field getters
  int FieldSize(const Message& message, const FieldDescriptor* field) const;
  int32 GetRepeatedInt32(const Message& message, const FieldDescriptor* field,
                         int index) const;
  int64 GetRepeatedInt64(const Message& message, const FieldDescriptor* field,
                         int index) const;
  uint32 GetRepeatedUInt32(const Message& message, const FieldDescriptor* field,
                           int index) const;
  uint64 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
  void SetRepeatedInt32(Message* message, const FieldDescriptor* field,
                        int index, int32 value) const;
  void SetRepeatedInt64(Message* message, const FieldDescriptor* field,
                        int index, int64 value) const;
  void SetRepeatedUInt32(Message* message, const FieldDescriptor* field,
                         int index, uint32 value) const;
  void SetRepeatedUInt64(Message* message, const FieldDescriptor* field,
                         int index, uint64 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, const 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
  void AddInt32(Message* message, const FieldDescriptor* field,
                int32 value) const;
  void AddInt64(Message* message, const FieldDescriptor* field,
                int64 value) const;
  void AddUInt32(Message* message, const FieldDescriptor* field,
                 uint32 value) const;
  void AddUInt64(Message* message, const FieldDescriptor* field,
                 uint64 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,
                 const 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,
                      const MessageFactory* factory = nullptr) const;

  const FieldDescriptor* FindKnownExtensionByName(
      const std::string& name) const;
  const FieldDescriptor* FindKnownExtensionByNumber(int number) const;
  bool SupportsUnknownEnumValues() const;
};

UnknownFieldSet 类介绍(了解)

const UnknownFieldSet& GetUnknownFields(const Message& message) const; 由此返回!

  • UnknownFieldSet 包含在解析消息但型但未由其类型定义的所有字段。

  • 若要将 UnknownFieldSet 附加到任何消息,请调用 Reflection::SetUnknownFields()

  • 类定义在 unknown_field_set.h 中。

cpp 复制代码
class PROTOBUF_EXPORT UnknownFieldSet {
 public:
  UnknownFieldSet();
  ~UnknownFieldSet();

  void Clear();
  void MergeFrom(const UnknownFieldSet& other);
  int field_count() const;
  const UnknownField& field(int index) const;
  UnknownField* mutable_field(int index);

  // Add fields
  void AddVarint(int number, uint64 value);
  void AddFixed32(int number, uint32 value);
  void AddFixed64(int number, uint64 value);
  void AddLengthDelimited(int number, const std::string& value);
  void AddGroup(int number, UnknownFieldSet* value);
  UnknownField* AddField(int number);

  // Parsing/serialization
  bool ParseFromCodedStream(CodedInputStream* input);
  bool ParsePartialFromCodedStream(CodedInputStream* input);
  bool ParseFromZeroCopyStream(ZeroCopyInputStream* input);
  bool ParsePartialFromZeroCopyStream(ZeroCopyInputStream* input);

  bool ParseFromString(const std::string& data) {
    return ParseFromArray(data.data(), static_cast<int>(data.size()));
  }
  bool ParseFromArray(const void* data, int size);
  bool SerializeToString(std::string* output) const;
  bool SerializeToCodedStream(CodedOutputStream* output) const;
  bool SerializePartialToCodedStream(CodedOutputStream* output) const;
};

UnknownField 类介绍(重要)

  • 表示一个未知的字段。通过上者 field(int) 方式获取!

  • 类定义在 unknown_field_set.h 中。

cpp 复制代码
class PROTOBUF_EXPORT UnknownField {
 public:
  enum Type {
    TYPE_VARINT,
    TYPE_FIXED32,
    TYPE_FIXED64,
    TYPE_LENGTH_DELIMITED,
    TYPE_GROUP
  };

  int number() const; // 返回位置字段的编号
  int type() const; // 返回枚举常量

  // Accessors for the value of the field, depending on type.
  // 和上面的 type() 返回是一一对应的!!!注意!!! 
  uint64 varint() const;
  uint32 fixed32() const;
  uint64 fixed64() const;
  const std::string& length_delimited() const;
  const UnknownFieldSet& group() const;

  // Mutators
  // 和上面的 type() 返回是一一对应的!!!注意!!!
  void set_varint(uint64 value);
  void set_fixed32(uint32 value);
  void set_fixed64(uint64 value);
  void set_length_delimited(const std::string& value);
  std::string* mutable_length_delimited();
};
8.3.2 升级通讯录至 3.1 版本 ------ 验证未知字段

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

cpp 复制代码
#include <iostream>
#include <fstream>
#include "google/protobuf/unknown_field_set.h"
#include "contacts.pb.h"
using namespace std;
using namespace contacts;
using namespace google::protobuf;

// 打印联系人列表
void PrintContacts(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;

    for (const auto& phone : people.phone()) {
      cout << "电话:" << phone.number() << endl;
    }

    // 获取未知字段
    const Reflection* reflection = people::GetReflection(); // 静态方法
    const UnknownFieldSet& unknown_fields = reflection->GetUnknownFields(people);
    cout << "未知字段数量:" << unknown_fields.field_count() << endl;
    for (int j = 0; j < unknown_fields.field_count(); ++j) {
      const UnknownField& unknown_field = unknown_fields.field(j);
      cout << "未知字段编号:" << unknown_field.number() << endl;
      cout << "未知字段类型:" << unknown_field.type() << endl;
      switch (unknown_field.type()) {
        case UnknownField::TYPE_VARINT:
          cout << "未知字段值:" << unknown_field.varint() << endl;
          break;
        case UnknownField::TYPE_LENGTH_DELIMITED:
          cout << "未知字段值:" << unknown_field.length_delimited() << 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;
  fstream input(argv[1], ios::in | ios::binary);
  if (!contacts.ParseFromIstream(&input)) {
    cerr << "Failed to parse contacts." << endl;
    return -1;
  }
  input.close();

  // 打印通讯录
  PrintContacts(contacts);

  google::protobuf::ShutdownProtobufLibrary();
  return 0;
}

其他文件不用做任何修改,重新对 client.cc 进行一次编译,得到如下结果:

cpp 复制代码
hyb@192-159-150-152:~/project/protobuf/update/client$ ./client ../contacts.bin
----------联系人1----------
姓名:张珊
年龄:22
电话:131
未知字段数量:0
----------联系人2----------
姓名:李四
年龄:199011
电话:151
未知字段数量:1
未知字段编号:2
未知字段类型:3
未知字段值:1990-1-1
----------联系人3----------
姓名:王五
年龄:0
电话:110
未知字段数量:1
未知字段编号:4
未知字段类型:3
未知字段值:1980-1-1

类型方向:在介绍 UnknownField 中提到了其中包含了未知字段的具体类型:

cpp 复制代码
enum Type {
  TYPE_VARINT,
  TYPE_FIXED32,
  TYPE_FIXED64,
  TYPE_LENGTH_DELIMITED,
  TYPE_GROUP
};

类型 3 对应的是 TYPE_LENGTH_DELIMITED

8.4 前后兼容性

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

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

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

9. 选项 option

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

9.1 选项分类

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

cpp 复制代码
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 消息中
...

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

9.2 常用选项列举

optimize_for :该选项为文件选项,可以设置 protoc 编译器的优化级别,分别为 SPEEDCODE_SIZELITE_RUNTIME。受该选项影响,设置不同的优化级别,编译.proto 文件后生成的代码内容不同。

  • SPEED :protoc 编译器将生成的代码是高度优化的,代码运行效率高,但是由此生成的代码编译后会占用更多的空间。SPEED 是默认选项。
  • CODE_SIZE :proto 编译器将生成最少的类,会占用更少的空间,是依赖基于反射的代码来实现序列化、反序列化和各种其他操作。但和 SPEED 恰恰相反,它的代码运行效率较低。这种方式适合用在包含大量的.proto 文件,但并不盲目追求速度的应用中。
  • LITE_RUNTIME :生成的代码执行效率高,同时生成代码编译后的所占用的空间也是非常少。这是以牺牲 Protocol Buffer 提供的反射功能为代价的,仅仅提供 encoding + 序列化功能,所以我们在链接 BP 库时仅需链接 libprotobuf-lite,而非 libprotobuf。这种模式通常用于资源有限的平台,例如移动手机平台中。
bash 复制代码
option optimize_for = LITE_RUNTIME;
  • allow_alias :允许将相同的常数值分配给不同的枚举常量,用来定义别名。该选项为枚举选项。举个例子:
cpp 复制代码
enum PhoneType {
  option allow_alias = true;
  MP = 0;
  TEL = 1;
  LANDLINE = 1; // 若不加 option allow_alias = true; 这一行会编译报错
}

对 TEL 起别名 --- LANDLINE

9.3 设置自定义选项

Protobuf 允许自定义选项并使用。该功能大部分场景用不到,在这里不拓展讲解。有兴趣可以参考:https://developers.google.cn/protocol-buffers/docs/proto?hl=zh-cn#customoptions

相关推荐
Algebraaaaa2 小时前
为什么线程阻塞要用.join而不是.wait
java·c++·python
墨雪不会编程2 小时前
C++内存管理深度剖析
java·开发语言·c++
KingRumn2 小时前
Linux进程间通信之消息队列(POSIX)
linux·服务器
laoliu19962 小时前
Odoo 18企业版源码 包含 部署教程
运维·服务器
万法若空2 小时前
【wxWidgets教程】控件基础知识
c++·gui·wxwidgets·事件处理
图形学爱好者_Wu2 小时前
每日一个C++知识点|模板
c++
开开心心就好3 小时前
免费卸载工具,可清理残留批量管理启动项
linux·运维·服务器·windows·随机森林·pdf·1024程序员节
xiaolang_8616_wjl3 小时前
c++超级细致的基本框架
开发语言·数据结构·c++·算法
Lbwnb丶3 小时前
检测服务器是否是虚拟化,如KVM,VM等
linux·运维·服务器