Win10 下 ProtoBuf 安装编译以及在 C++ 中的用法

2023-10-31

ProtoBuf

  • Protocol Buffer ( protoBuf 或 PB )是 google 的一种数据交换的格式,它独立于语言,独立于平台。
  • google 提供了多种语言的实现:java、c#、c++、go 和 python,每一种实现都包含了相应语言的编译器以及库文件。
  • 由于它是一种二进制的格式,比使用 XML 进行数据交换快许多,可以把它用于分布式应用之间的数据通信或者异构环境下的数据交换,作为一种效率和兼容性都很优秀的二进制数据传输格式,可以用于诸如网络传输、配置文件、数据存储等诸多领域。

优点

  • 相比XML,它更小、更快、也更简单。可以定义自己的数据结构,然后使用代码生成器生成的代码来读写这个数据结构,甚至可以在无需重新部署程序的情况下更新数据结构,只需使用 Protobuf 对数据结构进行一次描述,即可利用各种不同语言或从各种不同数据流中对你的结构化数据轻松读写。
  • “向后”兼容性好。不必破坏已部署的、依靠“老”数据格式的程序就可以对数据结构进行升级,这样程序就可以不必担心因为消息结构的改变而造成的大规模的代码重构或者迁移的问题,因为添加新的消息中的 field 并不会引起已经发布的程序的任何改变
  • Protobuf 语义更清晰。无需类似 XML 解析器的东西,Protobuf 编译器就可将 .proto 文件编译生成对应的数据访问类以对 Protobuf 数据进行序列化、反序列化操作。
  • 使用 Protobuf 无需学习复杂的文档对象模型。Protobuf 的编程模式比较友好,简单易学,同时它拥有良好的文档和示例。

缺点

  • 相比XML,它功能简单,无法用来表示复杂的概念
  • XML 已经成为多种行业标准的编写工具,Protobuf 只是 Google 公司内部使用的工具,在通用性上还差很多。
  • 由于文本并不适合用来描述数据结构,所以 Protobuf 也不适合用来对基于文本的标记文档(如 HTML)建模。
  • 由于 XML 具有某种程度上的自解释性,它可以被人直接读取编辑,在这一点上 Protobuf 不行,它以二进制的方式存储,除非你有 .proto 定义,否则你没法直接读出 Protobuf 的任何内容。

用法

.proto 文件是 ProtoBuf 一个重要的文件,它定义了需要序列化数据的结构,用法步骤:

  • 在.proto文件中定义消息格式
  • 用 ProtoBuf 编译器编译 .proto 文件
  • 用 C++ 对应的 ProtoBuf API 来写或者读消息

下载

下载地址:https://github.com/protocolbuffers/protobuf/releases
下载的安装包:protoc-3.9.0-win32.zip、protoc-3.9.0-win64.zip
操作系统:WIN10 64位
下载的源码包:protobuf-3.9.0.zip

安装使用

1、解压protoc-3.9.0-win32.zip到任意目录

2、将解压目录 <path>\protoc-3.9.0-win32\bin 配置到环境变量path下

3、在命令行下即可使用 protoc 命令编译 .proto 文件生成 C++ 对应的 .h 和 .cc 文件

在 cmd 命令行中进行:

> protoc --version    #查看protoc的版本
#可以先 cd 到指定目录,然后设置为相对路径
> protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/xxx.proto #编译 .proto 文件
#-I:主要用于指定待编译的 .proto 消息定义文件所在的目录,即可能出现的包含文件的路径,该选项可以被同时指定多个,此处指定的路径不能为空,如果是当前目录,直接使用.,如果是子目录,直接使用子目录相对径

编译流程

操作系统:WIN10 64位
编译工具:VS 2015
在 Win 上编译需要用到 CMake :https://cmake.org/download/
Win 上编译使用流程:
1、解压源码到任意路径下 <path>\protobuf-3.9.0下

2、到官网(或国内下载站)下载 CMake,并安装

3、CMake 生成 VS 2015 工程

  • 打开CMake
  • 设置源码路径下的cmake目录 <path>/protobuf-3.9.0/cmake
  • 设置任意构建目录 <path>/probobuf_build
  • 点 configure 按钮
  • 选择对应的VS,这里选的是VS 2015,选择编译为WIN32,编译器默认
  • 点击 Finish 按钮,开始自动编译(只要不报error表示顺利,否则根据日志查找原因)
  • 点击 Generate 按钮生成VS项目
  • 用VS打开生成的工程,根据需要选择编译libprotobuf、libprotobuf-lite、libprotoc和protoc项目
  • 编译完成

4、拷贝所需lib文件和protoc.exe到对应的项目中

5、 .proto文件编译生成对应的.h和.cc文件

6、引入自己的工程使用

语法规则

1、编码规范

  • 描述文件以.proto做为文件后缀。
  • 结构定义包括:message、service、enum,这些以外的语句以分号结尾,rpc方法定义结尾的分号可有可无。
  • message 命名采用驼峰命名方式,字段命名采用小写字母加下划线分隔方式。
  • enums 类型名采用驼峰命名方式,字段命名采用大写字母加下划线分隔方式。
  • service与rpc方法名统一采用驼峰式命名。

