C/C++编程:Protobuf 使用

1060 篇文章 298 订阅

概述

ProtoBuf全称:protocol buffers,直译过来是:“协议缓冲区”,是一种与语言无关、与平台无关的可扩展机制,用于序列化结构化数据。

  • Protocol Buffers 是一种轻便高效的结构化数据存储格式,可以把结构体序列化为二进制,也可以把对应二进制反序列化回结构体。
    • 它很适合做数据存储或RPC数据交换格式。我们只需要定义一次数据结构,就可以使用ProtoBuf生成源代码,轻松搞定在各种数据流和各种语言中写入、读取结构化数据。
    • 可以用于即时通讯、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。
    • protobuf本身并不是和gRPC绑定的。它也可以被用于非RPC场景,如存储等
  • 所谓序列化通俗来说就是把内存的一段数据转化成二进制并存储或者通过网络传输,而读取磁盘或另一端收到后可以在内存中重建这段数据
  • 说白了protobuf单纯就是做编解码。你可以把你程序中的一些对象用pb序列化,然后存到本地文件,过一会儿再读取文件,然后恢复出那些对象

ProtoBuf和json或者xml,从一定意义上来说他们的作用是一样的。

  • json、xml都是一种序列化的方式,只是他们不需要提前预定义idl,且具备可读性,当然他们传输的体积也因此较大,可以说是各有优劣。
    • ProtoBuf相比于json\XML,更小(3 ~ 10倍)、更快(20 ~ 100倍)、更为简单。
  • ProtoBuf和json\xml最大的区别是:json\xml都是基于文本格式,ProtoBuf是二进制格式。
  • 另外,它不像 JSON 一样开箱即用,它依赖工具包来进行编译成 java 文件或 go 文件等(为此,它提供了很多语言的API)。目前提供了 C++、Java、Python、Objective-C、C#、JavaNano、JavaScript、Ruby、Go、PHP等语言的 API。
    在这里插入图片描述

有 Protocol buffer 这种轻便的序列化反序列化工具,Json 为什么还会大量使用?

  • 因为在世界上最大、最成功的分布式系统—Web 中,HTTP + JSON payload大获成功。
  • 至于原因,也很简单,不用写 schema 文件,不用代码生成,从而也就避免了复杂的依赖管理,简单,人类可读,容易调试。
  • JSON 格式对客户端友好,不仅仅是编程上的,工程上也可以通过在线的示例返回来确定返回的结构,而有 schema 的序列化协议在客户端的分发也是一个问题,虽然理论上最好的情况下,schema 本身就是最好、最重要的文档。
  • 另外,JSON 作为一种模型友好可亲,也有工具在 JSON 上面做 schema 描述、校验等,可以满足各种层次的工程要求。
  • 在性能上,对大部分关注 paylaod 里面具体的语义的应用,请求和响应的序列化和反序列化根本不是问题……如果有问题,可以很方便的改到 bson 或者 messagepack,性能比 protobuf 和 thrift 差的很小。

提问:google protobuf和gRPC的关系?

  • protobuf可以把结构体序列化为二进制,也可以把对应二进制反序列化回结构体。说白了pb单纯就是做编解码。你可以把你程序中的一些对象用pb序列化,然后存到本地文件,过一会儿再读取文件,然后恢复出那些对象
  • grpb是网络通信协议,在网络通信时,你也会发送和接收数据。收发的数据使用protobuf进行编解码(当然它也可以使用其它的编解码方式,比如thrift、JSON,也可以自己造轮子,比如Go的gob)当然grpc作为一个rpc框架,数据的编解码只是其中一小部分,还有很多其它的工作需要处理,参见各种其它框架

链接:

  • github地址:https://github.com/protocolbuffers/protobuf
  • 官方文档
  • 关于 ProtoBuf 示例代码包含在源代码包中的“examples”目录下。

安装 Google Protocol Buffer

