Protobuf:消息更新

news2024/10/21 19:51:20

Protobuf:消息更新

    • 更新字段
    • 保留字段
    • 未知字段
    • option选项


在开发中,需要对产品进行版本迭代。迭代前后,类的成员可能就会有所改动,一旦类成员改动,那么老版本的对象,新版本可能就无法解析,此时就会出现问题。为此,Protobuf设计了一套机制,用于对消息进行更新,使其可以向前向后兼容。

更新字段

如果说,消息前后,部分变量的类型发生改变,此时在部分情况下,是可以完成兼容的。

更新前:

message Person {
    int32 id = 1;
    int32 age = 2;
    string name = 3;
}

更新后:

message Person {
    int64 id = 1;
    int64 age = 2;
    string name = 3;
}

这就是一个合法的类型修改。

  1. int32uint32int64uint64bool这几个类型之间相互兼容,可以进行转化。

变长整型家族的类型,都是基于VarInt编码 ,编码方式是一样的,因此可以进行转化。如果从64位整型转到32位整型,此时就会发生截断,与C/C++的整型处理策略一致。

  1. sint32sint64相互兼容

这两个整型使用VarInt编码 + ZigZag编码,因为相比于上面四个类型,引入了ZigZag编码来提高负数的存储效率,所以无法与之前的整型兼容,这两个整型自成一派。

  1. stringbytes在采用UTF-8对字符进行编码的情况下,可以相互兼容

  2. fixed32sfixed32兼容

  3. fixed64sfixed64兼容

这些采用的都是定长编码,所以可以相互兼容。但是不能跨位数进行兼容,32位之间相互兼容,64位之间相互兼容。

  1. 将一个单独的值更改为新 oneof 类型成员之一是安全和二进制兼容的

如果你有一个现有的字段,并决定将其移入一个新的 oneof,这是安全的。因为在 ProtoBuf 的序列化格式中,oneof 的字段与普通字段的序列化格式是相同的。

假设有以下消息定义:

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

可以安全地将 name 移入一个新的 oneof

message UpdatedMessage {
  oneof identifier {
    string name = 1;
  }
  int32 age = 2;
}
  1. 若确定没有代码一次性设置多个值,那么将多个字段移入一个新 oneof 类型也是可行的

如果确保在所有使用该消息的代码中,多个字段不会同时被设置,那么可以将这些字段一起移入一个新的 oneof,以实现更严格的数据约束。

message OriginalMessage {
  string first_name = 1;
  string last_name = 2;
}

假设确定 first_namelast_name 从未同时被设置,可以重构为:

message UpdatedMessage {
  oneof name {
    string first_name = 1;
    string last_name = 2;
  }
}
  1. 将任何字段移入已存在的 oneof 类型是不安全的

如果将一个字段移入到已经存在的 oneof 中,这可能会导致数据不兼容,因为现在的 oneof 中可能已经存在其他字段,数据的含义会发生改变。

message OriginalMessage {
  oneof contact {
    string email = 1;
    string phone_number = 2;
  }
  string address = 3;
}

如果你尝试将 address 移入现有的 oneof contact 中:

message UnsafeMessage {
  oneof contact {
    string email = 1;
    string phone_number = 2;
    string address = 3;  // 移动到 oneof 中
  }
}

这样做是不安全的,因为现有的二进制数据可能已经使用了 address 字段,而将其移入 oneof 会导致反序列化时数据被误解。


保留字段

有的时候,需要删除消息中的字段,此时就要用到保留字段

因为protobuf中使用字段编号来标识一个数据,如果说直接删除一个字段,那么此时就可能出现数据错误的问题。

message Person {
    int32 id = 1;
    int32 age = 2;
    string name = 3;
}

删除name字段后,又增加了gender字段:

message Person {
    int32 id = 1;
    int32 age = 2;
    string gender = 3;
}

