1.字段保留syntax proto3; package test.unknown_fields; message UserInfoV1 { int64 id 1; string name 2; } message UserInfoV2 { int64 id 1; string name 2; int32 age 3; // 新增字段 }#include iostream #include string #include test.pb.h using namespace test::unknown_fields; int main() { GOOGLE_PROTOBUF_VERIFY_VERSION; // 1. v2 序列化含新增 age 字段 UserInfoV2 v2_data; v2_data.set_id(1001); v2_data.set_name(张三); v2_data.set_age(25); std::string v2_bytes; v2_data.SerializeToString(v2_bytes); // 2. v1 反序列化v2 数据 → 产生未知字段 UserInfoV1 v1_data; v1_data.ParseFromString(v2_bytes); std::cout v1解析结果:id v1_data.id() , name v1_data.name() std::endl; // 3. 获取未知字段字节数旧版本正确写法 std::string unknown_str; v1_data.unknown_fields().SerializeToString(unknown_str); // 旧版用这个 std::cout v1中未知字段字节数: unknown_str.size() std::endl; // 4. v1 序列化保留未知字段 std::string v1_bytes; v1_data.SerializeToString(v1_bytes); // 5. v2 反序列化→ 恢复 age 字段 UserInfoV2 v2_data2; v2_data2.ParseFromString(v1_bytes); std::cout v2解析结果:age v2_data2.age() std::endl; google::protobuf::ShutdownProtobufLibrary(); return 0; }UserInfoV1(后续简称v1)比 UserInfoV2(后续简称v2)少一个字段v2对象序列化之后v1反序列化 解析不了自己没有的字段(未知字段) 这个勉强能理解但是为什么v1对象序列化之后 v2却能反序列化得到v1不存在的字段呢???核心原因Protobuf 的未知字段保留机制这是它向后兼容的关键用通俗的话给你讲明白结合你的 proto 和代码一点不复杂1. 先搞懂V1 解析 V2 数据时没定义的 age 字段去哪了你的 V1 只定义了 id字段号 1、name字段号 2而 V2 新增了 age字段号 3。当 V2 序列化数据包含 id1001、name 张三、age25后会把「字段号 字段类型 字段值」一起编码成二进制比如 age25 会编码成「字段号 3 类型 int32 值 25」的二进制字节。当 V1 去解析这个二进制数据时能识别字段号 1id、字段号 2name会正常解析并存储不认识字段号 3age不会丢弃这个字段而是把它的「字段号 类型 值」的二进制字节存到 V1 消息的「未知字段集合」就是你代码里的unknown_fields()中。简单说V1 虽然 “看不懂” age但会把它的二进制数据 “存起来”不弄丢。2. 再明白V2 为啥能反序列化出 V1 保存的 age 值你的代码里V1 解析完 V2 数据后又做了一步「V1 序列化」v1_data.SerializeToString(v1_bytes)。这一步的关键V1 序列化时会把自己的已知字段id、name和保存的未知字段age 的二进制一起序列化到二进制数据里。当 V2 去解析这个 V1 序列化后的二进制数据时能识别字段号 1id、字段号 2name正常解析能识别字段号 3age—— 因为 V2 定义了这个字段所以会从二进制数据里找到 V1 保存的「字段号 3 类型 值 25」解析出 age25。单纯 V1 序列化V1 本身没有任何未知字段V2 反序列化后不会解析出任何 “额外的未知字段”比如 age —— 因为 V1 本身就没有定义 age也没有保存过任何未知字段序列化后的数据里只有 V1 自己的已知字段id、name。2.前后兼容1. 向前兼容重点你代码里已体现就是 新版本数据旧版本能正常解析你代码里的核心场景对应你的情况V2有 age 字段序列化的数据V1无 age 字段能正常解析不会报错。原理V1 虽然没有 age 定义但会把 age 对应的二进制数据字段 3当作「未知字段」保存起来不丢弃、不报错这就是向前兼容的核心 ——新版本新增的字段不会影响旧版本的解析。举个具体例子你用 V2 生成的 “id1001、name 张三、age25” 的数据用 V1 去解析V1 虽然不认识 age但会把 age 的二进制数据存起来不会报错还能正常解析自己认识的 id 和 name这就是向前兼容新版本数据适配旧版本。2. 向后兼容补充你可能用到的场景就是 旧版本数据新版本能正常解析还能恢复完整信息你代码里的 V1 序列化后V2 能解析出 age就是向后兼容对应你的情况V1 序列化的数据包含之前保存的 age 未知字段用 V2 去解析能完整恢复出 age 的值25不会因为是 V1 生成的数据V2 就解析不了。原理V1 保存了 age 的二进制数据V2 认识 age 对应的字段号3所以能从 V1 序列化的数据里把 age 解析出来这就是向后兼容 ——旧版本数据新版本能完整识别不丢失信息。不管是向前还是向后兼容核心就 2 点少一个都不行代码里刚好都满足字段号不重复、不修改你 V1 的 id字段 1、name字段 2V2 新增的 age字段 3字段号都是唯一的没有重复也没有修改原有字段的字段号 —— 这是兼容的基础如果把 id 的字段号改成 3V1 就解析不了 V2 的数据了。未知字段不丢弃V1 解析 V2 数据时没有扔掉不认识的 age 字段而是保存为未知字段后续序列化时一起带出 —— 这是向后兼容能实现的关键如果 V1 直接扔掉 age 的数据V2 就解析不出 age 了不兼容的情况你不用踩坑如果后续修改字段号比如把 age 的字段号改成 2和 name 重复或者修改原有字段的类型比如把 id 改成字符串就会破坏兼容旧版本就解析不了了。3.reserved一、reserved 字段的核心作用一句话总结reserved 用于 “预留 / 禁用” 指定的字段号或字段名防止后续版本误使用这些字段从而保护 Protobuf 的前后兼容性—— 简单说就是给 “不能用” 的字段号 / 名字 “上锁”避免踩坑。二、结合 V1/V2 场景讲 2 个最常用的作用你大概率会用到作用 1禁用 “废弃的字段号”防止后续误复用假设你后续迭代想把 V2 的age3字段删掉比如不用这个字段了如果直接删掉后续有人不知情可能会新增一个新字段又用了字段号 3 —— 这就会破坏兼容比如旧版本 V1 保存的、原来 age3 的未知字段会被新字段解析错导致数据混乱。这时候用 reserved 禁用字段号 3就能避免这种问题// 迭代后的 V3 版本删掉了 age 字段用 reserved 禁用字段号3 message UserInfoV3 { int64 id 1; string name 2; reserved 3; // 禁用字段号3后续不能再用这个字段号定义任何字段 }这样一来不管谁后续修改这个 proto只要用字段号 3编译就会报错从根源上避免兼容问题。作用 2预留字段号为后续版本升级做准备假设你现在做 V1知道以后可能会新增 2 个字段但暂时用不到就可以用 reserved 预留几个字段号防止其他人误占用protobuf// V1 版本预留字段号3、4后续升级时用 message UserInfoV1 { int64 id 1; string name 2; reserved 3,4; // 预留字段号3和4现在不用后续新增字段时优先用这两个 }后续升级到 V2 时就可以直接用预留的字段号 3 定义 age不用怕和其他字段冲突也能保证兼容。三、关键注意事项结合你的兼容场景必看reserved 可以同时禁用「字段号」和「字段名」比如reserved 3, age;—— 既不能用字段号 3也不能用 “age” 这个名字双重保护。一旦用 reserved 禁用了字段号 / 名字后续任何版本都不能再使用哪怕你后悔了也不能复用否则会破坏前后兼容比如旧版本保存的未知字段会被解析错误。结合你之前的未知字段机制如果旧版本V1保存了某个字段号的未知字段后续新版本用 reserved 禁用了这个字段号那么新版本解析旧版本数据时会自动忽略这个未知字段不会报错但也不会解析避免数据混乱。不要和 existing 字段冲突比如你 V2 已经用了字段号 3age就不能再写reserved 3;—— 编译会直接报错必须先删除原字段再用 reserved 禁用。google::protobuf::MessageLite所有 Protobuf 消息的「根类」最基础的抽象google::protobuf::Message继承关系继承自 MessageLite在基础序列化上增加了反射 元数据能力google::protobuf::Descriptor作用存储消息的静态元数据比如 “这个消息叫什么有几个字段每个字段的号 / 类型 / 名字是什么”。google::protobuf::Reflection作用Protobuf 动态编程的核心可以在运行时读写消息的字段google::protobuf::UnknownFieldSet作用存储消息里所有「不认识的字段」的二进制数据比如 V1 解析 V2 时的 age 字段是未知字段的容器。google::protobuf::UnknownField作用存储单个未知字段的原始二进制数据根据字段类型有不同的存储形式。MessageLite 提供基础序列化能力。Message Reflection UnknownFieldSet 实现了「未知字段保留」—— 旧版本不认识的字段不会丢会被保存并传递。Descriptor 提供元数据让 Protobuf 能在运行时知道 “每个字段是什么”。实际上我们是通过google::protobuf::Reflection 中的GetUnknownFields()接口可以得到google::protobuf::UnknownFieldSet然后可以得到UnknownField 每一个单独未知字段的所有信息未知字段UnknownField的类型枚举与对应访问方法的映射关系是处理 protobuf 未知字段的核心接口说明。1. 左侧Type枚举未知字段的 wire type它定义了 protobuf 未知字段的 5 种基础线类型wire type对应不同的二进制编码格式TYPE_VARINT可变长整数编码如int32、sint64、bool等类型的底层编码TYPE_FIXED3232 位固定长度编码如fixed32、floatTYPE_FIXED6464 位固定长度编码如fixed64、doubleTYPE_LENGTH_DELIMITED长度分隔编码如string、bytes、嵌套 message、packed 重复字段TYPE_GROUP旧版分组编码protobuf 早期语法现已极少使用对应嵌套的字段组2. 右侧UnknownField访问方法每个方法与左侧的Type枚举一一对应用于读取对应类型的未知字段值varint()读取TYPE_VARINT类型的未知字段返回uint64_tfixed32()读取TYPE_FIXED32类型的未知字段返回uint32_tfixed64()读取TYPE_FIXED64类型的未知字段返回uint64_tlength_delimited()读取TYPE_LENGTH_DELIMITED类型的未知字段返回const std::string存储二进制数据或字符串group()读取TYPE_GROUP类型的未知字段返回const UnknownFieldSet嵌套的未知字段集合3. 核心作用当 protobuf 解析一个包含未知字段即当前.proto文件中未定义的字段通常是新版本消息新增的字段的消息时会将这些字段暂存到UnknownFieldSet中。开发者可以通过Reflection接口获取UnknownFieldSet遍历其中的UnknownField再根据其Type调用右侧对应的方法读取和处理这些未知字段数据从而实现向前兼容旧版本代码可以完整保留新版本消息中新增的字段数据避免数据丢失。4.optimize_foroption optimize_for SPEED; // 默认值三个可选值及核心差异取值中文释义核心特点适用场景SPEED默认速度优先1. 生成的代码包含大量手写风格的序列化 / 反序列化逻辑运行速度最快2. 生成的代码体积最大3. 依赖完整的 Protobuf 核心运行时库绝大多数服务端 / 高性能场景如高频 RPC 调用、大数据解析是最常用的选择CODE_SIZE代码体积优先1. 生成的代码复用通用的反射Reflection逻辑完成序列化 / 反序列化代码体积最小2. 运行速度比SPEED慢需通过反射动态处理字段3. 依赖完整运行时库嵌入式设备、移动端安装包体积敏感、低频调用的轻量场景LITE_RUNTIME轻量运行时优先1. 生成的代码体积较小且运行速度接近SPEED2. 仅依赖轻量级的protobuf-lite库剔除了反射、未知字段处理等高级功能3. 不支持Reflection、UnknownFieldSet等特性移动端Android/iOS、对运行时体积和性能都有要求的场景需注意使用该选项后依赖反射的功能会失效3. 关键注意事项proto3 兼容性proto3 简化了代码生成逻辑移除了optimize_for默认采用类似SPEED的优化策略且不再区分 lite 运行时如需轻量版需单独引入protobuf-lite库跨语言影响该选项主要影响 C 代码生成Java/Go 等语言的 Protobuf 实现已内置优化无需配置此选项功能限制选择LITE_RUNTIME后无法使用反射Reflection、未知字段UnknownFieldSet、动态消息DynamicMessage等高级功能。optimize_for : 该选项为⽂件选项可以设置 protoc 编译器的优化级别分别为SPEED、CODE_SIZE、LITE_RUNTIME。受该选项影响设置不同的优化级别编译 .proto ⽂件后⽣成的代码内容不同。syntaxproto3; //option optimize_for LITE_RUNTIME; //option optimize_for SPEED; message people{ string name 1; }option optimize_for LITE_RUNTIME;option optimize_for SPEED;我们发现我们选择不同的ptimize_for得到的方法所继承的父类是不同的ProtoBuf 允许⾃定义选项并使⽤。该功能⼤部分场景⽤不到在这⾥不拓展讲解5.json protobuf xml对比#include iostream #include chrono #include string #include cstdlib #include functional // XML 解析库 #include tinyxml2.h // 高性能 JSON 库RapidJSON替换原 nlohmann/json #include rapidjson/document.h #include rapidjson/writer.h #include rapidjson/stringbuffer.h // Protobuf 生成的头文件编译后生成 #include person.pb.h using namespace std; using namespace chrono; using namespace tinyxml2; using namespace rapidjson; using namespace test; // 统一的测试数据结构 struct TestPerson { int id; string name; int age; // 初始化测试数据 TestPerson() : id(123), name(test_user), age(25) {} }; /** * 通用计时工具函数执行指定函数指定次数返回总耗时毫秒 * param func 要执行的函数 * param iterations 执行次数 * return 总耗时毫秒 */ template typename Func double measureTime(Func func, int iterations) { auto start high_resolution_clock::now(); for (int i 0; i iterations; i) { func(); // 执行目标操作 } auto end high_resolution_clock::now(); durationdouble, milli total end - start; return total.count(); } // -------------------------- XML 序列化/反序列化 -------------------------- string xmlSerialize(const TestPerson p) { XMLDocument doc; XMLElement* root doc.NewElement(Person); doc.InsertFirstChild(root); root-SetAttribute(id, p.id); XMLElement* nameNode doc.NewElement(Name); nameNode-SetText(p.name.c_str()); root-InsertEndChild(nameNode); XMLElement* ageNode doc.NewElement(Age); ageNode-SetText(p.age); root-InsertEndChild(ageNode); XMLPrinter printer; doc.Print(printer); return printer.CStr(); } TestPerson xmlDeserialize(const string xmlStr) { TestPerson p; XMLDocument doc; doc.Parse(xmlStr.c_str()); XMLElement* root doc.FirstChildElement(Person); if (root) { p.id root-IntAttribute(id); XMLElement* nameNode root-FirstChildElement(Name); if (nameNode) p.name nameNode-GetText(); XMLElement* ageNode root-FirstChildElement(Age); if (ageNode) p.age ageNode-IntText(); } return p; } // -------------------------- JSON 序列化/反序列化RapidJSON 版 -------------------------- string jsonSerialize(const TestPerson p) { StringBuffer s; WriterStringBuffer writer(s); // 开始构建 JSON 对象 writer.StartObject(); // 写入 id整数 writer.Key(id); writer.Int(p.id); // 写入 name字符串 writer.Key(name); writer.String(p.name.c_str()); // 写入 age整数 writer.Key(age); writer.Int(p.age); // 结束 JSON 对象 writer.EndObject(); // 返回 JSON 字符串 return s.GetString(); } TestPerson jsonDeserialize(const string jsonStr) { TestPerson p; Document doc; // 解析 JSON 字符串无额外分配高性能 doc.Parse(jsonStr.c_str()); // 读取字段RapidJSON 直接访问无类型转换开销 if (doc.HasMember(id) doc[id].IsInt()) { p.id doc[id].GetInt(); } if (doc.HasMember(name) doc[name].IsString()) { p.name doc[name].GetString(); } if (doc.HasMember(age) doc[age].IsInt()) { p.age doc[age].GetInt(); } return p; } // -------------------------- Protobuf 序列化/反序列化 -------------------------- string protoSerialize(const TestPerson p) { Person proto_p; proto_p.set_id(p.id); proto_p.set_name(p.name); proto_p.set_age(p.age); string data; // 显式忽略返回值消除编译警告 (void)proto_p.SerializeToString(data); return data; } TestPerson protoDeserialize(const string protoStr) { TestPerson p; Person proto_p; // 显式忽略返回值消除编译警告 (void)proto_p.ParseFromString(protoStr); p.id proto_p.id(); p.name proto_p.name(); p.age proto_p.age(); return p; } /** * 执行单次格式的效率测试拆分序列化/反序列化时间 打印序列化大小 * param formatName 格式名称XML/JSON/Protobuf * param serialize 序列化函数 * param deserialize 反序列化函数 * param testData 测试数据 * param iterations 执行次数 */ void runTest(const string formatName, functionstring(const TestPerson) serialize, functionTestPerson(const string) deserialize, const TestPerson testData, int iterations) { // 预先生成序列化数据用于反序列化计时 计算序列化大小 string preSerialized serialize(testData); // 计算序列化后数据的字节大小 size_t serializedSize preSerialized.size(); // 1. 单独计算序列化耗时 double serializeTime measureTime([]() { // 仅执行序列化操作 string serialized serialize(testData); // 空操作仅保证序列化逻辑执行 (void)serialized; }, iterations); // 2. 单独计算反序列化耗时 double deserializeTime measureTime([]() { // 仅执行反序列化操作 TestPerson deserialized deserialize(preSerialized); // 数据验证确保反序列化逻辑正确 if (deserialized.id ! testData.id || deserialized.name ! testData.name || deserialized.age ! testData.age) { cerr formatName 反序列化数据错误 endl; exit(1); } }, iterations); // 打印结果新增序列化大小 cout formatName : endl; cout 序列化后数据大小: serializedSize 字节 endl; cout 序列化总耗时: serializeTime 毫秒 endl; cout 反序列化总耗时: deserializeTime 毫秒 endl; } int main() { // 初始化Protobuf必须 GOOGLE_PROTOBUF_VERIFY_VERSION; TestPerson testData; // 测试次数列表 int testIterations[] {100, 10000, 1000000}; // 遍历测试次数执行所有格式的测试 for (int iter : testIterations) { cout \n 测试次数: iter \n; runTest(XML, xmlSerialize, xmlDeserialize, testData, iter); runTest(JSON, jsonSerialize, jsonDeserialize, testData, iter); runTest(Protobuf, protoSerialize, protoDeserialize, testData, iter); } // 清理Protobuf资源 google::protobuf::ShutdownProtobufLibrary(); return 0; }序列化阶段JSON 已经比 XML 快了比如 100 万次JSON 序列化1471.64ms XML 序列化1587.43ms反序列化阶段JSON 依然比 XML 慢100 万次JSON 反序列化4367.61ms XML 反序列化1780.73ms这说明RapidJSON 的序列化已经优化到位但反序列化在小数据场景下仍不如 tinyxml2核心原因有几个 为什么 JSON 反序列化还是比 XML 慢1.测试数据太小库的「固定开销」被放大你的测试数据只有 3 个字段id123、nametest_user、age25数据量极小tinyxml2XML 结构简单Parse()是轻量级流式解析直接遍历节点、读取属性 / 文本几乎没有额外校验开销小数据下效率极高。RapidJSON即使是极小的 JSONParse()也要做完整的语法校验识别{}/:/等符号边界区分数字、字符串、对象类型构建 DOM 树并做内存分配这些固定开销在小数据场景下占比极高盖过了 JSON 格式本身的优势。2.tinyxml2 的反序列化逻辑更「极简」tinyxml2 不做复杂类型校验XML 里所有内容都是文本IntAttribute()/GetText()只是简单转换没有类型安全检查。RapidJSON 必须做严格类型校验HasMember()、IsInt()、GetInt()等操作需要在 DOM 树中查找键、验证类型小对象下这些查找开销比 tinyxml2 的「直接节点访问」更重。3.库的设计目标差异tinyxml2专为「轻量、快速」设计代码精简只保留 XML 核心功能极端优化小数据处理。RapidJSON通用高性能 JSON 库支持完整 JSON 标准嵌套、数组、复杂类型功能更全所以在小数据场景下额外功能的 overhead 会更明显。序列化协议通用性格式可读性序列化大小序列化性能适用场景JSON通用json、xml 已成为多种行业标准的编写工具文本格式好轻量使用键值对方式压缩了一定的数据空间中web 项目。因为浏览器对于 json 数据支持非常好有很多内建的函数支持。XML通用文本格式好重量数据冗余因为需要成对的闭合标签低XML 作为一种扩展标记语言衍生出了 HTML、RDF/RDFS它强调数据结构化的能力和可读性。ProtoBuf独立Protobuf 只是 Google 公司内部的工具二进制格式差只能反序列化后得到真正可读的数据轻量比 JSON 更轻量传输起来带宽和速度会有优化高适合高性能对响应速度有要求的数据传输场景。Protobuf 比 XML、JSON 更小、更快。
Protobuf (2)
1.字段保留syntax proto3; package test.unknown_fields; message UserInfoV1 { int64 id 1; string name 2; } message UserInfoV2 { int64 id 1; string name 2; int32 age 3; // 新增字段 }#include iostream #include string #include test.pb.h using namespace test::unknown_fields; int main() { GOOGLE_PROTOBUF_VERIFY_VERSION; // 1. v2 序列化含新增 age 字段 UserInfoV2 v2_data; v2_data.set_id(1001); v2_data.set_name(张三); v2_data.set_age(25); std::string v2_bytes; v2_data.SerializeToString(v2_bytes); // 2. v1 反序列化v2 数据 → 产生未知字段 UserInfoV1 v1_data; v1_data.ParseFromString(v2_bytes); std::cout v1解析结果:id v1_data.id() , name v1_data.name() std::endl; // 3. 获取未知字段字节数旧版本正确写法 std::string unknown_str; v1_data.unknown_fields().SerializeToString(unknown_str); // 旧版用这个 std::cout v1中未知字段字节数: unknown_str.size() std::endl; // 4. v1 序列化保留未知字段 std::string v1_bytes; v1_data.SerializeToString(v1_bytes); // 5. v2 反序列化→ 恢复 age 字段 UserInfoV2 v2_data2; v2_data2.ParseFromString(v1_bytes); std::cout v2解析结果:age v2_data2.age() std::endl; google::protobuf::ShutdownProtobufLibrary(); return 0; }UserInfoV1(后续简称v1)比 UserInfoV2(后续简称v2)少一个字段v2对象序列化之后v1反序列化 解析不了自己没有的字段(未知字段) 这个勉强能理解但是为什么v1对象序列化之后 v2却能反序列化得到v1不存在的字段呢???核心原因Protobuf 的未知字段保留机制这是它向后兼容的关键用通俗的话给你讲明白结合你的 proto 和代码一点不复杂1. 先搞懂V1 解析 V2 数据时没定义的 age 字段去哪了你的 V1 只定义了 id字段号 1、name字段号 2而 V2 新增了 age字段号 3。当 V2 序列化数据包含 id1001、name 张三、age25后会把「字段号 字段类型 字段值」一起编码成二进制比如 age25 会编码成「字段号 3 类型 int32 值 25」的二进制字节。当 V1 去解析这个二进制数据时能识别字段号 1id、字段号 2name会正常解析并存储不认识字段号 3age不会丢弃这个字段而是把它的「字段号 类型 值」的二进制字节存到 V1 消息的「未知字段集合」就是你代码里的unknown_fields()中。简单说V1 虽然 “看不懂” age但会把它的二进制数据 “存起来”不弄丢。2. 再明白V2 为啥能反序列化出 V1 保存的 age 值你的代码里V1 解析完 V2 数据后又做了一步「V1 序列化」v1_data.SerializeToString(v1_bytes)。这一步的关键V1 序列化时会把自己的已知字段id、name和保存的未知字段age 的二进制一起序列化到二进制数据里。当 V2 去解析这个 V1 序列化后的二进制数据时能识别字段号 1id、字段号 2name正常解析能识别字段号 3age—— 因为 V2 定义了这个字段所以会从二进制数据里找到 V1 保存的「字段号 3 类型 值 25」解析出 age25。单纯 V1 序列化V1 本身没有任何未知字段V2 反序列化后不会解析出任何 “额外的未知字段”比如 age —— 因为 V1 本身就没有定义 age也没有保存过任何未知字段序列化后的数据里只有 V1 自己的已知字段id、name。2.前后兼容1. 向前兼容重点你代码里已体现就是 新版本数据旧版本能正常解析你代码里的核心场景对应你的情况V2有 age 字段序列化的数据V1无 age 字段能正常解析不会报错。原理V1 虽然没有 age 定义但会把 age 对应的二进制数据字段 3当作「未知字段」保存起来不丢弃、不报错这就是向前兼容的核心 ——新版本新增的字段不会影响旧版本的解析。举个具体例子你用 V2 生成的 “id1001、name 张三、age25” 的数据用 V1 去解析V1 虽然不认识 age但会把 age 的二进制数据存起来不会报错还能正常解析自己认识的 id 和 name这就是向前兼容新版本数据适配旧版本。2. 向后兼容补充你可能用到的场景就是 旧版本数据新版本能正常解析还能恢复完整信息你代码里的 V1 序列化后V2 能解析出 age就是向后兼容对应你的情况V1 序列化的数据包含之前保存的 age 未知字段用 V2 去解析能完整恢复出 age 的值25不会因为是 V1 生成的数据V2 就解析不了。原理V1 保存了 age 的二进制数据V2 认识 age 对应的字段号3所以能从 V1 序列化的数据里把 age 解析出来这就是向后兼容 ——旧版本数据新版本能完整识别不丢失信息。不管是向前还是向后兼容核心就 2 点少一个都不行代码里刚好都满足字段号不重复、不修改你 V1 的 id字段 1、name字段 2V2 新增的 age字段 3字段号都是唯一的没有重复也没有修改原有字段的字段号 —— 这是兼容的基础如果把 id 的字段号改成 3V1 就解析不了 V2 的数据了。未知字段不丢弃V1 解析 V2 数据时没有扔掉不认识的 age 字段而是保存为未知字段后续序列化时一起带出 —— 这是向后兼容能实现的关键如果 V1 直接扔掉 age 的数据V2 就解析不出 age 了不兼容的情况你不用踩坑如果后续修改字段号比如把 age 的字段号改成 2和 name 重复或者修改原有字段的类型比如把 id 改成字符串就会破坏兼容旧版本就解析不了了。3.reserved一、reserved 字段的核心作用一句话总结reserved 用于 “预留 / 禁用” 指定的字段号或字段名防止后续版本误使用这些字段从而保护 Protobuf 的前后兼容性—— 简单说就是给 “不能用” 的字段号 / 名字 “上锁”避免踩坑。二、结合 V1/V2 场景讲 2 个最常用的作用你大概率会用到作用 1禁用 “废弃的字段号”防止后续误复用假设你后续迭代想把 V2 的age3字段删掉比如不用这个字段了如果直接删掉后续有人不知情可能会新增一个新字段又用了字段号 3 —— 这就会破坏兼容比如旧版本 V1 保存的、原来 age3 的未知字段会被新字段解析错导致数据混乱。这时候用 reserved 禁用字段号 3就能避免这种问题// 迭代后的 V3 版本删掉了 age 字段用 reserved 禁用字段号3 message UserInfoV3 { int64 id 1; string name 2; reserved 3; // 禁用字段号3后续不能再用这个字段号定义任何字段 }这样一来不管谁后续修改这个 proto只要用字段号 3编译就会报错从根源上避免兼容问题。作用 2预留字段号为后续版本升级做准备假设你现在做 V1知道以后可能会新增 2 个字段但暂时用不到就可以用 reserved 预留几个字段号防止其他人误占用protobuf// V1 版本预留字段号3、4后续升级时用 message UserInfoV1 { int64 id 1; string name 2; reserved 3,4; // 预留字段号3和4现在不用后续新增字段时优先用这两个 }后续升级到 V2 时就可以直接用预留的字段号 3 定义 age不用怕和其他字段冲突也能保证兼容。三、关键注意事项结合你的兼容场景必看reserved 可以同时禁用「字段号」和「字段名」比如reserved 3, age;—— 既不能用字段号 3也不能用 “age” 这个名字双重保护。一旦用 reserved 禁用了字段号 / 名字后续任何版本都不能再使用哪怕你后悔了也不能复用否则会破坏前后兼容比如旧版本保存的未知字段会被解析错误。结合你之前的未知字段机制如果旧版本V1保存了某个字段号的未知字段后续新版本用 reserved 禁用了这个字段号那么新版本解析旧版本数据时会自动忽略这个未知字段不会报错但也不会解析避免数据混乱。不要和 existing 字段冲突比如你 V2 已经用了字段号 3age就不能再写reserved 3;—— 编译会直接报错必须先删除原字段再用 reserved 禁用。google::protobuf::MessageLite所有 Protobuf 消息的「根类」最基础的抽象google::protobuf::Message继承关系继承自 MessageLite在基础序列化上增加了反射 元数据能力google::protobuf::Descriptor作用存储消息的静态元数据比如 “这个消息叫什么有几个字段每个字段的号 / 类型 / 名字是什么”。google::protobuf::Reflection作用Protobuf 动态编程的核心可以在运行时读写消息的字段google::protobuf::UnknownFieldSet作用存储消息里所有「不认识的字段」的二进制数据比如 V1 解析 V2 时的 age 字段是未知字段的容器。google::protobuf::UnknownField作用存储单个未知字段的原始二进制数据根据字段类型有不同的存储形式。MessageLite 提供基础序列化能力。Message Reflection UnknownFieldSet 实现了「未知字段保留」—— 旧版本不认识的字段不会丢会被保存并传递。Descriptor 提供元数据让 Protobuf 能在运行时知道 “每个字段是什么”。实际上我们是通过google::protobuf::Reflection 中的GetUnknownFields()接口可以得到google::protobuf::UnknownFieldSet然后可以得到UnknownField 每一个单独未知字段的所有信息未知字段UnknownField的类型枚举与对应访问方法的映射关系是处理 protobuf 未知字段的核心接口说明。1. 左侧Type枚举未知字段的 wire type它定义了 protobuf 未知字段的 5 种基础线类型wire type对应不同的二进制编码格式TYPE_VARINT可变长整数编码如int32、sint64、bool等类型的底层编码TYPE_FIXED3232 位固定长度编码如fixed32、floatTYPE_FIXED6464 位固定长度编码如fixed64、doubleTYPE_LENGTH_DELIMITED长度分隔编码如string、bytes、嵌套 message、packed 重复字段TYPE_GROUP旧版分组编码protobuf 早期语法现已极少使用对应嵌套的字段组2. 右侧UnknownField访问方法每个方法与左侧的Type枚举一一对应用于读取对应类型的未知字段值varint()读取TYPE_VARINT类型的未知字段返回uint64_tfixed32()读取TYPE_FIXED32类型的未知字段返回uint32_tfixed64()读取TYPE_FIXED64类型的未知字段返回uint64_tlength_delimited()读取TYPE_LENGTH_DELIMITED类型的未知字段返回const std::string存储二进制数据或字符串group()读取TYPE_GROUP类型的未知字段返回const UnknownFieldSet嵌套的未知字段集合3. 核心作用当 protobuf 解析一个包含未知字段即当前.proto文件中未定义的字段通常是新版本消息新增的字段的消息时会将这些字段暂存到UnknownFieldSet中。开发者可以通过Reflection接口获取UnknownFieldSet遍历其中的UnknownField再根据其Type调用右侧对应的方法读取和处理这些未知字段数据从而实现向前兼容旧版本代码可以完整保留新版本消息中新增的字段数据避免数据丢失。4.optimize_foroption optimize_for SPEED; // 默认值三个可选值及核心差异取值中文释义核心特点适用场景SPEED默认速度优先1. 生成的代码包含大量手写风格的序列化 / 反序列化逻辑运行速度最快2. 生成的代码体积最大3. 依赖完整的 Protobuf 核心运行时库绝大多数服务端 / 高性能场景如高频 RPC 调用、大数据解析是最常用的选择CODE_SIZE代码体积优先1. 生成的代码复用通用的反射Reflection逻辑完成序列化 / 反序列化代码体积最小2. 运行速度比SPEED慢需通过反射动态处理字段3. 依赖完整运行时库嵌入式设备、移动端安装包体积敏感、低频调用的轻量场景LITE_RUNTIME轻量运行时优先1. 生成的代码体积较小且运行速度接近SPEED2. 仅依赖轻量级的protobuf-lite库剔除了反射、未知字段处理等高级功能3. 不支持Reflection、UnknownFieldSet等特性移动端Android/iOS、对运行时体积和性能都有要求的场景需注意使用该选项后依赖反射的功能会失效3. 关键注意事项proto3 兼容性proto3 简化了代码生成逻辑移除了optimize_for默认采用类似SPEED的优化策略且不再区分 lite 运行时如需轻量版需单独引入protobuf-lite库跨语言影响该选项主要影响 C 代码生成Java/Go 等语言的 Protobuf 实现已内置优化无需配置此选项功能限制选择LITE_RUNTIME后无法使用反射Reflection、未知字段UnknownFieldSet、动态消息DynamicMessage等高级功能。optimize_for : 该选项为⽂件选项可以设置 protoc 编译器的优化级别分别为SPEED、CODE_SIZE、LITE_RUNTIME。受该选项影响设置不同的优化级别编译 .proto ⽂件后⽣成的代码内容不同。syntaxproto3; //option optimize_for LITE_RUNTIME; //option optimize_for SPEED; message people{ string name 1; }option optimize_for LITE_RUNTIME;option optimize_for SPEED;我们发现我们选择不同的ptimize_for得到的方法所继承的父类是不同的ProtoBuf 允许⾃定义选项并使⽤。该功能⼤部分场景⽤不到在这⾥不拓展讲解5.json protobuf xml对比#include iostream #include chrono #include string #include cstdlib #include functional // XML 解析库 #include tinyxml2.h // 高性能 JSON 库RapidJSON替换原 nlohmann/json #include rapidjson/document.h #include rapidjson/writer.h #include rapidjson/stringbuffer.h // Protobuf 生成的头文件编译后生成 #include person.pb.h using namespace std; using namespace chrono; using namespace tinyxml2; using namespace rapidjson; using namespace test; // 统一的测试数据结构 struct TestPerson { int id; string name; int age; // 初始化测试数据 TestPerson() : id(123), name(test_user), age(25) {} }; /** * 通用计时工具函数执行指定函数指定次数返回总耗时毫秒 * param func 要执行的函数 * param iterations 执行次数 * return 总耗时毫秒 */ template typename Func double measureTime(Func func, int iterations) { auto start high_resolution_clock::now(); for (int i 0; i iterations; i) { func(); // 执行目标操作 } auto end high_resolution_clock::now(); durationdouble, milli total end - start; return total.count(); } // -------------------------- XML 序列化/反序列化 -------------------------- string xmlSerialize(const TestPerson p) { XMLDocument doc; XMLElement* root doc.NewElement(Person); doc.InsertFirstChild(root); root-SetAttribute(id, p.id); XMLElement* nameNode doc.NewElement(Name); nameNode-SetText(p.name.c_str()); root-InsertEndChild(nameNode); XMLElement* ageNode doc.NewElement(Age); ageNode-SetText(p.age); root-InsertEndChild(ageNode); XMLPrinter printer; doc.Print(printer); return printer.CStr(); } TestPerson xmlDeserialize(const string xmlStr) { TestPerson p; XMLDocument doc; doc.Parse(xmlStr.c_str()); XMLElement* root doc.FirstChildElement(Person); if (root) { p.id root-IntAttribute(id); XMLElement* nameNode root-FirstChildElement(Name); if (nameNode) p.name nameNode-GetText(); XMLElement* ageNode root-FirstChildElement(Age); if (ageNode) p.age ageNode-IntText(); } return p; } // -------------------------- JSON 序列化/反序列化RapidJSON 版 -------------------------- string jsonSerialize(const TestPerson p) { StringBuffer s; WriterStringBuffer writer(s); // 开始构建 JSON 对象 writer.StartObject(); // 写入 id整数 writer.Key(id); writer.Int(p.id); // 写入 name字符串 writer.Key(name); writer.String(p.name.c_str()); // 写入 age整数 writer.Key(age); writer.Int(p.age); // 结束 JSON 对象 writer.EndObject(); // 返回 JSON 字符串 return s.GetString(); } TestPerson jsonDeserialize(const string jsonStr) { TestPerson p; Document doc; // 解析 JSON 字符串无额外分配高性能 doc.Parse(jsonStr.c_str()); // 读取字段RapidJSON 直接访问无类型转换开销 if (doc.HasMember(id) doc[id].IsInt()) { p.id doc[id].GetInt(); } if (doc.HasMember(name) doc[name].IsString()) { p.name doc[name].GetString(); } if (doc.HasMember(age) doc[age].IsInt()) { p.age doc[age].GetInt(); } return p; } // -------------------------- Protobuf 序列化/反序列化 -------------------------- string protoSerialize(const TestPerson p) { Person proto_p; proto_p.set_id(p.id); proto_p.set_name(p.name); proto_p.set_age(p.age); string data; // 显式忽略返回值消除编译警告 (void)proto_p.SerializeToString(data); return data; } TestPerson protoDeserialize(const string protoStr) { TestPerson p; Person proto_p; // 显式忽略返回值消除编译警告 (void)proto_p.ParseFromString(protoStr); p.id proto_p.id(); p.name proto_p.name(); p.age proto_p.age(); return p; } /** * 执行单次格式的效率测试拆分序列化/反序列化时间 打印序列化大小 * param formatName 格式名称XML/JSON/Protobuf * param serialize 序列化函数 * param deserialize 反序列化函数 * param testData 测试数据 * param iterations 执行次数 */ void runTest(const string formatName, functionstring(const TestPerson) serialize, functionTestPerson(const string) deserialize, const TestPerson testData, int iterations) { // 预先生成序列化数据用于反序列化计时 计算序列化大小 string preSerialized serialize(testData); // 计算序列化后数据的字节大小 size_t serializedSize preSerialized.size(); // 1. 单独计算序列化耗时 double serializeTime measureTime([]() { // 仅执行序列化操作 string serialized serialize(testData); // 空操作仅保证序列化逻辑执行 (void)serialized; }, iterations); // 2. 单独计算反序列化耗时 double deserializeTime measureTime([]() { // 仅执行反序列化操作 TestPerson deserialized deserialize(preSerialized); // 数据验证确保反序列化逻辑正确 if (deserialized.id ! testData.id || deserialized.name ! testData.name || deserialized.age ! testData.age) { cerr formatName 反序列化数据错误 endl; exit(1); } }, iterations); // 打印结果新增序列化大小 cout formatName : endl; cout 序列化后数据大小: serializedSize 字节 endl; cout 序列化总耗时: serializeTime 毫秒 endl; cout 反序列化总耗时: deserializeTime 毫秒 endl; } int main() { // 初始化Protobuf必须 GOOGLE_PROTOBUF_VERIFY_VERSION; TestPerson testData; // 测试次数列表 int testIterations[] {100, 10000, 1000000}; // 遍历测试次数执行所有格式的测试 for (int iter : testIterations) { cout \n 测试次数: iter \n; runTest(XML, xmlSerialize, xmlDeserialize, testData, iter); runTest(JSON, jsonSerialize, jsonDeserialize, testData, iter); runTest(Protobuf, protoSerialize, protoDeserialize, testData, iter); } // 清理Protobuf资源 google::protobuf::ShutdownProtobufLibrary(); return 0; }序列化阶段JSON 已经比 XML 快了比如 100 万次JSON 序列化1471.64ms XML 序列化1587.43ms反序列化阶段JSON 依然比 XML 慢100 万次JSON 反序列化4367.61ms XML 反序列化1780.73ms这说明RapidJSON 的序列化已经优化到位但反序列化在小数据场景下仍不如 tinyxml2核心原因有几个 为什么 JSON 反序列化还是比 XML 慢1.测试数据太小库的「固定开销」被放大你的测试数据只有 3 个字段id123、nametest_user、age25数据量极小tinyxml2XML 结构简单Parse()是轻量级流式解析直接遍历节点、读取属性 / 文本几乎没有额外校验开销小数据下效率极高。RapidJSON即使是极小的 JSONParse()也要做完整的语法校验识别{}/:/等符号边界区分数字、字符串、对象类型构建 DOM 树并做内存分配这些固定开销在小数据场景下占比极高盖过了 JSON 格式本身的优势。2.tinyxml2 的反序列化逻辑更「极简」tinyxml2 不做复杂类型校验XML 里所有内容都是文本IntAttribute()/GetText()只是简单转换没有类型安全检查。RapidJSON 必须做严格类型校验HasMember()、IsInt()、GetInt()等操作需要在 DOM 树中查找键、验证类型小对象下这些查找开销比 tinyxml2 的「直接节点访问」更重。3.库的设计目标差异tinyxml2专为「轻量、快速」设计代码精简只保留 XML 核心功能极端优化小数据处理。RapidJSON通用高性能 JSON 库支持完整 JSON 标准嵌套、数组、复杂类型功能更全所以在小数据场景下额外功能的 overhead 会更明显。序列化协议通用性格式可读性序列化大小序列化性能适用场景JSON通用json、xml 已成为多种行业标准的编写工具文本格式好轻量使用键值对方式压缩了一定的数据空间中web 项目。因为浏览器对于 json 数据支持非常好有很多内建的函数支持。XML通用文本格式好重量数据冗余因为需要成对的闭合标签低XML 作为一种扩展标记语言衍生出了 HTML、RDF/RDFS它强调数据结构化的能力和可读性。ProtoBuf独立Protobuf 只是 Google 公司内部的工具二进制格式差只能反序列化后得到真正可读的数据轻量比 JSON 更轻量传输起来带宽和速度会有优化高适合高性能对响应速度有要求的数据传输场景。Protobuf 比 XML、JSON 更小、更快。