
ProtoBuf实战进阶时,项目迭代总绕不开小麻烦:新增字段后旧程序咋读?未赋值字段该用啥值?这些问题的答案,就藏在默认值、消息更新与兼容性里
本文便以实战为引,带你摸清这些进阶规则。
结语 摸清默认值的隐性规则,掌握消息更新的安全技巧,吃透兼容性的核心逻辑,你便给 ProtoBuf 秘语加了 “抗迭代” 的护盾。这些技巧不只是孤立方法,更是让秘语灵活升级又不打断服务的关键。而这,正是从 “会写 ProtoBuf” 到 “写稳 ProtoBuf” 的重要跨越。
反序列化消息时,如果被反序列化的⼆进制序列中不包含某个字段,反序列化对象中相应字段时,就会设置为该字段的默认值。不同的类型对应的默认值不同:
false 0 0repeated的字段的默认值是空的( 通常是相应语⾔的⼀个空列表 ) oneof字段 和 any字段 ,C++ 和 Java 语⾔中都有has_⽅法来检测当前字段是否被设置场景理解:has方法的作用

如果现有的消息类型已经不再满⾜我们的需求,例如,需要扩展⼀个字段,在不破坏任何现有代码的情况下更新消息类型⾮常简单。遵循如下规则即可:
int32, uint32, int64, uint64 和bool是完全兼容的。可以从这些类型中的⼀个改为另⼀个,⽽不破坏前后兼容性。若解析出来的数值与相应的类型不匹配,会采⽤与 C++ ⼀致的处理⽅案(例如,若将 64 位整数当做 32 位进⾏读取,它将被截断为 32 位)sint32 和sint64相互兼容但不与其他的整型兼容string 和 bytes 在合法UTF-8字节前提下也是兼容的
- bytes 包含消息编码版本的情况下,嵌套消息与bytes也是兼容的。fixed32 与 sfixed32 兼容,fixed64 与 sfixed64兼容enum 与 int32,uint32,int64和 uint64 兼容(注意若值不匹配会被截断)。但要注意当反序列化消息时会根据语⾔采⽤不同的处理⽅案:例如,未识别的proto3 枚举类型会被保存在消息中,但是当消息反序列化时如何表⽰是依赖于编程语⾔的。整型字段总是会保持其的值oneof:将⼀个单独的值更改为 新oneof类型成员之⼀是安全和⼆进制兼容的oneof类型也是可⾏的oneof 类型是不安全的。如果通过
删除或注释掉字段来更新消息类型,未来的⽤⼾在添加新字段时,有可能会使⽤以前已经存在,但已经被删除或注释掉的字段编号。将来使⽤该.proto的旧版本时的程序会引发很多问题:数据损坏、隐私错误等等
确保不会发⽣这种情况的⼀种⽅法是:使⽤ reserved 将指定字段的编号或名称设置为保留项 。当我们再使⽤这些编号或名称时,protocol buffer 的编译器将会警告这些编号或名称不可⽤。举个例子
message Message {
// 设置保留项
reserved 100, 101, 200 to 299;
reserved "field3", "field4";
// 注意:不要在⼀⾏ reserved 声明中同时声明字段编号和名称。
// reserved 102, "field5";
// 设置保留项之后,下⾯代码会告警
int32 field1 = 100; //告警:Field 'field1' uses reserved number 100
int32 field2 = 101; //告警:Field 'field2' uses reserved number 101
int32 field3 = 102; //告警:Field name 'field3' is reserved
int32 field4 = 103; //告警:Field name 'field4' is reserved
}现模拟有两个服务,他们各⾃使⽤⼀份通讯录.proto⽂件,内容约定好了是⼀模⼀样的。
⼀段时间后,service 更新了⾃⼰的 .proto ⽂件,更新内容为:删除了某个字段,并新增了⼀个字段,新增的字段使⽤了被删除字段的字段编号。并将新的序列化对象写进了⽂件。
但 client 并没有更新⾃⼰的 .proto ⽂件。根据结论,可能会出现数据损坏的现象,接下来就让我们来验证下这个结论。
新建两个⽬录:service、client。分别存放两个服务的代码。
service⽬录下新增contacts.proto(通讯录 3.0)
syntax = "proto3";
package s_contacts;
//联系人
message PeopleInfo{
reserver 2,10,11, 100 to 200;
reserver age;
string name = 1; //姓名
int32 age = 2; //年龄
message Phone{
string number = 1; //电话号码
}
repeated Phone phone = 3; //电话
}
//通讯录
message Contacts{
repeated PeopleInfo contacts = 1;
}
client目录下新增contacts.proto(通讯录 3.0)
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;
}继续对 service 目录下新增 service.cc (通讯录 3.0),负责向文件中写通讯录消息,内容如下:
#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(){
Contacts contacts;
//先读取已存在的 contacts
fstream input("../contact.bin", ios::in | ios::binary);
if (!input) {
cout << "contacts.bin not found. Creating a new file." << endl;
} else if (!contacts.ParseFromIstream(&input)) {
cerr << "Failed to parse contacts." << endl;
input.close();
return -1;
}
//新增一个联系人
AddpeopleInfo(contact.add_contacts());
//向磁盘文件写入信的contacts
fstream output("../contacts.bin", 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();
return 0;
}client 目录下新增 client.cc (通讯录 3.0),负责向读出文件中的通讯录消息,内容如下:
#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& unknown_field = set.field(j);
cout << "未知字段" << j+1 << ": "
<< " 编号:" << unknown_field.number();
switch(unknown_field.type()) {
case UnknownField::Type::TYPE_VARINT:
cout << " 值:" << unknown_field.varint() << endl;
break;
case UnknownField::Type::TYPE_LENGTH_DELIMITED:
cout << " 值:" << unknown_field.length_delimited() << endl;
break;
// case ...
}
}
}
}
int main(){
Contacts contacts;
//先读取已存在的 contacts
fstream input("../contact.bin", ios::in | ios::binary);
if(!contacts.ParseFromIstream(&input)){
cerr << "Failed to parse contacts." << endl;
input.close();
return -1;
}
// 打印 contacts
PrintfContacts(contacts);
input.close();
return 0;
}我们不能直接已删除一些老字段,如果非要这么做的话,将来在实现我们自己的业务代码的时候,会造成一些影响。
确认无误后,对service目录下的 contacts.proto 文件进行更新:删除 age 字段,新增 birthday 字段,新增的字段使用被删除字段的字段编号。