注意: Protobuf 有两个大版本,proto2 和 proto3,同比 python 的 2.x 和 3.x 版本,如果是新接触的话,同样建议直接入手 proto3 版本。proto3 相对 proto2 而言,简言之就是支持更多的语言(Ruby、C#等)、删除了一些复杂的语法和特性、引入了更多的约定等。

下载地址: https://github.com/protocolbuffers/ProtoBuf/releases
在这里插入图片描述

注意,不同的电脑系统安装包是不一样的:

windows

在这里插入图片描述

注意:我们需要将下载得到的可执行文件protoc所在的 bin 目录加到我们电脑的环境变量中。
在这里插入图片描述

可以通过protoc --version #查看protoc的版本
在这里插入图片描述

ubuntu

sudo apt update; sudo apt upgrade
sudo apt install libprotobuf-dev protobuf-compiler

centos

编译安装

sudo wget https://github.com/protocolbuffers/protobuf/releases/download/v3.19.4/protobuf-all-3.19.4.tar.gz
sudo tar -zxvf protobuf-all-3.19.4.tar.gz
  • 安装依赖
sudo apt-get install build-essential
 sudo apt install libtool autoconf make  

sudo apt install make-guile

  • 编译安装
cd protobuf-3.19.4
sudo ./autogen.sh
sudo ./configure
sudo make
sudo make check
sudo make install
sudo ldconfig    # refresh shared library cache

make install 注意权限问题,最好使用 sudo make install。安装成功之后,使用 which protoc 就可以查看 protoc 已经安装成功了。ProtoBuf 默认安装的路径在 /usr/local,当然我们可以在配置的时候改变安装路径,使用如下命令:

./configure --prefix=/usr

安装成功后,我们执行protoc --version 查看我们的 Protocol Buffers 的版本

protoc --version

protoc就是protobuf的编译器,它把proto文件编译成不同的语言

理论

定义消息类型

protobuf里最基本的类型就是message。一般来讲,设计协议是在fileName.proto文件中, 其中fileName是自己定义, 在通过protoc转换成为对应的代码。

  • message 表示一个消息体, 相当于一个类
  • 每个message中都有对应的变量, 每个变量有个对应的编号, 每个message内不同变量的编号不能重复。

每一个messgae都会有一个或者多个字段(field),其中字段包含如下元素
在这里插入图片描述

使用(首字母大写)作为消息名,例如SongServerRequest。字段名称使用下划线分隔名称,例如song_name

message SongServerRequest {
  required string song_name = 1;
}

c++访问器:

  const string& song_name() { ... }
  void set_song_name(const string& x) { ... }
  • 对于每一个字段,它由三个部分组成:
    • 字段类型,比如上面的string、int
    • 字段名: 使用(首字母大写)作为消息名,例如SongServerRequest。字段名称使用下划线分隔名称,例如song_name。
    • 字段编号:
      • 消息定义中的每个字段都有一个唯一的编号,以便标识字段,它在每一个消息中应该是独一无二的,在编码后其实传递的是这个编号而不是字段名
      • 1到15范围内的字段编号需要一个字节来编码,16到2047范围内的字段编号需要两个字节。因此,您应该为经常出现的消息元素保留数字1到15。记住为将来可能添加的频繁出现的元素留出一些空间。
      • 您可以指定的最小字段编号是 1,最大的是 536,870,911。您也不能使用数字 19000 到 19999 ( FieldDescriptor::kFirstReservedNumberthrough FieldDescriptor::kLastReservedNumber),因为它们是为 Protocol Buffers 实现保留的——如果您在.proto. 同样,您不能使用任何以前保留的字段编号。
  • 指定字段规则: 消息字段可以是以下之一:
    • singular: 格式良好的消息可以有零个或一个此字段(但不能超过一个)。这是 proto3 语法的默认字段规则
    • optional:与 singular 相同,不过您可以检查该值是否明确设置
    • repeated:
      • 在格式正确的消息中,此字段类型可以重复零次或多次。系统会保留重复值的顺序
    • map:这是一个成对的键值对字段

可以在单个.proto中定义多种消息类型。比如下面:

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}