2、注释
提供以下两种注释方式:

// 单行注释
/* 多行注释 */

3、默认值
解析消息时,如果编码消息不包含特定的单数元素,则解析对象中的相应字段将设置为该字段的默认值,这些默认值是特定于类型的。

  • 对于字符串类型,默认值为空字符串
  • 对于字节类型,默认值为空字节
  • 对于布尔类型,默认值为false
  • 对于数字类型,默认值为零
  • 对于枚举,默认值是第一个定义的枚举值,该值必须为0
  • 对于消息字段,未设置该字段,它的确切值取决于语言
  • 重复字段的默认值为空(通常是相应语言的空列表)

4、message定义以及编译生成 C++ 文件

  • 在.proto文件定义消息,message是.proto文件最小的逻辑单元,由一系列name-value键值对构成。
package hw; 
message test
{
	required int32 id = 1;
	required string str = 2;
	optional int32 opt = 3;//可选字段
	repeated string phone_num = 4;
}
  • message消息包含一个或多个编号唯一的字段,每个字段由【字段限制+字段类型+字段名+编号】组成,字段限制分为【optional(可选的)、required(必须的)、repeated(重复的)】
  • required关键字 表示是一个必须字段,必须相对于发送方,在发送消息之前必须设置该字段的值,对于接收方,必须能够识别该字段的意思。发送之前没有设置required字段或者无法识别required字段都会引发编解码异常,导致消息被丢弃。
  • optional关键字 字面意思是可选的意思,具体protobuf里面怎么处理这个字段呢,就是protobuf处理的时候另外加了一个bool的变量,用来标记这个optional字段是否有值,发送方在发送的时候,如果这个字段有值,那么就给bool变量标记为true,否则就标记为false,接收方在收到这个字段的同时,也会收到发送方同时发送的bool变量,拿着bool变量就知道这个字段是否有值了,可选对于发送方,在发送消息时,可以有选择性的设置或者不设置该字段的值。对于接收方,如果能够识别可选字段就进行相应的处理,如果无法识别,则忽略该字段,消息中的其它字段正常处理。optional字段的特性,很多接口在升级版本中都把后来添加的字段都统一的设置为optional字段,这样老的版本无需升级程序也可以正常的与新的软件进行通信,只不过新的字段无法识别而已,因为并不是每个节点都需要新的功能,因此可以做到按需升级和平滑过渡。
  • repeated关键字 表示该字段可以包含0~N个元素。每一次可以包含多个值,可以看作是在传递一个数组的值。也是optional字段一样,另外加了一个count计数变量,用于标明这个字段有多少个,这样发送方发送的时候,同时发送了count计数变量和这个字段的起始地址,接收方在接受到数据之后,按照count来解析对应的数据即可。
  • 定义好消息后,使用ProtoBuf编译器生成C++对应的.h和.cc文件(hw.test.pb.h,hw.test.pb.cc),源文件提供了message消息的序列化和反序列化等方法,在生成的头文件中定义了一个 C++ 类,该类提供了一系列操作定义结构的方法。
protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/hw.test.proto

5、嵌套以及导入message

import hw.test;//用 Import 关键字引入在其他 .proto 文件中定义的消息
package addresslist; 
message Person 
{
	//1到15范围内的字段编号需要一个字节进行编码,包括字段编号和字段类型
	//16到2047范围内的字段编号占用两个字节
	//应该为非常频繁出现的消息元素保留数字1到15
	//要为将来可能添加的常用元素留出一些空间
	required string name = 1;
	required int32 id = 2;
	optional string email = 3;
	enum PhoneType //定义枚举的时候,我们要保证第一个枚举值必须是0,枚举值不能重复,
	//除非使用 option allow_alias = true 选项来开启别名
	//枚举值的范围是32-bit integer,但因为枚举值使用变长编码,
	//所以不推荐使用负数作为枚举值,因为这会带来效率问题
	{
		MOBILE = 0;
		HOME = 1;
		WORK = 2;
	}
	message PhoneNumber //定义嵌套消息
	{
		required string number = 1;
		optional PhoneType type = 2 [default = HOME];
	}
	repeated PhoneNumber phone = 4;
}

6、map映射
如果要在数据定义中创建关联映射,Protocol Buffers提供了一种方便的语法:

map< key_type, value_type> map_field = N ;

其中key_type可以是任何整数或字符串类型。

  • 枚举不是有效的key_type,value_type可以是除map映射类型外的任何类型。
  • map的字段可以是repeated。
  • 序列化后的顺序和map迭代器的顺序是不确定的,所以你不要期望以固定顺序处理map。
  • 当为.proto文件产生生成文本格式的时候,map会按照key 的顺序排序,数值化的key会按照数值排序。
  • 从序列化中解析或者融合时,如果有重复的key则后一个key不会被使用,当从文本格式中解析map时,如果存在重复的key,则解析可能会失败。
  • 如果为映射字段提供键但没有值,则字段序列化时的行为取决于语言。
  • 在Python中,使用类型的默认值。

