ProtoBuf语法揭秘:探秘编译魔法与性能优化策略,解锁多层级选项配置的底层奥秘

文章目录

本篇摘要

本文详解ProtoBuf核心类型:enum需遵循命名规范,0值必选且同级不重名;Any可存任意消息,支持动态扩展;oneof强制单字段互斥,节省内存;map创建键值映射,键限标量类型。还涵盖默认值规则、协议兼容策略、未知字段处理及option配置优化。

一.proto3语法解析之enum类型

  1. 命名规范
  • 枚举类型名称:使用驼峰命名法,首字母大写,如MyEnum
  • 常量值名称:全大写字母,多个字母间用下划线连接,如ENUM_CONST = 0;
  1. 示例 :定义名为PhoneType的枚举类型:
protobuf 复制代码
enum PhoneType {
  MP = 0; // 移动电话
  TEL = 1; // 固定电话
}
  1. 规则
  • 0值常量必须存在且作为第一个元素,以兼容proto2语义(第一个元素作为默认值,值为0)
  • 枚举类型可在消息外定义,也可在消息体内嵌套定义
  • 枚举常量值在32位整数范围内,负值无效不建议使用

注意:

  1. 同级(同层)枚举类型中常量不能重名,否则单个.proto文件下测试编译会报错(某某某常量已被定义)
  2. 单个.proto文件下,最外层与嵌套枚举类型不算同级
  3. 多个.proto文件下,若文件未声明package且存在引用,各文件最外层枚举算同级;若文件声明了package则不算同级

演示下:

cpp 复制代码
syntax= "proto3";
package enum;
enum PhoneType{
     MF=0;//移动电话
    TEL=1;//固定电话
}


  • 成功生成了对应文件。
  • 可以发现同级下如果有相同类型会报错。


  • 此时就不再保存;发现转化的对应c++代码生成的类名称也完成了嵌套规则。

还有就是如果导入的是同级别的存在同名的枚举类型;那么也会报错;此时就可以手动加上对应的命名空间(package);此时就不算同级别则不会报错了。

下面结合枚举改造下之前的通讯录:/font>

  • 这里注意生成的c++代码也会进行嵌套(枚举);只需要在每次选择手机号的还是输入下即可。
  • 其次就是对应read的时候这里如果只打印type;那么就是数字;故可以使用对应接口打印出类型名称。
  • 下面进行手动添加下。
  • read一下发现对应的之前没有添加枚举类型的也有了;这里protobuf规定没有指明对应枚举类型对应默认使用为0的枚举。

二.proto3语法解析之Any类型

Any 类型(Protocol Buffers 通用容器类型):

  1. 核心功能
  • 任意消息存储:可像编程语言中的泛型容器一样,存储任意类型的 Protocol Buffers 消息(自定义消息或内置类型)。
  • 支持重复字段 :可通过 repeated 修饰,实现存储多个任意类型消息(类似数组/列表)。
  1. 类型本质
  • 预定义类型:由 Google 官方预先定义,非用户自定义类型,安装 Protobuf 后即可使用。
  1. 获取方式
  • 文件位置 :安装 Protobuf 后,在其 include 目录下的 .proto 文件中可找到所有预定义类型(包括 Any 的声明和详细定义)。
  1. 典型用途
  • 适用于需要动态处理多种未知消息类型的场景(如通用数据传输、插件化架构等),通过灵活封装不同消息实现扩展性。

下面演示下:

  • 默认对应Any的类型一些文件实现在protobuf安装的时候就实现安装好了。
  • Any类型常用的几个函数接口。
  • 对应的关于设置进去容器字段选项。

下面给之前写的通讯录利用Any类型加上对应的地址;也就是把地址填入对应Any类型管理的数据了:

  • 首先导入对应的Any文件。
  • 定义对应的地址类。
  • 联系人信息设置对应的Any类型变量。
cpp 复制代码
  contacts::Address address;

        cout << "请输入联系人家庭地址:";

        string home;

        getline(cin, home);

        address.set_home_address(home);
        cout << "请输入联系人工作地址:";
        string work;
        getline(cin, work);
        address.set_work_address(work);

        //进行加入Any管理的变量(把对应地址):
       persons->mutable_data()->PackFrom(address);
  • 在输入联系人的时候添加对应地址信息。
  • 对应的打印。
  • 进行增加新的联系人信息。
  • 显示添加的新联系人信息。

注:

  • 这里常用的关于Any类型的变量如判断 情况 拿到对应类型变量的地址(has_value clear_value mutable_value);其次就是将对象放入以及取出(PackFrom UnpackTo)。

  • 其次就是Any类型变量还可以用repeated修饰也就是当成容器数组来用;可以使用add_value来添加等。

三.proto3语法解析之oneof类型

  1. 用途:用于消息中有多个可选字段,但同一时间只能设置其中一个字段的场景。
  2. 核心作用:强制约束同一时刻仅允许一个字段有值,避免逻辑冲突。
  3. 优势:节省内存(未设置的字段不占额外空间),并明确字段互斥的语义。

