C++中protobuffer的具体使用方法以及重要原理的实现

2024-09-04 22:20

本文主要是介绍C++中protobuffer的具体使用方法以及重要原理的实现,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一、protobuffer的具体使用

对于基本的知识可以看我之前的文章。 那一片文章主要是知识点,这一片是实战。

1、头部

我们通过syntax 这个来指定版本号,如果不写的话就会默认为proto2,2这个版本是一个比较旧的版本。旧的版本写起来就比较繁琐。

syntax = "proto3";    // 默认是proto2
package tutorial;     // package类似C++命名空间   比如:  std::cout  这样// 可以引用本地的,也可以引用include里面的,已经写好的proto文件是可以引用
import "google/protobuf/timestamp.proto";    //这里的是用来调用Timestamp 类型适合高精度时间戳的场合

2、优化

        2.1、speed:这是默认的优化级别,他生成代码注重于解码和编码的速度,但是可能专用较多的内存和生成较大的二进制文件。

        2.2、code_size:这个和speed正好相反,这个级别生成的代码较小,适合于对于代码大小有严格要求的场景,但是,他可能会牺牲一些性能。

        2.3、lite_runtime:这个级别旨在生成尽可能小的运行库,同时保持合理的解码和编码性能。它特别适用于移动设备和资源受限的环境,因为减少代码大小可以减少应用的内存占用和加载时间。

option optimize_for = LITE_RUNTIME;         //编译优化选项
option optimize_for = SPEED;
option optimize_for = CODE_SIZE;

3、生成位置

  其中路径1为.proto所在的路径,路径2为.cc和.h生成的位置。将指定proto文件生成.pb.cc和.pb.h。

protoc -I=/路径1 --cpp_out=./路径2 /路径1/addressbook.protoprotoc -I=./ --cpp_out=./ addressbook.proto    //生成到当前目录中//将对应目录的所有proto文件生成.pb.cc和.pb.h
protoc -I=./ --cpp_out=./ *.proto

4、定制选项

当需要用到以下语言的时候,就可以定制特定语言的可选选项。

//下面是针对不同的语言进行的优化选项,或者是定制的选项
// [START java_declaration]
option java_package = "com.example.tutorial";
option java_outer_classname = "AddressBookProtos";
// [END java_declaration]// [START csharp_declaration]
option csharp_namespace = "Google.Protobuf.Examples.AddressBook";
// [END csharp_declaration]

 5、主体部分

我们通过创造通讯录的方法来进行实战。其中protobuffer的写法类似于C++中类的写法。我们来看看这里面都写了什么吧:

        首先就是创建类,可以写上类的名字,接下来就是保留id号,可以保留以后想要使用的id号,这样就可以避免不小心使用掉这些数字。下面就是指定名称了,他们每个名称都要通过这些编号进行搜索。

        当使用的是旧版本,我们需要在每一个名称前面进行加入关键字:singular,repeated,option。但是新版本就解决了这个小问题,可以不用全部加了,减轻负担,默认都是singular,但是特殊的需要加。

        接下来就是enum的枚举类型了,比如我们想让用户写入的是我们指定的数据,那么就可以通过枚举来实现。

        然后就是嵌套类型了,可以多种不同的类型进行嵌套,后面就是oneof的类型,主要是解决多个字段的中的某一个,当选择了其中一个之后别的别的就会自动进行删除操作,这样可以节省内存。


// 通讯录
// [START messages]  
message Person {    // message类似C++的classreserved 8,15,9 to 11;        //这里的reserved是保留字reserved "foo","bar";string name   = 1;  // 名字int32 id      = 2;  // Unique ID number for this person. 每个人的唯一idstring email  = 3;enum PhoneType {    // enum 枚举类型MOBILE = 0;HOME = 1;WORK = 2;}message PhoneNumber {string number = 1;    // 字符串 电话号码PhoneType type = 2;   //}repeated PhoneNumber phones = 4;    // 重复0~多个,一个人有多个电话   在前面加入repeated 后,这个就可以支持多个数据。google.protobuf.Timestamp last_updated = 5; // import "google/protobuf/timestamp.proto"   这里是一个时间戳类型的值。
}// Our address book file is just one of these.
message AddressBook {repeated Person people = 1;   // 电话簿有多人的电话message Samplemessage{oneof test_oneof{string name = 2;submessage sub_message = 3;}
}
// [END messages]