message SearchResponse {
 ...
}
  • 如果想要添加注释,使用 C/C++风格的 // 或者 /* … */ 语法.
/* SearchRequest represents a search query, with pagination options to
 * indicate which results to include in the response. */

message SearchRequest {
  string query = 1;
  int32 page_number = 2;  // Which page number do we want?
  int32 result_per_page = 3;  // Number of results to return per page.
}

保留字段

  • 为了避免再次使用到已移除的字段可以设定保留字段。如果任何未来用户尝试使用这些字段标识符,编译器就会报错
  • 请注意,您不能在同一reserved语句中混合字段名称和字段编号。
message Foo {
  reserved 2, 15, 9 to 11;
  reserved "foo", "bar";
}

标量值类型

.proto类型说明C++JavaPythonGo
doublefloat64
floatfloat32
int32使用可变长度编码。编码负数效率低下——如果你的字段可能有负值,请改用sint32。int32int32
int64使用可变长度编码。编码负数效率低下——如果你的字段可能有负值,请改用sint64。int64
uint32使用可变长度编码。uint32
uint64使用可变长度编码。uint64
sint32使用可变长度编码。符号整型值。这些比常规int32s编码负数更有效。int32
sint64使用可变长度编码。符号整型值。这些比常规int64s编码负数更有效。int64
fixed32总是四字节。如果值通常大于228,则比uint 32更有效uint32
fixed64总是八字节。如果值通常大于256,则比uint64更有效uint64
sfixed32总是四字节。int32
sfixed64总是八字节。int64
boolbool
string字符串必须始终包含UTF - 8编码或7位ASCII文本string
bytes可以包含任意字节序列string

数组

message SearchResponse {
  repeated Result results = 1;
}
​
message Result {
  string url = 1;
  string title = 2;
  repeated string snippets = 3;
}

枚举

枚举类型名使用CamelCase (带首字母大写),值名使用CAPITALS_WITH_UNDERSCORES:

  • 每个枚举定义必须包含一个映射到零的常量作为第一个元素
  • 注意:不要使用负数作为枚举值,编码效率低。
  • 每个枚举值应以分号结束,而不是逗号。
enum Foo {
  FIRST_VALUE = 0;
  SECOND_VALUE = 1;
}

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
  enum Corpus {
    UNIVERSAL = 0;
    WEB = 1;
    IMAGES = 2;
    LOCAL = 3;
    NEWS = 4;
    PRODUCTS = 5;
    VIDEO = 6;
  }
  Corpus corpus = 4;
}
  • 可以通过为不同的枚举常量分配相同的值来定义别名。为此,您需要将allow_alias选项设置为true,否则协议编译器会在找到别名时生成错误消息。
enum EnumAllowingAlias {
  option allow_alias = true;
  UNKNOWN = 0;
  STARTED = 1;
  RUNNING = 1;
}
enum EnumNotAllowingAlias {
  UNKNOWN = 0;
  STARTED = 1;
  // RUNNING = 1;  // Uncommenting this line will cause a compile error inside Google and a warning message outside.
}
  • 如果你在版本开发的过程中移除了某个枚举,但是在老的协议中还会使用,可以使用reserved 关键字标识。 请注意,不能在同一保留语句中混合字段名和数值。
enum Foo {
  reserved 2, 15, 9 to 11, 40 to max;
  reserved "FOO", "BAR";
}

服务

如果要在RPC(Remote Procedure Call,远程过程调用)系统中使用消息类型,可以在.proto文件中定义RPC服务接口,协议缓冲区编译器将根据所选语言生成服务接口代码和存根。

service FooService {
  rpc GetSomething(FooRequest) returns (FooResponse);
}

使用其他消息类型

使用import引用另外一个文件的pb

