【C++ 脚手架】Protobuf 的介绍与使用

发布时间:2026/5/19 17:00:55

【C++ 脚手架】Protobuf 的介绍与使用 Protobuf 的介绍与使用Protocol Buffers简称 protobuf是 Google 开发的一种语言中立、平台中立、可扩展的序列化结构数据的方法。它常用于跨语⾔服务由于 Protobuf ⽀持多种编程语⾔它⾮常适合构建跨语⾔的 RPC远程过程调⽤服务。数据序列化用于网络通信、数据存储等场景将结构化数据序列化为字节流。Protobuf 定义了⼀种接⼝描述语⾔IDL⽤于描述数据结构然后可以⾃动⽣成各种编程语⾔的代码来操作这些数据结构。使用流程在.proto文件中定义数据结果消息类型。使用 protoc 编译器生成目标语言的代码。在你的程序中引用生成的代码进行序列化与反序列化。一、Protobuf 安装sudoaptinstallprotobuf-compiler libprotobuf-dev#如果需要使⽤grpc相关编译sudoaptinstallprotobuf-compiler-grpc# 验证是否安装成功protoc--version二、Protobuf 基本语法2.1 基本结构消息Protobuf 中的基本数据结构单元类似于 C 中的结构体或 Java 中的类。字段消息中的数据项每个字段都有⼀个唯⼀的字段编号和数据类型。编号 1 15 占用 1 个字节16 2047 的占用 2 个字节常用字段应使用较小的编号19000 19999 不可用在 Protobuf 协议实现中对这些数进行了预留枚举用于定义一组命名的常数。服务在 RPC 场景中定义服务接口。2.2 数据类型.proto TypeNotesC Typedoubledoublefloatfloatint32使用变长编码[1]。负数的编码效率较低——若字段可能为负值应使用 sint32 代替。int32int64使用变长编码[1]。负数的编码效率较低——若字段可能为负值应使用 sint64 代替。int64uint32使用变长编码[1]。uint32uint64使用变长编码[1]。uint64sint32使用变长编码[1]。符号整型。负值的编码效率高于常规的 int32 类型。int32sint64使用变长编码[1]。符号整型。负值的编码效率高于常规的 int64 类型。int64fixed32定长 4 字节。若值常大于 (2^{28}) 则会比 uint32 更高效。uint32fixed64定长 8 字节。若值常大于 (2^{56}) 则会比 uint64 更高效。uint64sfixed32定长 4 字节。int32sfixed64定长 8 字节。int64boolboolstring包含 UTF-8 和 ASCII 编码的字符串长度不能超过 (2^{32})。stringbytes可包含任意的字节序列但长度不能超过 (2^{32})。string[1] 变长编码是指经过 protobuf 编码后原本 4 字节或 8 字节的数可能会被变为其他字节数。2.3 定义 .proto 文件syntax proto3; // 使用 proto3 语法 package example; // 生成 C 代码后example 变成命名空间防止命名冲突 // 默认情况protoc命令并不会针对service服务⽣成对应rpc代码 // 需要开启选项才会进⾏⽣成 option cc_generic_services true; // 枚举类型 enum PhoneType { MOBILE 0; // 枚举类型的编号必须从0开始 HOME 1; WORK 2; } // 定义消息类型 message Student { string name 1; // 字段编号 1 int32 id 2; // 字段编号 2 // 嵌套消息 message PhoneNumber { string number 1; PhoneType type 2; } repeated PhoneNumber phones 3; // repeated 表示数组 mapstring , float scores 4; // hash类型键值对 } // RPC 远程调用的相关接口 message PersonRequest { int32 id 1; } message PersonResponse { Student stu 1; } // 定义一个 RPC 服务 service PersonService { rpc getPerson(PersonRequest) returns (PersonResponse); }2.4 编译命令protoc[--proto_pathIMPORT_PATH]--cpp_outDST_DIR path/to/file.protoprotoc编译工具--proto_path指定被编译的.proto文件所在的目录可多次指定可简写为-IIMPORT_PATH如果不指定该参数则在当前目录搜索当某个.proto文件import其他.proto文件或编译的.proto文件不在当前目录下这时就需要用-I指定搜索目录--cpp_out指编译后的⽂件为 C ⽂件DST_DIR编译后⽣成⽂件的⽬标路径path/to/file.proto要编译的.proto⽂件2.5 编译后生成的结构person.pb.hnamespaceperson{enumPhoneType:int{MOBILE0,HOME1,WORK2,// 用于枚举类型的边界检查PhoneType_INT_MIN_SENTINEL_DO_NOT_USE_::std::numeric_limits::int32_t::min(),PhoneType_INT_MAX_SENTINEL_DO_NOT_USE_::std::numeric_limits::int32_t::max(),};// string name 1;voidclear_name();// 清除 name 字段的值const::std::stringname()const;// 获取 name 字段的值只读templatetypenameArg_const::std::string,typename...Args_voidset_name(Arg_arg,Args_...args);// 设置 name 字段的值支持多种参数类型::std::string*PROTOBUF_NONNULLmutable_name();// 获取 name 字段的可修改指针// int32 id 2;voidclear_id();::int32_tid()const;voidset_id(::int32_tvalue);// mapstring, float scores 4;intscores_size()const;// 获取 scores map 的元素个数voidclear_scores();// 清空 scores map 中的所有元素// 获取 scores map 的只读引用// 返回: 常量 map 引用可用于遍历和查找// 示例: for (const auto pair : scores()) { ... }// scores()[math] 99.8const::google::protobuf::Map::std::string,floatscores()const;// 获取 scores map 的可修改指针// 返回: 可修改的 map 指针用于添加、删除或修改元素// 示例: (*mutable_scores())[math] 95.5;::google::protobuf::Map::std::string,float*PROTOBUF_NONNULLmutable_scores();// repeated .person.Student.PhoneNumber phones 3;intphones_size()const;// 获取 phones 列表的元素个数voidclear_phones();// 获取指定索引位置的 PhoneNumber 对象的可修改指针// 参数: index - 索引位置0 到 phones_size()-1// 返回: 可修改的 PhoneNumber 指针// 示例: mutable_phones(0)-set_number(123456);::person::Student_PhoneNumber*PROTOBUF_NONNULLmutable_phones(intindex);::google::protobuf::RepeatedPtrField::person::Student_PhoneNumber*PROTOBUF_NONNULLmutable_phones();// 取指定索引位置的 PhoneNumber 对象的只读引用const::person::Student_PhoneNumberphones(intindex)const;// 在 phones 列表末尾添加一个新的 PhoneNumber 对象// 返回: 新添加的 PhoneNumber 对象的可修改指针// 示例: add_phones()-set_number(987654);::person::Student_PhoneNumber*PROTOBUF_NONNULLadd_phones();// 获取整个 phones 列表的只读引用const::google::protobuf::RepeatedPtrField::person::Student_PhoneNumberphones()const;classPersonService:public::google::protobuf::Service{public:virtualvoidgetPerson(::google::protobuf::RpcController*PROTOBUF_NULLABLE controller,const::person::PersonRequest*PROTOBUF_NONNULL request,::person::PersonResponse*PROTOBUF_NONNULL response,::google::protobuf::Closure*PROTOBUF_NULLABLE done);}classPersonService_Stubfinal:publicPersonService{public:voidgetPerson(::google::protobuf::RpcController*PROTOBUF_NULLABLE controller,const::person::PersonRequest*PROTOBUF_NONNULL request,::person::PersonResponse*PROTOBUF_NONNULL response,::google::protobuf::Closure*PROTOBUF_NULLABLE done)override;}}repeated作用于不同字段的类型时基础类型int32, string, float, bool 等可以直接使用add_字段名(value)添加元素复合类型messageenum需要使用add_字段名()返回指针然后设置2.6 序列化和反序列化每个 protobuf message 编译后生成的类都继承自google::protobuf::Message或MessageLite提供了丰富的通用接口。namespacegoogle{namespaceprotobuf{classPROTOBUF_EXPORTMessage:publicMessageLite{}classPROTOBUF_EXPORTMessageLite{public:// 序列化方法boolSerializeToString(std::string*output)const;// 序列化到 string 中boolSerializeToArray(void*data,intsize)const;// 序列化到 C 风格数组中boolSerializeToOstream(std::ostream*output)const;// 序列化到C流中std::stringSerializeAsString()const;// 返回空 string 表⽰出错// 反序列化方法boolParseFromString(conststd::stringdata);// 从 string 中反序列化boolParseFromArray(constvoid*data,intsize);// 从 C 风格数组中反序列化boolParseFromIstream(std::istream*input);// 从 C 流中反序列化}}}2.7 基本使用目录结构|-test.cc|-person.pb.cc|-person.pb.h|-person.proto|-makefileperson.protosyntax proto3; package person; option cc_generic_services true; enum PhoneType { MOBILE 0; HOME 1; WORK 2; } message Student { string name 1; int32 id 2; message PhoneNumber { string number 1; PhoneType type 2; } repeated PhoneNumber phones 3; mapstring , float scores 4; } message PersonRequest { int32 id 1; } message PersonResponse { Student stu 1; } service PersonService { rpc getPerson(PersonRequest) returns (PersonResponse); }test.cc#includeperson.pb.h#includeiostreamstd::stringserialize_test(){person::Student stu;stu.set_id(1001);stu.set_name(zhangsan);autophone1stu.add_phones();phone1-set_number(123456789);phone1-set_type(person::PhoneType::HOME);autoscoresstu.mutable_scores();(*scores)[math]95;(*scores)[english]85.5;scores-insert({chinese,100});std::string strstu.SerializeAsString();std::coutstrstd::endl;returnstr;}voidunserialize_test(conststd::stringstr){person::Student stu;boolretstu.ParseFromString(str);if(retfalse){std::coutunserialize failedstd::endl;return;}std::coutstu.id()std::endl;std::coutstu.name()std::endl;for(inti0;istu.phones().size();i){std::coutstu.phones(i).number() stu.phones(i).type()std::endl;}autoitstu.scores().find(math);if(it!stu.scores().end()){std::coutit-secondstd::endl;// 数学成绩}for(autoit:stu.scores()){std::coutit.first it.secondstd::endl;}}intmain(){std::cout序列化结果std::endl;std::string resserialize_test();std::coutresstd::endl;std::cout反序列化结果std::endl;unserialize_test(res);return0;}Makefilemain: test.cc person.pb.cc g -stdc17 -o $ $^ -lprotobuf -lpthread .PHONY:clean clean: rm -f test.cc三、补充操作3.1 mapprotobuf 中 message 消息结构中⽀持 map 字段的定义其字段类型对应的是 protobuf 中的Map类注意并⾮CSTL中 的 map虽然他们的基本操作都差不多但是类型是不同的。namespacegoogle{namespaceprotobuf{templatetypenameKey,typenameTstructMapPair{MapPair(constKeyother_first,constTother_second)};templatetypenameKey,typenameTclassMap{public:usingiterator....;usingconst_iterator....;iteratorbegin();iteratorend();voidclear();size_typesize()boolempty()iteratorfind(constTrivialKeyk)std::pairiterator,boolinsert(constKeyValuePairkv)iteratoroperator[](constTrivialKeyk)voiderase(iterator it)};}}3.2 Json 与 Protobuf 的相互转换#includegoogle/protobuf/util/json_util.h// 核心转换函数namespacegoogle::protobuf::util{classPROTOBUF_EXPORTStatus{public:boolok();StringPiecemessage();stringToString();};structJsonParseOptions{//解析过程中是否忽略未知的JSON字段boolignore_unknown_fields;//是否在解析枚举类型时不区分⼤⼩写boolcase_insensitive_enum_parsing;};// JSON 字符串 - Protobuf MessageStatusJsonStringToMessage(conststd::stringjson,Message*message,constJsonParseOptionsoptionsJsonParseOptions());structJsonPrintOptions{//是否添加空⽩字符让json更易读booladd_whitespace;//是否总是输出基本类型的字段。默认值的基本类型字段在JSON输出中会被省略boolalways_print_primitive_fields;//是否总是将枚举类型作为整数打印。默认情况下枚举值会被渲染为字符串boolalways_print_enums_as_ints;//是否保留 pb 字段名称true则使⽤原始名⽽不是根据 JSON 命名约定进⾏转换boolpreserve_proto_field_names;//默认均为false};// Protobuf Message - JSON 字符串StatusMessageToJsonString(constMessagemessage,std::string*json,constJsonPrintOptionsoptionsJsonPrintOptions());}案例#includeperson.pb.h#includeiostream#includegoogle/protobuf/util/json_util.hvoidprotobufToJsonTest(){person::Student stu;stu.set_id(100);stu.set_name(zhangsan);autophone1stu.add_phones();phone1-set_number(123456789);phone1-set_type(person::PhoneType::HOME);autoscoresstu.mutable_scores();(*scores)[math]95;(*scores)[english]85.5;scores-insert({chinese,100});std::string json;google::protobuf::util::JsonPrintOptions options;options.add_whitespacetrue;google::protobuf::util::Status resgoogle::protobuf::util::MessageToJsonString(stu,json,options);if(!res.ok()){std::coutmessage to json failed: res.ToString()std::endl;return;}std::coutjsonstd::endl;}voidjsonToProtobufTest(){std::string jsonR( { name: ccc, id: 10000, phones: [ { number: 13620775555, type: 1 } ], scores: { math: 98, chinese: 100 } } );person::Student stu;google::protobuf::util::Status resgoogle::protobuf::util::JsonStringToMessage(json,stu);if(!res.ok()){std::coutjson to protubuf failed: res.ToString()std::endl;return;}std::coutstu.id()std::endl;std::coutstu.name()std::endl;for(inti0;istu.phones().size();i){std::coutstu.phones(i).number() stu.phones(i).type()std::endl;}for(autoit:stu.scores()){std::coutit.first it.secondstd::endl;}}intmain(){std::coutprotobufToJsonTest: std::endl;protobufToJsonTest();std::coutjsonToProtobufTest: std::endl;jsonToProtobufTest();return0;}输出结果protobufToJsonTest:{name:zhangsan,id:100,phones:[{number:123456789,type:HOME}],scores:{chinese:100,math:95,english:85.5}}jsonToProtobufTest:10000ccc136207755551math98chinese100

相关新闻