二、重要原理-编码原理

        首先我们要理解变长编码(Varints:base128)和固定编码。举个例子:int32_t :0x12345678占用四字节,int32_t:0x12 占用四字节,这里都是占用的四字节,这里采用的就是定长编码,当使用变长编码,0x12345678占用四字节,而0x12 占用一字节。

        原理:base128 使用每个字节的最高有效位作为标志位, 而剩余的 7 位以二进制补码的形式来存储数字值本身, 当最高有效位为 1 时, 代表其后还跟有字节, 当最高有效位为 0 时, 代表已经是该数字的最后的一个字节;

        我们假设数字1的int类型占四个字节。那么他的标准整型存储的二进制应该是这样的:

        可以看到只有最后一个字节存储了有效数值,前三个字节都是0。

        为什么设计变⻓编码:普通的 int 数据类型, 无论其值的大小, 所占用的存储空间都是相等的,比如不管是0x12345678 还是0x12都占用4字节,那能否让0x12在表示的时候只占用1个字节呢?

        是否可以根据数值的大小来动态地占用存储空间, 使得值比较小的数字占用较少的字节数, 值相对比较大的数字占用较多的字节数, 这即是变⻓整型编码的基本思想。

        采用变⻓整型编码的数字, 其占用的字节数不是完全一致的, Varints 编码使用每个字节的最高有效位作为标志位, 而剩余的 7 位以二进制补码的形式来存储数字值本身, 当最高有效位为 1 时, 代表其后还跟有字节, 当最高有效位为 0 时, 代表已经是该数字的最后的一个字节。

如果采用Varints编码,那么二进制就是:

 我们可以再举个例子:666 。从下面的编码解码过程可以看出, 可变⻓整型编码对于不同大小的数字, 其所占用的存储空间是不同的。

三、protobuffer的总结

1:Protobuf 采用 Varints 编码和 Zigzag 编码来编码数据, 其中 Varints 编码的思想是移除数字高位的 0, 用变⻓的二进制位来描述一个数字, 对于小数字, 其编码⻓度短, 可提高数据传输效率, 但由于它在每个字节的最高位额外采用了一个标志位来标记其后是否还跟有有效字节, 因此对于大的正数, 它会比使用普通的定⻓格式占用更多的空间, 另外对于负数, 直接采用 Varints编码将恒定占用 10 个字节, Zigzag 编码可将负数映射为无符号的正数, 然后采用 Varints 编码进行数据压缩, 在各种语言的 Protobuf 实现中, 对于 int32 类型的数据, Protobuf 都会转为 uint64 而后使用 Varints 编码来处理, 因此当字段可能为负数时,我们应使用 sint32 或 sint64, 这样 Protobuf 会按照 Zigzag 编码将数据变换后再采用 Varints 编码进行压缩, 从而缩短数据的二进制位数。

2:Protobuf 不是完全自描述的信息描述格式, 接收端需要有相应的解码器(即 proto 定义)才可解析数据格式, 序列化后的 Protobuf 数据不携带字段名, 只使用字段编号来标识一个字段, 因此更改 proto 的字段名不会影响数据解析(但这显然不是一种好的行为), 字段编号会被编码进二进制的消息结构中, 因此我们应尽可能地使用小字段编号。

3:Protobuf 是一种紧密的消息结构, 编码后字段之间没有间隔, 每个字段头由两部分组成: 字段编号和 wire type, 字段头可确定数据段的⻓度, 因此其字段之前无需加入间隔, 也无需引入特定的数据来标记字段末尾, 因此 Protobuf 的编码⻓度短, 传输效率高。

四、协议升级

当我们要将之前的protobuffer进行升级的时候,我们就会体会到他的优势了,我们需要满足一些规则:

1:不要修改之前字段的数据结构。

2:如果您添加新字段,则任何由代码使用“旧”消息格式序列化的消息仍然可以通过新生成的代码进行分析。您应该记住这些元素的默认值,以便新代码可以正确地与旧代码生成的消息进行交互。同样,由新代码创建的消息可以由旧代码解析:旧的二进制文件在解析时会简单地忽略新字段。

3:int32,uint32,int64,uint64 和 bool 全都兼容。这意味着您可以将字段从这些类型之一更改为另一个字段而不破坏向前或向后兼容性。如果一个数字从不适合相应类型的线路中解析出来,则会得到与在 C++ 中将该数字转换为该类型相同的效果(例如,如果将 64 位数字读为 int32,它将被截断为 32 位)。