syntax = "proto3";
​
import "google/protobuf/wrappers.proto";
​
package ecommerce;
​
message Order {
  string id = 1;
  repeated string items = 2;
  string description = 3;
  float price = 4;
  google.protobuf.StringValue destination = 5;
}

默认情况下,只能使用直接导入的.proto文件中的定义。但是,有时可能需要将.proto文件移动到新位置。不用直接移动.proto文件并在一次更改中更新所有import调用,现在可以在旧位置放置一个伪.proto文件,使用import public概念将所有导入转发到新位置。任何导入包含import public语句的proto的人都可以传递地依赖import public依赖项。例如:

// new.proto
// All definitions are moved here
// new.proto
// All definitions are moved here
// client.proto
import "old.proto";
// You use definitions from old.proto and new.proto, but not other.proto

协议编译器使用-I/–proto_path路径标志在协议编译器命令行上指定的一组目录中搜索导入的文件。如果没有给定标志,它将在调用编译器的目录中查找。通常,您应该将–proto_path标志设置为项目的根目录,并对所有导入使用完全限定名。

从.proto文件中生成了什么?

在.proto上运行协议缓冲区编译器时,编译器用您指定的编程语言生成代码,您需要使用您在文件中描述的消息类型,包括获取和设置字段值,将消息序列化为输出流,以及从输入流解析消息。

  • 对于C++,编译器会从每个 .proto文件生成一个 .h and .cc文件, 文件中描述的每种消息类型都有一个类。

  • 对于Java,编译器为每个消息类型生成一个Java文件,其中包括每个类的定义,以及用于创建消息类实例的特殊构建器类。

  • Python有点不同,Python编译器生成一个模块,其中包含中每种消息类型的静态描述符,然后与元类一起使用,在运行时创建必要的Python数据访问类。

  • 对于Go, 编译器生成一个.pb.go文件,其中包含文件中每种消息类型的类型。

  • 对于Ruby, 编译器生成一个.rb文件带有包含消息类型的Ruby模块。

  • 对于Objective-C, 编译器为每个.proto文件中生成一个pbobjc.h和pbobjc.m文件,文件中描述的每种消息类型都有一个类。

  • 对于C#, 编译器为每个.proto文件生成一个.cs文件,文件中描述的每种消息类型都有一个类。

缺省值

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

  • 对于字符串,默认值为空字符串。

  • 对于字节,默认值为空字节。

  • 对于布尔,默认值为false。

  • 对于数字类型,默认值为零。

  • 对于枚举,默认值是第一个定义的枚举值,必须为0。

  • 对于消息字段,该字段未设置。它的确切值取决于语言。有关详细信息,请参见生成的代码指南。

  • 重复字段的默认值为空(通常是相应语言的空列表)。

    请注意,对于标量消息字段,一旦消息被解析,就无法判断字段是显式设置为默认值(例如,布尔值是否设置为false )还是根本没有设置:定义消息类型时应该记住这一点。例如,如果不希望默认情况下也发生某些行为,不要有一个布尔值在设置为false时打开该行为。另请注意,如果标量消息字段设置为默认值,则不会在线路上序列化该值。

使用其他消息类型

您可以使用其他消息类型作为字段类型

message SearchResponse {
  repeated Result results = 1;
}

message Result {
  string url = 1;
  string title = 2;
  repeated string snippets = 3;
}

嵌套类型

可以在其他消息类型中定义和使用消息类型,如下例:

message SearchResponse {
  message Result {
    string url = 1;
    string title = 2;
    repeated string snippets = 3;
  }
  repeated Result results = 1;
}

如果您想在其父消息类型之外重用此消息类型,则需要先指定它的父类型,如下所示:

message SomeOtherMessage {
  SearchResponse.Result result = 1;
}

您可以随心所欲地嵌套:

message Outer {                  // Level 0
  message MiddleAA {  // Level 1
    message Inner {   // Level 2
      int64 ival = 1;
      bool  booly = 2;
    }
  }
  message MiddleBB {  // Level 1
    message Inner {   // Level 2
      int32 ival = 1;
      bool  booly = 2;
    }
  }
}

更新消息类型

如果现有的消息类型不满足你的所有需求——例如,你希望消息格式有一个额外的字段——但是你仍然希望使用用旧格式创建的代码,别担心!在不破坏任何现有代码的情况下更新消息类型非常简单。请记住以下规则:

  • 不要更改任何现有字段的字段编号。
  • 如果您添加新字段,则使用“旧”消息格式的代码序列化的任何消息仍然可以由新生成的代码解析。您应该记住这些元素的默认值,以便新代码可以正确地与旧代码生成的消息交互。类似地,新代码创建的消息可以由旧代码解析:旧二进制文件在解析时会忽略新字段。有关详细信息,请参阅未知字段部分。
  • 只要在更新的消息类型中不再使用字段编号,就可以删除字段。您可能想要重命名该字段,可能添加前缀“OBSOLETE_”,或将字段编号设为保留,以便您的未来用户.proto不会意外重用该编号。
  • int32、uint32、int64、uint64和bool都是兼容的——这意味着您可以将字段从其中一种类型更改为另一种类型,而不会破坏向前或向后兼容性。如果从不适合相应类型的线路中解析出一个数字,您将获得与在 C++ 中将该数字强制转换为该类型相同的效果(例如,如果将 64 位数字读取为int32,它将被截断为 32 位)。
  • sint32并且sint64彼此兼容,但与其他整数类型不兼容。
  • 只要字节是有效的UTF - 8,string和bytes就兼容
  • fixed32与sfixed32兼容, fixed64与sfixed64兼容。

未知字段

未知字段是格式良好的协议缓冲区序列化数据,表示解析器无法识别的字段。例如,当旧二进制文件用新字段解析新二进制文件发送的数据时,这些新字段将成为旧二进制文件中的未知字段。

最初,proto3 消息在解析过程中总是丢弃未知字段,但在 3.5 版本中,我们重新引入了保留未知字段以匹配 proto2 行为。在 3.5 及更高版本中,未知字段在解析期间保留并包含在序列化输出中。

any

Any消息类型允许您将消息作为嵌入类型,而不需要它们 .proto定义。Any包含任意序列化的消息(字节),以及一个URL,该URL充当该消息的全局唯一标识符并解析为该消息的类型。要使用Any类型,你需要导入google/protobuf/any.proto.

import "google/protobuf/any.proto";

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

给定消息类型的默认类型 URL 是type.googleapis.com/packagename.messagename

不同的语言实现将支持运行时库助手以Any类型安全的方式打包和解包值——例如,在 Java 中,Any类型将具有特殊的pack()和unpack()访问器,而在 C++ 中则有PackFrom()和UnpackTo()方法:

// 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 ...
  }
}

Oneof

如果您有一条包含多个字段的消息,并且最多同时设置一个字段,可以强制执行此行为并使用 oneof 功能节省内存。

oneof 字段与常规字段一样,除了一个 oneof 共享内存中的所有字段外,最多可以同时设置一个字段。设置 oneof 的任何成员会自动清除所有其他成员。您可以使用case()或WhichOneof()方法检查Oneof 中的哪个值被设置(如果有的话),具体取决于您选择的语言。

使用 Oneof

怎么用呢?.proto使用oneof关键字后跟 oneof 名称

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

注意:

  • oneof 字段不能使用repeated和map
  • 设置oneof字段将自动清除oneof的所有其他成员。因此,如果你设置了oneof中的多个字段,则只有你最后设置的字段仍然有值。
SampleMessage message;
message.set_name("name");
CHECK(message.has_name());
message.mutable_sub_message(); // Will clear name field.
CHECK(!message.has_name());
  • oneof不能被repeated修饰
  • 如果你使用的是C++,请确保你的代码不会导致内存崩溃。以下示例代码将会崩溃,因为通过调用set_name()方法已经删除了sub_message。