7、oneof
如果你的消息中有很多可选字段, 并且同时至多一个字段会被设置, 你可以加强这个行为,使用oneof特性节省内存。为了在.proto定义oneof字段, 你需要在名字前面加上oneof关键字

message testMessage 
{
	oneof test_oneof 
	{
		string name = 4;
		SubMessage sub_message = 9;
	}
}

然后你可以增加oneof字段到 oneof 定义中. 你可以增加任意类型的字段。

注意Oneof特性:

  • oneof字段只有最后被设置的字段才有效,即后面的set操作会覆盖前面的set操作
  • oneof不可以是repeated的
  • 反射API可以作用于oneof字段
  • 如果使用C++要防止内存泄露,即后面的set操作会覆盖之前的set操作,导致前面设置的字段对象发生析构,要注意字段对象的指针操作
  • 如果使用C++的Swap()方法交换两条oneof消息,两条消息都不会保存之前的字段
  • 向后兼容。添加或删除oneof字段的时候要注意,如果检测到oneof字段的返回值是None/NOT_SET,这意味着oneof没有被设置或者设置了一个不同版本的oneof的字段,但是没有办法能够区分这两种情况,因为没有办法确认一个未知的字段是否是一个oneof的成员
  • 编号复用问题。1)删除或添加字段到oneof:在消息序列化或解析后会丢失一些信息,一些字段将被清空;2)删除一个字段然后重新添加:在消息序列化或解析后会清除当前设置的oneof字段;3)分割或合并字段:同普通的删除字段操作

8、Any(任意消息类型)
Any类型是一种不需要在.proto文件中定义就可以直接使用的消息类型,使用前import google/protobuf/any.proto文件即可。

import  "google/protobuf/any.proto";

message ErrorStatus  {
  string message =  1;
  repeated google.protobuf.Any details =  2;
}

C++使用PackFrom()和UnpackTo()方法来打包和解包Any类型消息。

// Storing an arbitrary message type in Any.
NetworkErrorDetails details =  ...;
ErrorStatus status;
status.add_details()->PackFrom(details);

// Reading an arbitrary message from Any.
ErrorStatus status =  ...;
for  (const  Any& detail : status.details())  
{  
	if  (detail.Is<NetworkErrorDetails>())  
	{    
		NetworkErrorDetails network_error;
    	detail.UnpackTo(&network_error);
        ... 
        processing network_error 
        ...  
	}
}

9、定义服务
如果想在RPC系统中使用消息类型,就需要在.proto文件中定义RPC服务接口,然后使用编译器生成对应语言的存根。

service SearchService {
  rpc Search (SearchRequest) returns (SearchResponse);
}

10、更新一个数据类型
在实际的开发中会存在这样一种应用场景,既消息格式因为某些需求的变化而不得不进行必要的升级,但是有些使用原有消息格式的应用程序暂时又不能被立刻升级,这便要求我们在升级消息格式时要遵守一定的规则,从而可以保证基于新老消息格式的新老程序同时运行。规则如下:

  • 不要修改已经存在字段的标签号。
  • 任何新添加的字段必须是optional和repeated限定符,否则无法保证新老程序在互相传递消息时的消息兼容性。
  • 在原有的消息中,不能移除已经存在的required字段,optional和repeated类型的字段可以被移除,但是他们之前使用的标签号必须被保留,不能被新的字段重用。
  • int32、uint32、int64、uint64和bool等类型之间是兼容的,sint32和sint64是兼容的,string和bytes是兼容的,fixed32和sfixed32,以及fixed64和sfixed64之间是兼容的,这意味着如果想修改原有字段的类型时,为了保证兼容性,只能将其修改为与其原有类型兼容的类型,否则就将打破新老消息格式的兼容性。
  • optional和repeated限定符也是相互兼容的。

在 C++ 中的示例

1、定义.proto文件

syntax = "proto2";//不定义默认是这个(有警告),也可以定义成"proto3"

option java_package = "com.demo.myproto";
//使用精简版的protobuf库,需要链接libprotobuf-lite.lib
option optimize_for = LITE_RUNTIME;//生成的可执行程序速度快,体积小,以牺牲Protocol Buffer提供的反射功能为代价

package myproto;

message HelloWorld {
	required int32 id = 1;
	required string str = 2;
	optional int32 opt=3;
}

保存成myproto.proto文件,放到指定目录

2、生成 C++ 文件
打开 cmd 命令行,cd 到指定目录下

> protoc -I ./ --cpp_out=./ myproto.proto #在本目录下转

3、将生成的 C++ 文件加入工程中

4、附加protobuf头文件目录(protobuf源文件 src 下的 google 文件夹引入到工程,生成的 C++ 文件需要)、附加依赖库文件目录、附加依赖库文件名 libprotobuf-lite.lib、设置运行库一致

5、编写测试程序

#include <stdlib.h>
#include <stdio.h>

#include "myproto.pb.h"
#include <google/protobuf/io/zero_copy_stream_impl_lite.h>
#include <google/protobuf/message_lite.h>

/*
message HelloWorld {
	required int32 id = 1;
	required string str = 2;
	optional int32 opt=3;
}
*/