简单说就是被这个类型变量包含的成员;只能设置一个;然后内部进行编译成c++代码的时候会生成对应枚举来供选择(注意的是这里面不能选择使用对应的repeated选项)。

下面再基于oneof增加对应的qq wechat等选项:

  • 联系人信息里面定义对应类型。
  • 生成的对应关于这oneof包含的这两个类型的枚举;来供选择;以及判断oneof里面保存的是哪个变量。
  • 可以看到生成的对应的c++文件关于这两个类里面都提供了对应的接口。

下面别忘记编译一下proto文件再就进行make:

write.cc(这里只能选择一个):

read.cc:

  • 这里会在对应的个人信息添加对应的枚举类;可以通过函数接口判断里面设置了哪个变量(oneof);没有设置就是默认值故不操作。
  • 也是成功添加。
  • 打印也是没问题。

四.proto3语法解析之map类型

  1. 语法:使用map<key_type, value_type> map_field = N;格式创建关联映射字段。
  2. 键类型:除floatbytes外的任意标量类型。
  3. 值类型:可以是任意类型。
  4. 修饰限制:map字段不能用repeated修饰。
  5. 元素顺序:map中元素无序。

下面演示下:

  • 下面只需要在描述的联系人信息添加对应map即可。

write.cc:

cpp 复制代码
for (int i = 0;; i++)
    {

        cout << "请输入备注" << i + 1 << "标题(只输入回车完成备注新增):";

        string key;
        getline(cin, key);
        if (key.empty())  {break;}
        cout << "请输入备注" << i + 1 << "内容: ";
        string value;
        getline(cin, value);
        persons->mutable_comment()->insert({key,value});
    }
  • 这里用法就和使用map一样。

read.cc:

cpp 复制代码
         if (cs.contacts(i).comment_size()) {
            cout << "备注信息:" << endl;
        }
        for (auto it = cs.contacts(i).comment().cbegin(); it !=cs.contacts(i).comment().cend(); it++) {
            cout << "   " << it->first << ": " << it->second << endl;
        }
  • 最后遍历取出即可。
  • 下面进行添加。
  • 也是成功打印。

五.默认值

反序列化消息时,若二进制序列不含某字段,反序列化后对象中对应字段会设为该字段默认值,且不同类型默认值不同。

  1. 字符串类型:默认值为空字符串。
  2. 字节类型:默认值为空字节。
  3. 布尔类型:默认值为false。
  4. 数值类型:默认值为0。
  5. 枚举类型:默认值是第一个定义的枚举值,且必须为0。
  6. 消息字段:未设置时取值依赖语言。
  7. repeated字段:默认值为空(通常是相应语言的一个空列表) 。
  8. 特定语言检测方法 :在C++和Java语言中,对于消息字段、oneof字段和any字段,有has_方法来检测当前字段是否被设置。

注:对于如string int32 int64这样的简单变量才支持对应set_操作;而如数组(repeated) map等这样的变量就不支持而改成了mutable_操作。

因此可能会存在比如数值类型;导致忘记设置了;那么它默认就是0;此时就不能判断是设置的0还是默认的;如果有has的话就能检测;可惜没有;因此就需要设置的时候确定好。

六.更新规则

1. 别碰的:

  • 已有字段的编号绝对不能改!改了旧版本就不认识新字段。
  • 删字段时,编号要标记为 reserved(保留),别重复用,也别直接删/注释,容易乱(如果保留了再次使用也会报错)。

2. 能换的类型:

  • 数值类型(int32/uint32/int64/uint64/bool)互相换,解析时自动截断(比如64位改32位,多的位不要了)。
  • sint32sint64 能互相换,和其他整型不行。
  • stringbytes 只要在合法UTF-8下,能互换。
  • enum 能和 int32/uint32 这些换,但值超范围会被截断;反序列化时,不认识的枚举值会保留(不同语言处理不一样)。
  • fixed32sfixed32fixed64sfixed64 能互换。

3. oneof 怎么改:

  • 单个值改成 oneof 里的新成员,安全!
  • 没代码同时设多个值时,把多个字段塞进新 oneof 也行。
  • 别把已有字段塞进现有的 oneof,会崩!

下面简单说下对于字段修改及删除操作;基于文件读写带来的影响:

如果读与写用的是同一个文件;但是proto文件不是同一个;当写入文件后;改变写已经存在的字段的类型变量的时候;看下对应读的效果:

  • 发现文件在进行反序列化的时候根据字段编号的;因此把生日就覆盖到年龄上了。

而预期的是年龄默认值为0;生日不打印;因此就不能复用之前的编号;然后新创建编号为生日:

只需要在对应类内 reserved字段如(reserved 1,2;可以多个保留或者也可以reserved "field3", "field4"进行变量类型保留),然后新建编号:

  • 这里新添加,但是年龄为2编号;注释掉;之后输入生日。
  • 打印出来;因为保留文件中的2号字段也就是年龄;当读取的时候读取2号字段就是读到之前设置的生日信息,然后对应设置进来的王五;由于2号字段保留了,又因为没有设置值故为默认值0;对应生日虽然设置进来但是读取文件没有对应字段故不读取。

