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

相关文章

Java中流式并行操作parallelStream的原理和使用方法

《Java中流式并行操作parallelStream的原理和使用方法》本文详细介绍了Java中的并行流(parallelStream)的原理、正确使用方法以及在实际业务中的应用案例,并指出在使用并行流... 目录Java中流式并行操作parallelStream0. 问题的产生1. 什么是parallelS

MySQL数据库双机热备的配置方法详解

《MySQL数据库双机热备的配置方法详解》在企业级应用中,数据库的高可用性和数据的安全性是至关重要的,MySQL作为最流行的开源关系型数据库管理系统之一,提供了多种方式来实现高可用性,其中双机热备(M... 目录1. 环境准备1.1 安装mysql1.2 配置MySQL1.2.1 主服务器配置1.2.2 从

C++中unordered_set哈希集合的实现

《C++中unordered_set哈希集合的实现》std::unordered_set是C++标准库中的无序关联容器,基于哈希表实现,具有元素唯一性和无序性特点,本文就来详细的介绍一下unorder... 目录一、概述二、头文件与命名空间三、常用方法与示例1. 构造与析构2. 迭代器与遍历3. 容量相关4

Linux join命令的使用及说明

《Linuxjoin命令的使用及说明》`join`命令用于在Linux中按字段将两个文件进行连接,类似于SQL的JOIN,它需要两个文件按用于匹配的字段排序,并且第一个文件的换行符必须是LF,`jo... 目录一. 基本语法二. 数据准备三. 指定文件的连接key四.-a输出指定文件的所有行五.-o指定输出

Java中Redisson 的原理深度解析

《Java中Redisson的原理深度解析》Redisson是一个高性能的Redis客户端,它通过将Redis数据结构映射为Java对象和分布式对象,实现了在Java应用中方便地使用Redis,本文... 目录前言一、核心设计理念二、核心架构与通信层1. 基于 Netty 的异步非阻塞通信2. 编解码器三、

Linux jq命令的使用解读

《Linuxjq命令的使用解读》jq是一个强大的命令行工具,用于处理JSON数据,它可以用来查看、过滤、修改、格式化JSON数据,通过使用各种选项和过滤器,可以实现复杂的JSON处理任务... 目录一. 简介二. 选项2.1.2.2-c2.3-r2.4-R三. 字段提取3.1 普通字段3.2 数组字段四.

C++中悬垂引用(Dangling Reference) 的实现

《C++中悬垂引用(DanglingReference)的实现》C++中的悬垂引用指引用绑定的对象被销毁后引用仍存在的情况,会导致访问无效内存,下面就来详细的介绍一下产生的原因以及如何避免,感兴趣... 目录悬垂引用的产生原因1. 引用绑定到局部变量,变量超出作用域后销毁2. 引用绑定到动态分配的对象,对象

Linux kill正在执行的后台任务 kill进程组使用详解

《Linuxkill正在执行的后台任务kill进程组使用详解》文章介绍了两个脚本的功能和区别,以及执行这些脚本时遇到的进程管理问题,通过查看进程树、使用`kill`命令和`lsof`命令,分析了子... 目录零. 用到的命令一. 待执行的脚本二. 执行含子进程的脚本,并kill2.1 进程查看2.2 遇到的

SpringBoot基于注解实现数据库字段回填的完整方案

《SpringBoot基于注解实现数据库字段回填的完整方案》这篇文章主要为大家详细介绍了SpringBoot如何基于注解实现数据库字段回填的相关方法,文中的示例代码讲解详细,感兴趣的小伙伴可以了解... 目录数据库表pom.XMLRelationFieldRelationFieldMapping基础的一些代

Java HashMap的底层实现原理深度解析

《JavaHashMap的底层实现原理深度解析》HashMap基于数组+链表+红黑树结构,通过哈希算法和扩容机制优化性能,负载因子与树化阈值平衡效率,是Java开发必备的高效数据结构,本文给大家介绍... 目录一、概述:HashMap的宏观结构二、核心数据结构解析1. 数组(桶数组)2. 链表节点(Node