int main()
{
	printf("My Hello World Application !\r\n");
	
	//设置参数
	myproto::HelloWorld in_msg;
	in_msg.set_id(1);
	in_msg.set_str("zab");
	in_msg.set_opt(0);
	
	//对消息进行编码
	int nSize = in_msg.ByteSize() + 8;
	char *pcData = new char[nSize];
	memset(pcData, 0, nSize);
	int nEncodedLen = 0;
	
	google::protobuf::io::ArrayOutputStream array_stream(pcData, nSize);
	google::protobuf::io::CodedOutputStream output_stream(&array_stream);//操作编码时的varints
	//Varint 是一种紧凑的表示数字的方法
	//它用一个或多个字节来表示一个数字,值越小的数字使用越少的字节数
	//这能减少用来表示数字的字节数
	//Varint 中的每个 byte 的最高位 bit 有特殊的含义,如果该位为 1,表示后续的 byte 也是该数字的一部分,如果该位为 0,则结束
	//其他的 7 个 bit 都用来表示数字
	//因此小于 128 的数字都可以用一个 byte 表示
	//大于 128 的数字,会用两个字节
	output_stream.WriteVarint32(in_msg.ByteSize());//写为Varint
	if (in_msg.SerializeToCodedStream(&output_stream)) {
		nEncodedLen = output_stream.ByteCount();
		
		//对消息进行解码
		myproto::HelloWorld out_msg;
		//构造一个InputStream,返回data所指向的字节数组
		//若指定了block_size,则每次调用Next()返回不超过指定大小的数据块;否则,返回整个字节数组
		google::protobuf::io::CodedInputStream input_stream ((google::protobuf::uint8*)pcData, nEncodedLen);//操作解码时的varints
		google::protobuf::uint32 ui_size = 0;
		if (input_stream.ReadVarint32(&ui_size)) {//还原Varint
			google::protobuf::io::CodedInputStream::Limit limit = input_stream.PushLimit(ui_size);//保护在长度范围内解析
			if (out_msg.MergeFromCodedStream(&input_stream)) {//分割填充
				// 当对报文进行解析后,以进一步判断报文是否被以正确方式读取完毕
				if (input_stream.ConsumedEntireMessage()) {
					input_stream.PopLimit(limit);//释放保护
					printf("HelloWorld id=%d,str=%s,opt=%d.\r\n", out_msg.id(), out_msg.str().c_str(), out_msg.opt());
				} else {
					printf("consume msg fail!\r\n");
				}
			} else {
				printf("merge msg stream fail!\r\n");
			}
		} else {
			printf("read msg stream fail!\r\n");
		}
	} else {
		printf("encode msg fail!\r\n");
	}
	
	if (NULL != pcData) {
		delete[] pcData;
		pcData = NULL;
	}
	nEncodedLen = 0;

	printf("My Hello World Application End!\r\n");

	system("pause");

    return 0;
}

ProtoBuf的三种优化级别

optimize_for是文件级别的选项,Protocol Buffer 定义三种优化级别SPEED、CODE_SIZE、LITE_RUNTIME,缺省情况下是SPEED。

  • SPEED: 表示生成的代码运行效率高,但是由此生成的代码编译后会占用更多的空间
  • CODE_SIZE: 和SPEED恰恰相反,代码运行效率较低,但是由此生成的代码编译后会占用更少的空间,通常用于资源有限的平台,如Mobile
  • LITE_RUNTIME: 生成的代码执行效率高,同时生成代码编译后的所占用的空间也是非常少,以牺牲Protocol Buffer提供的反射功能为代价的,在C++中链接Protocol Buffer库时仅需链接libprotobuf-lite,而非libprotobuf,在Java中仅需包含protobuf-java-2.4.1-lite.jar,而非protobuf-java-2.4.1.jar
  • SPEED和LITE_RUNTIME相比,在于调试级别上,例如 msg.SerializeToString(&str) 在SPEED模式下会利用反射机制打印出详细字段和字段值,但是LITE_RUNTIME则仅仅打印字段值组成的字符串,因此可以在程序调试阶段使用 SPEED模式,而上线以后使用提升性能使用 LITE_RUNTIME 模式优化

动态编译

动态编译
一般情况下,使用 Protobuf 的人们都会先写好 .proto 文件,再用 Protobuf 编译器生成目标语言所需要的源代码文件,可是在某且情况下,人们无法预先知道 .proto 文件,他们需要动态处理一些未知的 .proto 文件,比如一个通用的消息转发中间件,它不可能预知需要处理怎样的消息,这需要动态编译 .proto 文件,并使用其中的 Message

无须编译.proto生成.pb.cc和.pb.h,需要使用protobuf中提供的两个头文件

  • protobuf中头文件的位置/usr/local/include/google/protobuf
  • google/protobuf/compiler/importer.h ,google/protobuf/dynamic_message.h

示例

#include <iostream>
#include <string>
#include <google/protobuf/compiler/importer.h>
#include <google/protobuf/dynamic_message.h>