问题说明:
Protobuf 中,数据映射依赖“字段编号”而非字段名。反序列化时,只要编号一致,值就会被填充到对应编号的字段上。结论与规范:
reserved 保留已废弃的字段编号和名称,防止后续误用。deprecated=true(提醒后续开发者该字段已废弃)。 lint/CI/代码审查等手段校验 reserved 与编号复用风险,统一管理 schema 变更。reserve关键字,指定一批的字段编号变为保留下设定为保留项。如果我们要使用这个保留的字段,protobuffer编译器就会报警。

设置多个字段编号 to范围 100 to 200


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

未知字段:解析结构良好的 protocol buffer 已序列化数据中的未识别字段的表示方式。例如,当旧程序解析带有新字段的数据时,这些新字段就会成为旧程序的未知字段


MessageLite 类介绍(了解)
MessageLite从名字看是轻量级的message,仅仅提供序列化、反序列化功能 类定义在message_lite.h中
Message 类介绍(了解)
message类,都是继承自MessageMessage最重要的两个接口GetDescriptor/GetReflection,可以获取该类型对应Descriptor对象指针和Reflection 对象指针google提供的 message.h 中//google::protobuf::Message 部分代码展示
const Descriptor* GetDescriptor() const;
const Reflection* GetReflection() const;Descriptor 类介绍(了解)
// 部分代码展示
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;
}Reflection 类介绍(了解)
Reflection接口类,主要提供了动态读写消息字段的接口,对消息对象的自动读写主要通过该类完成。
message中的字段,对每种类型,Reflection都提供了一个单独的接口用于读写字段对应的值。
repeated类型需要使用 GetRepeated*()/SetRepeated*() 接口,不可以和非repeated
类型接口混用
message对象只可以被由它自身的reflection(message.GetReflection()) 来操作
google 提供的 message.h 中。
// 部分代码展示
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;UnknownFieldSet 类介绍(重要)
-UnknownFieldSet包含在分析消息时遇到但未由其类型定义的所有字段。
UnknownFieldSet 附加到任何消息,请调用 Reflection::GetUnknownFields()
类定义在unknown_field_set.h中UnknownField 类介绍(重要)
unknown_field_set.h 中
根据上述的例子可以得出,pb是具有向前兼容的。为了叙述方便,把增加了“生日”属性的 service称为“新模块”;未做变动的client称为 “老模块”。
前后兼容的作用:当我们维护一个很庞大的分布式系统时,由于你无法同时 升级所有 模块,为了保证在升级过程中,整个系统能够尽可能不受影响,就需要尽量保证通讯协议的“向后兼容”或“向前兼容”。
.proto 文件中可以声明许多选项,使用option标注。选项能影响 proto 编译器的某些处理方式。
选项的完整列表在google/protobuf/descriptor.proto中定义。
部分代码:
syntax = "proto2"; //descriptor.proto 使用 proto2 语法版本
message FileOptions { ... } // 文件选项 定义在 FileOptions 消息中
message MessageOptions { ... } // 消息类型选项 定义在 MessageOptions 消息中
message FieldOptions { ... } // 消息字段选项 定义在 FieldOptions 消息中
message OneofOptions { ... } // oneof字段选项 定义在 OneofOptions 消息中
message EnumOptions { ... } // 枚举类型选项 定义在 EnumOptions 消息中
message EnumValueOptions { .. } // 枚举值选项 定义在 EnumValueOptions 消息中
message ServiceOptions { ... } // 服务选项 定义在 ServiceOptions 消息中
message MethodOptions { ... } // 服务方法选项 定义在 MethodOptions 消息中
...由此可见,选项分为文件级、消息级、字段级等等, 但并没有一种选项能作用于所有的类型
protoc 生成代码的优化侧重点(不影响消息的线格式,跨端/跨语言互通不受影响)。SPEED(默认)
CODE_SIZE
LITE_RUNTIME
libprotobuf-lite 而非 libprotobuf常见误写纠正
作用:允许多个枚举常量拥有相同的数值(枚举“别名”)。 不开启时(默认),同值会编译报错;开启后可复用值。 使用注意:
syntax = "proto3";
package demo;
// 文件级选项:控制生成代码的优化策略
option optimize_for = LITE_RUNTIME;
enum PhoneType {
// 枚举级选项:允许枚举值别名
option allow_alias = true;
MP = 0;
TEL = 1;
LANDLINE = 1; // 与 TEL 共享同一数值(别名)
}【选型建议速览】
SPEED.proto 很多:CODE_SIZELITE_RUNTIME(C++ 记得链接 libprotobuf-lite)ProtoBuf 允许自定义选项并使用。该功能大部分场景用不到,在这里不拓展讲解。有兴趣可以参考: 官方链接
摸清默认值的隐性规则,掌握消息更新的安全技巧,吃透兼容性的核心逻辑,你便给
ProtoBuf秘语加了 “抗迭代”的护盾。 这些技巧不只是孤立方法,更是让秘语灵活升级又不打断服务的关键。而这,正是从 “会写 ProtoBuf” 到 “写稳ProtoBuf” 的重要跨越。
本篇关于ProtoBuf进阶知识点的介绍就暂告段落啦,希望能对大家的学习产生帮助,欢迎各位佬前来支持斧正!!!