此时就会导致问题,如果旧版本的客户发送了旧版本的消息,此时根据字段编号,gender性别就会收到name姓名的信息,此时就会发生错误。

也就是说:删除字段后,字段编号不能重复使用

为此,protobuf提供了一个reserved关键字,用于保留字段编号与变量名,被保留的字段编号与变量名就不能再被使用。

message Person {
    reserved 3;
    reserved "name";

    int32 id = 1;
    int32 age = 2;
    // string name = 3;
    string gender = 3;
}

此处通过reserved保留了字段编号3,以及变量名"name",那么这两个内容就不能在该message内部使用,string gender = 3会报错。

reserved还支持多种格式:

一次保留多个字段编号:

reserved x, y, z;

一次保留多个变量名:

reserved "aaa", "bbb", "ccc";

保留一个区间内的字段编号:

reserved x to y;

未知字段

如果一个旧版本的客户端,收到一个新版本的消息,会发生什么?

protobuf接收消息时,如果发现消息内含有自己无法识别的字段,会将该字段放到未知字段

现有以下消息类型:

syntax = "proto3";
package test_pkg;

message Person {
    int32 id = 1;
    int32 age = 2;
    string name = 3;
}

编译后,生成对应的Person对象,然后把对象序列化至文件test.txt中:

#include <iostream>
#include <fstream>
#include "test.pb.h"

using namespace std;
using namespace test_pkg;

int main()
{
    Person person;
    person.set_id(1);
    person.set_age(18);
    person.set_name("张三");

    ofstream ofs("test.txt", ios_base::binary);

    string str;
    person.SerializeToString(&str);

    ofs << str;
    ofs.close();

    return 0;
}

序列化成功后,可以通过protoc --decode查看序列化结果,命令格式:

protoc --decode=包名.消息名 源文件.proto < 被解析文件

此处 源文件.proto指定消息所处的文件,包名.消息名是要解析的消息类型,被解析文件内部是序列化后的结果。

在这里插入图片描述

可以看到,test.txt内的数据,被正确解析出来了。此处name字段存储的是字符串的编码。

随后修改消息类型:

syntax = "proto3";
package test_pkg;

message Person {
    reserved 2, 3;
    int32 id = 1;
}

删掉了agename字段。

再次通过新的消息类型解析test.txt

在这里插入图片描述

可以看到,无法再识别agename字段了,但是它依然可以判断数据存储的内容,以及对应的字段编号。

如果客户端得到反序列化后的消息,不会把无法识别的字段丢弃,而是存储到未知字段

protobuf的类架构:

在这里插入图片描述

注意后续的用于:Message表示上图的类,message表示用户定义的消息

  • Message
    • 所有message继承Message
    • 提供了GetDescriptorGetReflection接口,用于访问ReflectionDescriptor
  • MessaageLite
    • 该类提供序列化与反序列化的接口,Message继承该类
  • Descriptor
    • message的描述,比如message的名字,所包含的字段等
  • Reflection
    • 提供了messagesetget等基本操作接口
    • 提供GetUnknownFields接口,用于访问未知字段
  • UnknownFieldSet:未知字段集
    • 该类包含了所有无法解析的字段
  • UnknownField:未知字段
    • 用于描述一个具体的未知字段

接下讲解如何操作未知字段。

UnknownFieldSet类中,维护了一个集合,内部存储所有的未知字段,在unknown_field_set.h头文件中,包含了该类的声明:

class PROTOBUF_EXPORT UnknownFieldSet {
 public:
  UnknownFieldSet();
  ~UnknownFieldSet();
  inline void Clear();
  inline bool empty() const;
  inline int field_count() const;
  inline const UnknownField& field(int index) const;
};

这些都是很简单的接口,field_count统计未知字段集中有多少个未知字段,field(int index)获取指定下标的未知字段,返回一个UnknownField类型引用。