简单说:可以理解成读取的时候和写入的时候是按照对应字段编号识别进行读写的,这也就是修改或者删除的时候需要注意字段编号信息。

总之,编号和保留字段守规矩,类型在兼容范围内换,oneof 操作小心,新旧版本就不会互相认不出来。

七.未知字段

未知字段指解析已序列化数据时的未识别字段,如旧程序解析带新字段数据的情形;proto3原本解析时丢弃未知字段,3.5及以上版本重新保留未知字段,反序列化和序列化结果都包含。

下面演示下:

  • 这里就是对未知字段使用的时候进行调用函数的接口。

下面就拿上面修改过字段的读端新加入字段;然后写端进行反序列化进来;而读端对应的.pb.h文件可以看到如下(此时新加入的字段而读端反序列化后不能识别的就是未知字段了):

  • 对应未知字段每个变量含有的一个表(枚举类;标记它是啥类型变量)。


  • 这里其实就是获取对应类型的变量内容(这里需要结合type这个枚举值来获取;不然会出错);也就是先判断变量是什么类型;然后在进行得到内容。

下面演示下:

对应部分代码:

cpp 复制代码
    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 ...
            }
        }
  • 这里其实对应的就是上面图的操作;先获取对应的GetReflection;也就是对应的静态函数;拿到对应对象;然后获取对应某个人的未知字段集合GetUnknownFields(people);根据数量进行遍历(set.field_count());判断什么类型;获取对应什么类型的值;然后就是对应的每个未知字段变量的number编号就是之前被在proto文件中设置的编号。

八.前后兼容性

  1. 向前兼容:老模块(未做变动的 client )能够正确识别新模块(增加了"生日"属性的 service )生成或发出的协议,新增加的属性(如"生日")会被当作未知字段( pb 3.5 版本及之后 )。

  2. 向后兼容:新模块能够正确识别老模块生成或发出的协议。

  3. 作用 :在维护庞大分布式系统且无法同时升级所有模块时,为保证升级过程中整个系统尽可能不受影响,需要尽量保证通讯协议的 "向后兼容" 或 "向前兼容"。

也就是为了这但做到了对应的对于接受到未知字段保存功能。

九.proto3语法解析之option类型

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

选项分类:

  • 选项的完整列表在google/protobuf/descriptor.proto中定义。
  • 选项分为文件级(如FileOptions)、消息级(如MessageOptions)、字段级(如FieldOptions)等多种,没有一种选项能作用于所有类型。

常用选项列举:

1·optimize_for

为文件选项 ,可设置protoc编译器的优化级别,有SPEEDCODE_SIZELITE_RUNTIME三种。

  • SPEED:生成的代码高度优化、运行效率高,但占用空间多,是默认选项。
  • CODE_SIZE:生成最少的类,占用空间少,依赖基于反射的代码实现相关操作,运行效率低,适合大量.proto文件且不盲目追求速度的应用。
  • LITE_RUNTIME:生成的代码执行效率高、占用空间少,牺牲了反射功能,仅提供编码+序列化功能,链接BP库时仅需链接libprotobuf - lite,常用于资源有限平台(如移动手机平台),示例代码为option optimize_for = LITE_RUNTIME;

2·allow_alias

为枚举选项,允许将相同常量值分配给不同枚举常量来定义别名。

  • enum PhoneType中设置option allow_alias = true;后,MP = 0;TEL = 1;LANDLINE = 1;不会报错(此时这俩变量就是同一个东西),若不设置则会编译报错。

十.仓库链接

Protobuf仓库传送门

十一.本篇小结

通过本篇可以学习到ProtoBuf通过enum/Any/oneof/map等类型灵活定义数据结构,结合默认值、兼容规则及未知字段保留机制,保障协议升级稳定性。合理使用option优化性能,严格遵循编号/命名规范,实现高效安全的跨版本通信。

相关推荐
fpcc7 小时前
C++编程实践——eventFD
linux·c++
仟濹7 小时前
「经典数字题」集合 | C/C++
c语言·开发语言·c++
Skrrapper7 小时前
【STL】set、multiset、unordered_set、unordered_multiset 的区别
c++·算法·哈希算法
冷崖8 小时前
QML-Model-View
javascript·c++
峥无8 小时前
《从适配器本质到面试题:一文掌握 C++ 栈、队列与优先级队列核心》
开发语言·c++·queue·stack
七夜zippoe8 小时前
仓颉FFI实战:C/C++互操作与性能优化
c语言·c++·性能优化
西哥写代码8 小时前
基于dcmtk的dicom工具 第十三章 dicom文件导出bmp、jpg、png、tiff、mp4
c++·mfc·dicom·dcmtk·tiffopen·dipngplugin·dijpegplugin
永远有缘9 小时前
Java、Python、C# 和 C++ 在函数定义语法上的主要区别
java·c++·python·c#
绛洞花主敏明10 小时前
Go切片的赋值
c++·算法·golang