class MyErrorCollector: public google::protobuf::compiler::MultiFileErrorCollector
{
    virtual void AddError(const std::string & filename, int line, int column, const std::string & message){
        // define import error collector
        printf("%s, %d, %d, %s\n", filename.c_str(), line, column, message.c_str());
    }
};

int main()
{
    google::protobuf::compiler::DiskSourceTree sourceTree; // source tree
    sourceTree.MapPath("", "/home/szw/code/protobuf/tmp2"); // initialize source tree
    MyErrorCollector errorCollector; // dynamic import error collector
    google::protobuf::compiler::Importer importer(&sourceTree, &errorCollector); // importer
    
    importer.Import("test.proto");
    
    // find a message descriptor from message descriptor pool
    const google::protobuf::Descriptor * descriptor = importer.pool()->FindMessageTypeByName("lm.helloworld");
    if (!descriptor){
        perror("Message is not found!");
        exit(-1);
    }

    // create a dynamic message factory
    google::protobuf::DynamicMessageFactory * factory = new google::protobuf::DynamicMessageFactory(importer.pool());
    // create a const message ptr by factory method and message descriptor 
    const  google::protobuf::Message * tmp = factory->GetPrototype(descriptor);
    
    // create a non-const message object by const message ptr
    // define import error collector
    google::protobuf::Message * msg = tmp->New();
    
    return 0;
}

其中,头文件<google/protobuf/compiler/importer.h>包含了编译器对象相关,头文件<google/protobuf/dynamic_message.h>包含了Message_Descriptor和Factory相关

  • 首先初始化一个DiskSourceTree对象, 指明目标.proto文件的根目录
  • 创建一个MyErrorCollector对象, 用于收集动态编译过程中产生的编译bug, 该对象需要根据proto提供的纯虚基类派生, 并需要重写其中的纯虚函数, 使之不再是一个抽象类
  • 初始化一个Importer对象, 用于动态编译, 将DiskSourceTree和MyErrorCollector的地址传入
  • 将目标.proto动态编译, 并加入编译器池中
  • 可以通过FindMessageTypeName()和消息名, 来获取到目标消息的Message_Descriptor
  • 创建动态工厂, 并利用工厂方法GetPrototype和目标消息的Message_Descriptor获取一个指针const message *
  • 利用消息实例指针的New()方法获取到non-const message ptr

类型反射
类型反射即是根据字段的名称获取到字段类型的一种机制,protobuf自身便配备了类型反射机制,需要头文件<google/protobuf/descriptor.h>与<google/protobuf/message.h>。

protobuf对于每个元素都有一个相应的descriptor,这个descriptor包含该元素的所有元信息。
在这里插入图片描述

  • FileDescriptor: 对一个proto文件的描述,它包含文件名、包名、选项(如package, java_package, java_outer_classname等)、文件中定义的所有message、文件中定义的所有enum、文件中定义的所有service、文件中所有定义的extension、文件中定义的所有依赖文件(import)等。在FileDescriptor中还存在一个DescriptorPool实例,它保存了所有的dependencies(依赖文件的FileDescriptor)、name到GenericDescriptor的映射、字段到FieldDescriptor的映射、枚举项到EnumValueDescriptor的映射,从而可以从该DescriptorPool中查找相关的信息,因而可以通过名字从FileDescriptor中查找Message、Enum、Service、Extensions等。可以通过 --descriptor_set_out 指定生成某个proto文件相对应的FileDescriptorSet文件。
  • Descriptor: 对一个message定义的描述,它包含该message定义的名字、所有字段、内嵌message、内嵌enum、关联的FileDescriptor等。可以使用字段名或字段号查找FieldDescriptor。
  • FieldDescriptor:对一个字段或扩展字段定义的描述,它包含字段名、字段号、字段类型、字段定义(required/optional/repeated/packed)、默认值、是否是扩展字段以及和它关联的Descriptor/FileDescriptor等。
  • EnumDescriptor:对一个enum定义的描述,它包含enum名、全名、和它关联的FileDescriptor。可以使用枚举项或枚举值查找EnumValueDescriptor
  • EnumValueDescriptor:对一个枚举项定义的描述,它包含枚举名、枚举值、关联的EnumDescriptor/FileDescriptor等。
  • ServiceDescriptor:对一个service定义的描述,它包含service名、全名、关联的FileDescriptor等。
  • MethodDescriptor:对一个在service中的method的描述,它包含method名、全名、参数类型、返回类型、关联的FileDescriptor/ServiceDescriptor等。

动态定义proto
能不能通过程序生成protobuf文件呢?答案是可以的。FileDescriptorProto允许你动态的定义你的proto文件:

//syntax = "proto3";  
//message mymsg  
//{  
//    uint32 len = 1;  
//    uint32 type = 2;  
//}
FileDescriptorProto file_proto;
file_proto.set_name("my.proto");  
file_proto.set_syntax("proto3");  
DescriptorProto *message_proto = file_proto.add_message_type();  
message_proto->set_name("mymsg");  
FieldDescriptorProto *field_proto = NULL;  
field_proto = message_proto->add_field();  
field_proto->set_name("len");  
field_proto->set_type(FieldDescriptorProto::TYPE_UINT32);  
field_proto->set_number(1);  
field_proto->set_label(FieldDescriptorProto::LABEL_OPTIONAL);  
field_proto = message_proto->add_field();  
field_proto->set_name("type");  
field_proto->set_type(FieldDescriptorProto::TYPE_UINT32);  
field_proto->set_number(2);  
DescriptorPool pool;  
const FileDescriptor *file_descriptor = pool.BuildFile(file_proto);  
cout << file_descriptor->DebugString();

Protobuf 序列化原理

Varint
Varint 是一种紧凑的表示数字的方法。它用一个或多个字节来表示一个数字,值越小的数字使用越少的字节数。这能减少用来表示数字的字节数。

比如对于 int32 类型的数字,一般需要 4 个 byte 来表示。但是采用 Varint,对于很小的 int32 类型的数字,则可以用 1 个 byte 来表示。当然凡事都有好的也有不好的一面,采用 Varint 表示法,大的数字则需要 5 个 byte 来表示。从统计的角度来说,一般不会所有的消息中的数字都是大数,因此大多数情况下,采用 Varint 后,可以用更少的字节数来表示数字信息。下面就详细介绍一下 Varint。

Varint 中的每个 byte 的最高位 bit 有特殊的含义,如果该位为 1,表示后续的 byte 也是该数字的一部分,如果该位为 0,则结束。其他的 7 个 bit 都用来表示数字。因此小于 128 的数字都可以用一个 byte 表示。大于 128 的数字,比如 300,会用两个字节来表示:1010 1100 0000 0010

下图演示了 Google Protocol Buffer 如何解析两个 bytes。注意到最终计算前将两个 byte 的位置相互交换过一次,这是因为 Google Protocol Buffer 字节序采用 little-endian 的方式。
在这里插入图片描述
消息经过序列化后会成为一个二进制数据流,该流中的数据为一系列的 Key-Value 对。如下图所示:
在这里插入图片描述
采用这种 Key-Pair 结构无需使用分隔符来分割不同的 Field。对于可选的 Field,如果消息中不存在该 field,那么在最终的 Message Buffer 中就没有该 field,这些特性都有助于节约消息本身的大小。

时间效率
通过protobuf序列化/反序列化的过程可以得出:protobuf是通过算法生成二进制流,序列化与反序列化不需要解析相应的节点属性和多余的描述信息,所以序列化和反序列化时间效率较高。

空间效率
xml、json是用字段名称来确定类实例中字段之间的独立性,所以序列化后的数据多了很多描述信息,增加了序列化后的字节序列的容量。Protobuf的序列化/反序列化过程可以得出:protobuf是由字段索引(fieldIndex)与数据类型(type)计算(fieldIndex<<3|type)得出的key维护字段之间的映射且只占一个字节,所以相比json与xml文件,protobuf的序列化字节没有过多的key与描述符信息,所以占用空间要小很多。