SampleMessage message;
SubMessage* sub_message = message.mutable_sub_message();
message.set_name("name");      // 将删除 sub_message
sub_message->set_...            // 这里崩溃了
  • 还是在C++中,如果你用Swap()两个带有oneof的消息,则每条消息都将拥有对方的值:在下面的示例中,msg1将拥有sub_message,而msg2将拥有name。
SampleMessage msg1;
msg1.set_name("name");
SampleMessage msg2;
msg2.mutable_sub_message();
msg1.swap(&msg2);
CHECK(msg1.has_sub_message());
CHECK(msg2.has_name());

Maps

语法:

map<key_type, value_type> map_field = N;

  • 其中key_type可以是任何整数或字符串类型(因此,除浮点类型和之外的任何标量类型)。请注意, enum 不是有效的key_type.
  • value_type可以是除了map以外的任何类型

举个例子:

syntax = "proto3";
message Product
{
    string name = 1; // 商品名
    // 定义一个k/v类型,key是string类型,value也是string类型
    map<string, string> attrs = 2; // 商品属性,键值对
}

注意:

  • map字段不能被repeated修饰
  • wire格式化的顺序和map迭代器的顺序是不确定的,所以你不能依赖map项的特定顺序
  • 为.proto生成文本格式时,map按照key排序。数字键按照数字排序。
  • 当从wire解析或合并时,如果存在重复的键,则使用最后看到的键。从文本解析map时,如果键重复,则解析可能失败。
  • 如果映射字段提供了键但没有值,则序列化字段时的行为取决于语言。在C++,Java和Python中,序列化的时类型的默认值,而其他语言不会序列化。

map语法序列化后等同于如下内容,故而即使是不支持map语法的protocol buffers实现也是可以处理你的数据。

message MapFieldEntry {
  key_type key = 1;
  value_type value = 2;
}

repeated MapFieldEntry map_field = N;

Packages

可以向.proto文件中添加可选的package明符,以防止协议消息类型之间的名称冲突。

package foo.bar;
message Open { ... }

怎么用?

message Foo {
  ...
  foo.bar.Open open = 1;
  ...
}

JSON Mapping

proto3支持JSON中的规范编码,从而在系统之间共享数据更加容易。下表中按类型对编码进行了描述。

如果JSON编码数据中缺少了某个值,或者该值为null,则在解析为protocol buffer时,它将被解释为适当的默认值。如果字段在protocol buffer中具有默认值,则默认情况下会在JSON编码的数据中将其省略以节省空间。具体实现可以提供在 JSON编码中可选的默认值。

proto3jsonjson实例说明
messageobject{“fooBar”: v, “g”: null, …}
enum

生成自定义类

要生成Java,Python,C ++,Go,Ruby,Objective-C或C#代码,你需要使用.proto文件中定义的消息类型,需要在.proto上运行protocol buffers编译器。如果尚未安装编译器,请下载软件包并按照README中的说明进行操作。对于Go,你还需要为编译器安装一个特殊的代码生成器插件:你可以在GitHub上的golang / protobuf仓库中找到此代码和安装说明。

protocol buffers编译器的调用如下:

protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR --go_out=DST_DIR --ruby_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto
  • protoc是工具名, 可以直接运行的命令。
  • IMPORT_PATH指定解析导入指令时查找.proto文件的目录。如果省略,则使用当前目录。可以通过多次传递–proto_path选项来指定多个导入目录。将按顺序搜索它们。
  • –cpp_out是参数, 指定生成C++代码, =后面指定生成的目录。
    • –cpp_out在DST_DIR生成C++代码。参考C++代码生成指南
    • –java_out在DST_DIR生成Java代码。参考Java代码生成指南
    • –python_out在DST_DIR生成Python代码。参考Python代码生成指南
  • 必须提供一个或多个.proto文件作为输入。可以一次指定多个.proto文件。尽管这些文件是相对于当前目录命名的,但每个文件都必须驻留在IMPORT_PATH导入的其中一个路径中,以便编译器可以确定其规范名称。