`UnknownField声明如下:

// Represents one field in an UnknownFieldSet.
class PROTOBUF_EXPORT UnknownField {
 public:
  enum Type {
    TYPE_VARINT,
    TYPE_FIXED32,
    TYPE_FIXED64,
    TYPE_LENGTH_DELIMITED,
    TYPE_GROUP
  };
  
  inline int number() const;
  inline Type type() const;

  // Accessors -------------------------------------------------------
  // Each method works only for UnknownFields of the corresponding type.

  inline uint64_t varint() const;
  inline uint32_t fixed32() const;
  inline uint64_t fixed64() const;
  inline const std::string& length_delimited() const;
  inline const UnknownFieldSet& group() const;

  inline void set_varint(uint64_t value);
  inline void set_fixed32(uint32_t value);
  inline void set_fixed64(uint64_t value);
  inline void set_length_delimited(const std::string& value);
  inline std::string* mutable_length_delimited();
  inline UnknownFieldSet* mutable_group();
};

第一个枚举用于表示这个未知字段的具体类型,其中TYPE_LENGTH_DELIMITED是字符串string

  • number:返回该未知字段的字段编号
  • type:返回该未知字段的类型

当检测到具体类型后,调用下面的接口来获取与设置值,比如varint()就是获取字段的整型值。

接下来写一个代码,解析刚才的test.txt

#include <iostream>
#include <fstream>
#include <google/protobuf/unknown_field_set.h>
#include "test.pb.h"

using namespace std;
using namespace test_pkg;
using namespace google::protobuf;

int main()
{
    ifstream ifs("test.txt", ios_base::binary);
    Person ps;
    ps.ParseFromIstream(&ifs);

    cout << "Id: " << ps.id() << endl;

    const Reflection* ref = Person::GetReflection();
    const UnknownFieldSet& ufs = ref->GetUnknownFields(ps);

    for (int i = 0; i < ufs.field_count(); i++)
    {
        const UnknownField& uf = ufs.field(i);
        cout << "未知字段" << endl;
        cout << "   字段编号: " << uf.number() << endl;
        cout << "   字段类型: " << uf.type() << endl;

        switch (uf.type())
        {
        case UnknownField::Type::TYPE_LENGTH_DELIMITED: // 字符串
            cout << "   string: " << uf.length_delimited() << endl;
            break;
        case UnknownField::Type::TYPE_VARINT:
            cout << "   varint: " << uf.varint() << endl;
        }
    }
    return 0;
}

首先,如果要使用位置字段,先要包含头文件<google/protobuf/unknown_field_set.h>

一开始通过ifstream读取test.txt文件,然后通过文件流反序列化数据到对象ps中。

由于访问未知字段集,要通过Reflection类,所以通过GetReflection构造一个该类。

再通过ref->GetUnknownFields(ps),获取到ps内的未知字段集。

一层for循环,遍历未知字段集的所有元素,并且输出。

输出结果:

在这里插入图片描述

可以得知,protobuf确实是吧无法识别的字段放到未知字段集了,并且给出了接口,让用户可以访问未知字段。


option选项

protobuf中,提供了一个option关键字,其用于指定编译器的处理方式。

语法:

option 选项 =;
  • optimize_for:文件选项,设置protoc编译器的优化级别
    • SPEED:高度优化代码,运行效率最高,但是会占用更多空间,是默认选项
    • CODE_SIZE:减少空间占用,但是会降低代码都运行效率,如果proto文件比较多,并且对效率要求不高时,建议启用该选项
    • LITE_RUNTIME:生成的代码效率高,空间占用也少,但是只提供序列化与反序列化接口,也就是只继承MessageLite类,不再提供Reflection接口

示例:

syntax = "proto3";
package test_pkg;

option optimize_for = CODE_SIZE;

message Person {
    int32 id = 1;
}

此时该proto文件就会以CODE_SIZE模式进行编译,此时空间占用降低,但是代码效率也会降低。


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2213774.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ubuntu中多cuda版本兼容问题

当ubuntu中已经有老版本的cuda时&#xff0c;按正常步骤直接下载新的cuda和cudnn&#xff0c;只需要注意在下载新的cuda版本时&#xff0c;出现“A symlink already exists at /usr/local/cuda. Update to this installation?”&#xff0c;选择“no”&#xff0c;之后按如下的…

【华为HCIP实战课程十二】OSPF网络中1类2类LSA SPF详解,网络工程师

一、OSPF 1类LSA详解 1、通告者(产生LSA的设备):任何一台设备都会产生1类LSA 2、通告的范围:区域内部 3、功能和内容:产生拓扑信息和路由信息 LSA是OSPF链路状态信息的载体 4、每台OSPF路由器使用一条Router-LSA描述本区域内的链路状态信息 Type :LSA类型,Router-L…

Java学习Day45:兰喜村(Redis)

1.redis概念 1.是什么 redis&#xff08;c语言开发的高并发键值对数据库&#xff09;是nosql的一种&#xff0c;是键值存储数据库&#xff1b; 其核心概念是三高&#xff1a;高并发&#xff0c;高可用性和高扩展性&#xff1b; 优点&#xff1a;快速存取高并发 缺点&#…

Echart自定义饼图

const chartOption computed(() > {return {//与容器边距// grid: {// left: 3%,// right: 4%,// bottom: 3%,// containLabel: true// },// 自定义鼠标悬浮显示内容tooltip: {trigger: item,formatter: function (params: any) {return ${params.value} 个},textS…

新手铲屎官提问,如何在双十一选到性价比高的宠物空气净化器

不知不觉就已经迎来了双十一&#xff0c;这一年即将到头了&#xff0c;意味着我养猫已经是第五个年头了。 当初养猫的时候&#xff0c;就看中了长毛类型的猫&#xff0c;因为感觉摸起来会更舒服&#xff0c;美型到舒服确实是舒服了&#xff0c;但是面临的挑战也不少。其中浮毛…

电脑显示d3dcompiler_47.dll缺失如何修复,马上教你6个修复方法

在用电脑的时候&#xff0c;很多人就遇到过一个叫“计算机缺失d3dcompiler47.dll”的错误提示。在详细解读计算机缺失d3dcompiler_47.dll问题时&#xff0c;我们首先需要了解这个文件的作用&#xff0c;以及缺失d3dcompiler_47.dll对系统的影响和解决方法。 一&#xff0c;d3dc…

『网络游戏』数据库增加主角属性【27】

打开数据库设计表 添加字段 修改服务器脚本&#xff1a;GameMsg.cs 修改服务器脚本&#xff1a;DBMgr.cs 运行服务端 运行客户端 - 点击创建角色进入游戏后左上角的主角UI被打开暂未设计 刷新查看数据库信息 本章结束

VHDL基本结构和逻辑示例

VHDL基本结构和逻辑示例 1.VHDL的基本结构 VHDL的基本结构包含了三段&#xff1a; -- library and package -- entity -- architecturelibrary and package&#xff1a;相关库和软件包&#xff08;相当与c语言的头文件&#xff09; entity&#xff1a;实体&#xff08;描述输…

三、Anaconda 的使用

Anaconda 的使用 前言一、Anaconda 环境使用1.1 虚拟环境操作1.2 使用镜像源 二、PyCharm配置Anaconda环境2.1 第一步2.2 第二步2.3 第三步2.4 第四步2.5 第五步2.6 第六步2.7 第七步2.8 第八步 总结 前言 如果在一个环境中&#xff0c;我们做了多个项目的话&#xff0c;那么后…

SHAP 依赖图

SHAP 依赖图 SHAP 依赖图用于可视化单个特征对机器学习模型预测结果的影响&#xff0c;具体来说&#xff0c;x 轴是特征值&#xff0c;y 轴是 SHAP 值&#xff08;度量特征对预测结果的重要性&#xff09;&#xff0c;这些图可以直观地显示出某个特征是对模型预测起正向还是负…

web前端-----html5----用户注册

以改图为例 <!DOCTYPE html> <html lang"en"> <head> <meta charset"UTF-8"> <meta name"viewport" content"widthdevice-width, initial-scale1.0"> <title>用户注册</title> </hea…

计算机网络:数据链路层 —— 扩展共享式以太网

文章目录 共享式以太网共享式以太网存在的问题在物理层扩展以太网扩展站点与集线器之间的距离扩展共享式以太网的覆盖范围和站点数量 在链路层扩展以太网网桥的主要结构网桥的基本工作原理透明网桥自学习和转发帧生成树协议STP 共享式以太网 共享式以太网是当今局域网中广泛采…

uni-app基础语法(一)

我们今天的学习目标 基础语法1. 创建新页面2.pages配置页面3.tabbar配置4.condition 启动模式配置 基础语法 1. 创建新页面 2.pages配置页面 属性类型默认值描述pathString配置页面路径styleObject配置页面窗口表现&#xff0c;配置项参考pageStyle 我们来通过style修改页面的…

CASA(Carnegie-Ames-Stanford Approach) 模型原理及实践技术

植被作为陆地生态系统的重要组成部分对于生态环境功能的维持具有关键作用。植被净初级生产力&#xff08;Net Primary Productivity, NPP&#xff09;是指单位面积上绿色植被在单位时间内由光合作用生产的有机质总量扣除自养呼吸的剩余部分。植被NPP是表征陆地生态系统功能及可…

C语言:在Visual Studio中使用C语言scanf输入%s出现的栈溢出问题

学了C之后就很少使用C语言了&#xff0c;今天帮同学解答C语言问题&#xff0c;遇到了一个我以前没有遇到过的问题。 一、问题描述 先看以下代码&#xff1a; #include<stdio.h> int main() {char str[100] { 0 };scanf_s("%s", str);printf("%s",…

2024 年 04 月编程语言排行榜,PHP 排名创新低?

编程语言的流行度总是变化莫测&#xff0c;每个月的排行榜都揭示着新的趋势。2024年4月的编程语言排行榜揭示了一个引人关注的现象&#xff1a;PHP的排名再次下滑&#xff0c;创下了历史新低。这种变化对于PHP开发者和整个技术社区来说&#xff0c;意味着什么呢&#xff1f; P…

Java Maven day1014

ok了家人们&#xff0c;今天学习了如何安装和配置Maven项目&#xff0c;我们一起去看看吧 一.Maven概述 1.1 Maven作用 Maven 是专门用于管理和构建 Java 项目的工具&#xff0c;它的主要功能有&#xff1a; 提供了一套标准化的项目结构 提供了一套标准化的构建流程&#x…

力扣41~45题

题41&#xff08;困难&#xff09;&#xff1a; 分析&#xff1a; 这题我开始没什么思路,记录第一个逼我看评论的&#xff0c;后面看评论的方法&#xff0c;真解&#xff0c;借助一个数组&#xff0c;将nums对应数字放对应位置&#xff0c;然后如果下标和数字不同就返回 pyth…

支撑每秒数百万订单无压力,SpringBoot + Disruptor 太猛了!

文章目录 一、支撑每秒数百万订单无压力&#xff0c;SpringBoot Disruptor 太猛了&#xff01;二、项目环境配置1.Maven 配置 (pom.xml)2.Yaml 配置 (application.yml)3.Disruptor 的核心实现4.定义事件工厂&#xff08;OrderEventFactory&#xff09;5.定义事件处理器&#x…

概率 随机变量以及分布

一、基础定义及分类 1、随机变量 随机变量是一个从样本空间&#xff08;所有可能结果的集合&#xff09;到实数集的函数。&#xff08;随机变量的值可以是离散的&#xff0c;也可以是连续的。 &#xff09; 事件可以定义为随机变量取特定值的集合。 2、离散型随机变量 随机变…