参考:
https://www.ibm.com/developerworks/cn/linux/l-cn-gpb/

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Win10 下 ProtoBuf 安装编译以及在 C++ 中的用法 的相关文章

  • 时间与日期插件 -- laydate 使用方法(摘自官网)

    简单例子 xff1a function var start 61 elem 39 start 39 选择ID为START的input format 39 YYYY MM DD hh mm ss 39 自动生成的时间格式 min laydat
  • 消息序列化工具-protobuf介绍及安装使用技巧

    简介 protobuf是google团队开发的用于高效存储和读取结构化数据的工具 xml json也可以用来存储此类结构化数据 xff0c 但是使用protobuf表示的数据能更加高效 xff0c 并且将数据压缩得更小 xff0c 大约是j
  • protobuf数据类型

    四 限定符 required optional repeated 的基本规则 1 在每个消息中必须至少留有一个required类型的字段 2 每个消息中可以包含0个或多个optional类型的字段 3 repeated表示的字段可以包含0个
  • STM32中USART的使用方法

    USART作为一种标准接口在应用中十分常见 本文着重分析其作为 UART的配置和应用方法 1 STM32固件库使用外围设备的主要思路 在 STM32中 xff0c 外围设备的配置思路比较固定 首先是使能相关的时钟 xff0c 一方面是设备本
  • vs2015的OpenCV3.2.0编译

    我们希望添加第三方功能模块和库或者针对特定cpu和gpu的编译调整优化选项 这样的需求就需要自己去编译opencv了 准备东西 opencv opencv contrib cmake 还有两个文件 因为可能是国内的原因 在configure
  • protobuf下载与安装+ protobuf 与json相互转换方法

    WIN环境 下载与安装 下载 https github com protocolbuffers protobuf releases 官方git地址 目前最新的是3 8版本 我是c 环境 选择cpp下载包 protobuf cpp 3 8 0
  • 【Protobuf速成指南】enum类型的使用

    文章目录 2 1枚举类型 一 如何定义枚举类型 二 语法规范 三 重定义问题 四 enum类型相关函数 五 Contact 2 1 改写 六 总结 2 1枚举类型 本系列文章将通过对通讯录项目的不断完善 带大家由浅入深的学习Protobuf
  • Protobuf(Protocol Buffers)超详细入门教程(跨平台序列化, Java)——更新于2022.01

    目录 相关教程 相关文献 安装 C Installation Unix 环境 Protobuf3 0基础上手例子 相关教程 Protobuf Protocol Buffers 超详细入门教程 跨平台序列化 C CMake 更新于2022 0
  • protobuf介绍和语法

    目录 前言 语法 标识符 字段 字段类型 proto2和proto3区别 前言 Protobuf即Protocol Buffers 是Google公司开发的一种跨语言和平台的序列化数据结构的方式 是一个灵活的 高效的用于序列化数据的协议 与
  • protobuf c++编程笔记

    文章目录 字段内容的定义 修饰符 字段类型 引用方式 不同字段的方法 1 optional修饰的基本类型 2 optional修饰的对象类型 3 repeated修饰的基本类型 4 repeated修饰的对象类型 序列化 反序列化 字段内容
  • 【Protobuf】pb中类型字段不匹配问题

    文章目录 背景 结论 原始数据 测试1 测试2 背景 客户端更新proto 新增message字段 探索新增字段的数据类型和标签对服务端反序列化数据的影响 结论 新增字段数据类型与服务端相同标签数据类型 不同 无法获取数据 但是不报错 相同
  • protobuf 中复合数据类型的读写

    背景 protobuf 在生成的 C 代码中为 proto 文件中的每个 message 生成了对应的 C 类 并提供了数据成员的读写方法 message 类型读写 message 示例 message Point double lng 1
  • protobuf-IOS简单总结(编译、环境搭建)

    什么是protobuf Protocol Buffers are a way of encoding structured data in an efficient yet extensible format Google uses Pro
  • protobuf的介绍、安装与使用

    1 protobuf是什么 protobuf是google旗下的一款平台无关 语言无关 可扩展的序列化结构数据格式 所以很适合用做数据存储和作为不同应用 不同语言之间相互通信的数据交换格式 只要实现相同的协议格式即同一 proto文件被编译
  • Protobuf下载和编译

    系列导航 一 Protobuf下载和编译 二 Protobuf在Java中的简单使用 一 简介 protobuf全称Google Protocol Buffers 是google开发的的一套用于数据存储 网络通信时用于协议编解码的工具库 是
  • vs2019 中编译和使用 protobuf 库

    背景 protobuf 是一种跨平台的序列化结构数据的方法 可用于网络数据传输及存储 本文对使用 vs2019 编译 protobuf 库文件进行说明 相关代码及安装文件均来自官网下载 VS2019 编译 protobuf 生成 sln 工
  • Protobuf类型

    1 基本类型 这些是原始的基本数据类型 用于存储数值和字符串 包括 double 双精度浮点数 float 单精度浮点数 int32 32 位有符号整数 int64 64 位有符号整数 uint32 32 位无符号整数 uint64 64
  • ProtoBuf-反射原理与使用

    文章目录 前言 相关应用场景 一 ProtoBuf 反射原理概述 1 获取message和service的属性和方法 1 1 使用protoc将proto文件生成 h和 cc文件 1 2 只使用proto文件 不使用protoc进行编译 1
  • 河道堤防GNSS位移监测系统

    一 方案背景 我国河系众多 海岸线漫长 在江边 河边 海边修筑修筑着几万公里的提防设施保卫着沿江 沿海居民的生命安全 也保卫着经济发展的累累硕果 近年来 因台风暴雨造成河道堤坝决堤 产生的经济损失越来越大 各对方对防洪堤 海堤的安全检查要求
  • C++使用protobuf实现序列化与反序列化

    一 protobuf简介 1 1 protobuf的定义 protobuf是用来干嘛的 protobuf是一种用于 对结构数据进行序列化的工具 从而实现 数据存储和交换 主要用于网络通信中 收发两端进行消息交互 所谓的 结构数据 是指类似于