4:enum 就数组而言,是可以与 int32,uint32,int64 和 uint64 兼容(请注意,如果它们不适合,值将被截断)。但是请注意,当消息反序列化时,客户端代码可能会以不同的方式对待它们:例如,未识别的 proto3 枚举类型将保留在消息中,但消息反序列化时如何表示是与语言相关的。(这点和语言相关,上面提到过了)Int 域始终只保留它们的值。

5:将单个值更改为新的成员是安全和二进制兼容的。如果您确定一次没有代码设置多个字段,则将多个字段移至新的字段可能是安全的。将任何字段移到现有字段中都是不安全的。(注意字段和值的区别,字段是 field,值是 value)。

这篇关于C++中protobuffer的具体使用方法以及重要原理的实现的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1137192

相关文章

python设置环境变量路径实现过程

《python设置环境变量路径实现过程》本文介绍设置Python路径的多种方法:临时设置(Windows用`set`,Linux/macOS用`export`)、永久设置(系统属性或shell配置文件... 目录设置python路径的方法临时设置环境变量(适用于当前会话)永久设置环境变量(Windows系统

python使用try函数详解

《python使用try函数详解》Pythontry语句用于异常处理,支持捕获特定/多种异常、else/final子句确保资源释放,结合with语句自动清理,可自定义异常及嵌套结构,灵活应对错误场景... 目录try 函数的基本语法捕获特定异常捕获多个异常使用 else 子句使用 finally 子句捕获所

C++11范围for初始化列表auto decltype详解

《C++11范围for初始化列表autodecltype详解》C++11引入auto类型推导、decltype类型推断、统一列表初始化、范围for循环及智能指针,提升代码简洁性、类型安全与资源管理效... 目录C++11新特性1. 自动类型推导auto1.1 基本语法2. decltype3. 列表初始化3

C++11右值引用与Lambda表达式的使用

《C++11右值引用与Lambda表达式的使用》C++11引入右值引用,实现移动语义提升性能,支持资源转移与完美转发;同时引入Lambda表达式,简化匿名函数定义,通过捕获列表和参数列表灵活处理变量... 目录C++11新特性右值引用和移动语义左值 / 右值常见的左值和右值移动语义移动构造函数移动复制运算符

Python对接支付宝支付之使用AliPay实现的详细操作指南

《Python对接支付宝支付之使用AliPay实现的详细操作指南》支付宝没有提供PythonSDK,但是强大的github就有提供python-alipay-sdk,封装里很多复杂操作,使用这个我们就... 目录一、引言二、准备工作2.1 支付宝开放平台入驻与应用创建2.2 密钥生成与配置2.3 安装ali

Spring Security 单点登录与自动登录机制的实现原理

《SpringSecurity单点登录与自动登录机制的实现原理》本文探讨SpringSecurity实现单点登录(SSO)与自动登录机制,涵盖JWT跨系统认证、RememberMe持久化Token... 目录一、核心概念解析1.1 单点登录(SSO)1.2 自动登录(Remember Me)二、代码分析三、

C#中lock关键字的使用小结

《C#中lock关键字的使用小结》在C#中,lock关键字用于确保当一个线程位于给定实例的代码块中时,其他线程无法访问同一实例的该代码块,下面就来介绍一下lock关键字的使用... 目录使用方式工作原理注意事项示例代码为什么不能lock值类型在C#中,lock关键字用于确保当一个线程位于给定实例的代码块中时

MySQL 强制使用特定索引的操作

《MySQL强制使用特定索引的操作》MySQL可通过FORCEINDEX、USEINDEX等语法强制查询使用特定索引,但优化器可能不采纳,需结合EXPLAIN分析执行计划,避免性能下降,注意版本差异... 目录1. 使用FORCE INDEX语法2. 使用USE INDEX语法3. 使用IGNORE IND

C# $字符串插值的使用

《C#$字符串插值的使用》本文介绍了C#中的字符串插值功能,详细介绍了使用$符号的实现方式,文中通过示例代码介绍的非常详细,需要的朋友们下面随着小编来一起学习学习吧... 目录$ 字符使用方式创建内插字符串包含不同的数据类型控制内插表达式的格式控制内插表达式的对齐方式内插表达式中使用转义序列内插表达式中使用

C#中的Converter的具体应用

《C#中的Converter的具体应用》C#中的Converter提供了一种灵活的类型转换机制,本文详细介绍了Converter的基本概念、使用场景,具有一定的参考价值,感兴趣的可以了解一下... 目录Converter的基本概念1. Converter委托2. 使用场景布尔型转换示例示例1:简单的字符串到