Protobuf 风格指南

字段和消息名称

可能需要的函数

CopyFrom

消息拷贝函数

void CopyFrom(const Message& from)

实例:


message Param {
  optional string name = 1;
  optional ParamType type = 2;
  optional string type_name = 3;
  oneof oneof_value {
    bool bool_value = 4;
    int64 int_value = 5;
    double double_value = 6;
    string string_value = 7;
  }
  optional bytes proto_desc = 8;
}



class Parameter {
 public:
   /**
   * @brief copy constructor
   */
  explicit Parameter(const Parameter& parameter);

private:
  Param param_;
}

Parameter::Parameter(const Parameter& parameter) {
  param_.CopyFrom(parameter.param_);
}

使用

第一个例子

生成

创建一个.proto文件:addressbook.proto,内容如下

syntax = "proto3";
package IM;
message Account {
    //账号
    uint64 ID = 1;
    //名字
    string name = 2;
    //密码
    string password = 3;
}
 
message User {
    Account user = 1;
}

编译.proto文件,生成C++语言的定义及操作文件

protoc addressbook.proto --cpp_out=./

生成的文件:addressbook.pb.h addressbook.proto

分析

让我们看看编译器为.proto文件创建了哪些类和函数。如果你查看addressbook.pb.h,你会发现你在addressbook.proto中指定的每条消息都有一个类。仔细查看Person类,您可以看到编译器已经为每个字段生成了访问器。例如,对于name, id, email和phones字段,并且生成了相应的方法:

使用

编写程序main.cpp

#include <iostream>
#include <fstream>
#include "addressbook.pb.h"
 
using namespace std;
 
int main(int argc, char** argv)
{
    IM::Account account1;
    account1.set_id(1);
    account1.set_name("windsun");
    account1.set_password("123456");
 
    string serializeToStr;
    account1.SerializeToString(&serializeToStr);
    cout <<"序列化后的字节:"<< serializeToStr << endl;
 
 
    IM::Account account2;
    if(!account2.ParseFromString(serializeToStr))
    {
        cerr << "failed to parse student." << endl;
        return -1;
    }
    cout << "反序列化:" << endl;
    cout << account2.id() << endl;
    cout << account2.name() << endl;
    cout << account2.password() << endl;
 
    google::protobuf::ShutdownProtobufLibrary();
 
    return 0;
}

编译

g++ main.cpp Account.pb.cc -o main -lprotobuf -std=c++11 -lpthread

运行

其他

protobuf用作配置文件

protobuf提供了一种textformat的序列化格式,类似json格式,清晰易读。比如一棵行为树节点描述文件:

数据定义为:

message BehaviorNodeConf
{
	required int32 type = 1;
	// 条件需要的参数
	repeated int32 args = 2;
	// 包含多个子节点
	repeated BehaviorNodeConf node = 3;
};

message BehaviorTreeConf
{
	// 行为树类型: AI, ON_HIT, ON_HITTED ...
	required int32 type = 1;
	// 行为树节点
	required BehaviorNodeConf node = 2;
};

配置文件为:

type: 5
node:
{
	type: 1
	node:
	{
		type: 101
		args: 2
	}
	node:
	{
		type: 1
		node:
		{
			type: 1001
			args: 0
			args: 100
		}
		node:
		{
			type: 1001
			args: 1
			args: -100
		}
	}
}

以下两行代码即可解析这个配置文件:

BehaviorTreeConf conf;
google::protobuf::TextFormat::ParseFromString(fileContent, &conf);

参考

https://www.kaifaxueyuan.com/basic/protobuf3/protocol-buffer-cpp-writing-a-message.html

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值