随机推荐

  • 编辑器mavon-editor离线使用

    cnd部分 可与运维人员商量一起配置 vue2的使用 1 1在public文件夹下面 放入编辑器的全部文件 1 2引入 1 2 1script下面引入 import Vue from vue import mavonEditor from
  • C# 基础知识 (五).变量类型和字符串处理

    这篇文章是阅读 C 入门经典 Beginning C 书籍里面的内容 作者Karli Watson 主要包括自己缺乏的一些C 基础知识和在线笔记使用 文章主要包括C 简单变量类型和复杂变量类型 命名规则 隐式转换和显示转换 变量字符串处理等
  • CNN卷积神经网络

    CNN卷积神经网络 前言 一 相关概念 卷积 彩色图像卷积 池化 padding Dropout正则化 局部归一化 二 经典网络 AlexNet VGGNet介绍 GoogLeNet ResNet介绍 resnet解决方案 结果 三 实操一
  • 【卷积神经网络】12、激活函数

    文章目录 一 Tanh 二 Sigmoid 三 ReLU 四 Leaky ReLU 五 ELU 六 SiLU 七 Mish 本文主要介绍卷积神经网络中常用的激活函数及其各自的优缺点 最简单的激活函数被称为线性激活 其中没有应用任何转换 一个
  • 数学建模——阅读论文的重要性

    1 我们来交流下怎么拿奖吧 数学建模竞赛虽然它的初衷是非常好的 需要体现一个人应用数学的能力以及创兴能力 但是实际上 经过我多年的比赛 发现其实绝大多数获奖 包括国家一等奖 也是可以通过一定的学习以及一定的技巧来获取的 对于数学建模新手来说
  • python socket传输大文件的方法

    方法一 发送端 1 计算发送文件大小 然后结合文件的其他信息 组成文件头先发送一次 2 发送文件数据时用sendall 一次发送所有数据 好像是重复调用了send 接收端 1 接收端根据接受文件的大小和recv size计算要接收数据的次数
  • 期货开户收费政策非常合理

    需要大家支付的费用由两部分组成 一部分是保证金 另一部分是费率 保证金和费率都由交易所收取 收取的费用是固定的 因为后期大家投资的项目是不一样的 所以需要大家准备的费用肯定也不一样 除了交易所所收取的费用以外 还包括了开户公司所收取的费用
  • Midjourney 使用总结

    1 关键词提问 中国古典女性 东方美女极致面容站在运河码头遥望丈夫 史诗奇幻场景风格 深绿浅棕 细节风帆 柔焦 人像隐喻 4K 电影级高品质照片 全景图 焦距 110mm 光圈 f3 5 画面比例16 9 全景构图 这个可以把图变成二次元
  • 恢复谷歌翻译的究极方法

    谷歌翻译为什么会失效 我想各位在去年11月的时候就知道了 可是要怎么解决失效的问题呢 之前我们是通过手动Ping可以连接的ip 各位可能觉得麻烦 心里觉得什么档次还要我手动ping就没有可以自动扫描的吗 还别说真的有我最近发现一个谷歌翻译修
  • StreamingAsset文件夹

    Unity中的大部分资源在发布时都被整合到工程中 我们不能访问 但有时我们需要通过路径名访问放在目标设备中的文件 这些文件被存储在目标设备的文件系统中 在Unity工程中 放在StreamingAssets文件夹中的任何资源都将被原样复制到
  • 使用Docker安装Elasticsearch和大数据

    在本教程中 我们将探索如何使用Docker容器化技术安装和配置Elasticsearch ES 和大数据处理工具 Elasticsearch是一个强大的开源搜索和分析引擎 而大数据处理工具则提供了对大规模数据集的处理和分析能力 通过将它们与
  • 2023-5-31第三十一天

    conform顺从 遵从 一致 squeeze挤压 proprietary专卖权 专利的 所有的 endeavor努力 尽力 comprise由 组成 包含 compose组成 写作 compact小型的 consult咨询 查阅 expa
  • Linux CentOS 7 安装mongoDB

    安装之前准备工作 环境说明 1系统虚拟机信息 CentOS7 X86 64位 2软件及版本 mongodb linux x86 64 3 6 3 tgz Xshell工具 MongoDB 提供了 linux 各发行版本 64 位的安装包 你
  • string容器

    string容器 构造和析构 string容器的设计目标 strinf容器的操作 构造和析构 void testOne cout lt lt 显示string中字符数组的最大长度 lt lt endl cout lt lt string n
  • AOP(Aspect-oriented programming,面向切面编程)

    概述 面向切面的程序设计 Aspect oriented programming AOP 是CS计算机科学中的一种程序设计泛型 旨在将横切关注点与业务主体进行进一步分离 以提高程序代码的模块化程度 其可以通过预编译方式和运行期动态代理实现在
  • VSCODE的安装与配置Anaconda环境

    1 下载安装包安装 推荐 最新版本的Anaconda没有VSCODE因此可以直接百度VSCODE进行安装 a VSCODE的下载 直接加载VSCODE的官网https code visualstudio com 点击Download for
  • Shell脚本中引用另一个脚本文件

    在Shell中要调用别的shell脚本或别的脚本中的变量有一下两种方式 方法一 使用点号 subscript sh 方法二 使用source source subscript sh 注意 1 两个点之间 有空格 2 两个脚本不在同一目录 要
  • 用GLM来读取显示有纹理的OBJ

    注意这里的GLM不是OPENGL MATHAMATICS LIBRAR 而是an Alias Wavefront OBJ file library 用来操作OBJ文件的一个库 这里用其来读取带纹理的OBJ文件并显示出来 1 下载GLM库 h
  • 括号生成(结合Catalan数详细分析)

    题目 给出n代表生成括号的对数 请你写出一个函数 使其能够生成所有可能的并且有效的括号组合 例如 给出n 3 生成结果为 PS 本题源自 leetcode 22 理论基础 Catalan数 卡特兰数 Catalan数列是序列 C 0
  • Win10 下 ProtoBuf 安装编译以及在 C++ 中的用法

    ProtoBuf Protocol Buffer protoBuf 或 PB 是 google 的一种数据交换的格式 它独立于语言 独立于平台 google 提供了多种语言的实现 java c c go 和 python 每一种实现都包含了