Table of Contents
我们项目中使用protocol buffer来进行服务器和客户端的消息交互,服务器使用C++,所以本文主要描述protocol buffer C++方面的使用,其他语言方面的使用参见google的官方文档.protocol buffer是google的一个开源项目,它是用于结构化数据串行化的灵活、高效、自动的方法,例如XML,不过它比xml更小、更快、也更简单。你可以定义自己的数据结构,然后使用代码生成器生成的代码来读写这个数据结构。你甚至可以在无需重新部署程序的情况下更新数据结构。
Protocol Buffers
https://developers.google.cn/protocol-buffers/docs/proto?hl=zh-cn
本指南介绍了如何使用协议缓冲区语言来构造协议缓冲区数据,包括.proto
文件语法以及如何从.proto
文件中生成数据访问类。它涵盖了协议缓冲区语言的proto2版本:有关proto3语法的信息,请参见《Proto3语言指南》。
这是参考指南–有关使用本文档中描述的许多功能的分步示例,请参阅所选语言的教程。
定义消息类型
首先,让我们看一个非常简单的示例。假设您要定义一个搜索请求消息格式,其中每个搜索请求都有一个查询字符串,您感兴趣的特定结果页面以及每页结果数量。这是.proto
用于定义消息类型的文件。
message SearchRequest {
required string query = 1;
optional int32 page_number = 2;
optional int32 result_per_page = 3;
}
所述SearchRequest
消息定义指定了三个字段(名称/值对),一个用于每条数据要在此类型的消息包括。每个字段都有一个名称和类型。
指定字段类型
在上面的示例中,所有字段均为标量类型:两个整数(page_number
和result_per_page
)和一个字符串(query
)。但是,您也可以为字段指定复合类型,包括枚举和其他消息类型。
分配字段编号
如您所见,消息定义中的每个字段都有一个唯一的编号。这些数字用于标识消息二进制格式的字段,一旦使用了消息类型,就不应更改这些数字。请注意,范围为1到15的字段编号需要一个字节来编码,包括字段编号和字段的类型(您可以在Protocol Buffer Encoding中找到有关此内容的更多信息)。16到2047之间的字段号占用两个字节。因此,应该为非常频繁出现的消息元素保留字段编号1到15。切记为将来可能添加的频繁出现的元素留出一些空间。
您可以指定最小的场数是1,最大为2 29日- 1,或536870911。您也不能使用数字19000到19999(FieldDescriptor::kFirstReservedNumber
至FieldDescriptor::kLastReservedNumber
),因为它们是为协议缓冲区实现保留的-如果您在中使用这些保留数之一,则协议缓冲区编译器会抱怨.proto
。同样,您不能使用任何以前保留的字段号。
指定字段规则
您指定消息字段是以下内容之一:
required
:格式正确的消息必须恰好具有此字段之一。optional
:格式正确的邮件可以包含零个或一个此字段(但不能超过一个)。repeated
:在格式正确的消息中,此字段可以重复任意次(包括零次)。重复值的顺序将保留。
由于历史原因,repeated
标量数字类型的字段编码效率不如预期。新代码应使用特殊选项[packed=true]
来获得更有效的编码。例如:
repeated int32 samples = 4 [packed=true];
您可以packed
在协议缓冲区编码中找到有关编码的更多信息。
永远是必需的您将字段标记为时应格外小心required
。如果您希望停止写入或发送必填字段,则将该字段更改为可选字段会很麻烦–老读者会认为没有该字段的邮件是不完整的,可能会无意中拒绝或丢弃它们。您应该考虑为缓冲区编写特定于应用程序的自定义验证例程。
当有人向枚举值添加时,出现带有必填字段的第二个问题。在这种情况下,无法识别的枚举值将被视为丢失,这也会导致所需的值检查失败。
添加更多消息类型
可以在一个.proto
文件中定义多种消息类型。如果要定义多个相关消息,这很有用–例如,如果要定义与您的SearchResponse
消息类型相对应的回复消息格式,则可以将其添加到相同的消息中.proto
:
message SearchRequest {
required string query = 1;
optional int32 page_number = 2;
optional int32 result_per_page = 3;
}
message SearchResponse {
...
}
合并消息会导致膨胀。虽然可以在单个.proto
文件中定义多种消息类型(例如,消息,枚举和服务),但是当在单个文件中定义大量具有不同依赖性的消息时,也会导致依赖性膨胀。建议每个.proto
文件包含尽可能少的消息类型。
添加评论
要将注释添加到.proto
文件中,请使用C / C ++样式//
和/* ... */
语法。
/* SearchRequest represents a search query, with pagination options to
* indicate which results to include in the response. */
message SearchRequest {
required string query = 1;
optional int32 page_number = 2; // Which page number do we want?
optional int32 result_per_page = 3; // Number of results to return per page.
}
保留字段
如果您通过完全删除字段或将其注释掉来更新消息类型,则将来的用户在自己对该类型进行更新时可以重用该字段号。如果他们以后加载相同版本的旧版本,可能会导致严重的问题.proto
,包括数据损坏,隐私错误等。确保不会发生这种情况的一种方法是,将已删除字段的字段编号(和/或名称,也可能导致JSON序列化的问题)指定为reserved
。如果将来有任何用户尝试使用这些字段标识符,则协议缓冲区编译器会抱怨。
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
请注意,您不能在同reserved
一条语句中混用字段名称和字段编号。
您产生了什么.proto
?
在上运行协议缓冲区编译器时.proto
,编译器会以您选择的语言生成代码,您将需要使用文件中描述的消息类型,包括获取和设置字段值,将消息序列化为输出流,并从输入流中解析消息。
- 对于C ++,编译器从每个生成一个
.h
和.cc
文件.proto
,并为文件中描述的每种消息类型提供一个类。 - 对于Java,编译器会生成一个
.java
文件,其中包含每种消息类型的类以及Builder
用于创建消息类实例的特殊类。 - Python稍有不同-Python编译器会在您的中生成一个模块,其中包含每种消息类型的静态描述符,
.proto
然后将该模块与元类一起使用,以在运行时创建必要的Python数据访问类。 - 对于Go,编译器
.pb.go
将为文件中的每种消息类型生成一个具有相应类型的文件。
您可以按照所选语言的教程,找到有关每种语言使用API的更多信息。有关API的更多详细信息,请参阅相关的API参考。
标量值类型
标量消息字段可以具有以下类型之一-该表显示.proto
文件中指定的类型,以及自动生成的类中的相应类型:
.proto类型 | 笔记 | C ++类型 | Java类型 | Python类型[2] | 去类型 |
---|---|---|---|---|---|
双 | 双 | 双 | 浮动 | * float64 | |
浮动 | 浮动 | 浮动 | 浮动 | * float32 | |
int32 | 使用可变长度编码。负数编码效率低下–如果您的字段可能具有负值,请改用sint32。 | int32 | 整型 | 整型 | * int32 |
int64 | 使用可变长度编码。负数编码效率低下–如果您的字段可能具有负值,请改用sint64。 | int64 | 长 | int / long [3] | * int64 |
uint32 | 使用可变长度编码。 | uint32 | 整数[1] | int / long [3] | * uint32 |
uint64 | 使用可变长度编码。 | uint64 | 长[1] | int / long [3] | * uint64 |
sint32 | 使用可变长度编码。有符号的int值。与常规int32相比,它们更有效地编码负数。 | int32 | 整型 | 整型 | * int32 |
sint64 | 使用可变长度编码。有符号的int值。与常规int64相比,它们更有效地编码负数。 | int64 | 长 | int / long [3] | * int64 |
固定的 | 始终为四个字节。如果值通常大于2 28,则比uint32更有效。 | uint32 | 整数[1] | int / long [3] | * uint32 |
固定64 | 始终为八个字节。如果值通常大于2 56,则比uint64更有效。 | uint64 | 长[1] | int / long [3] | * uint64 |
固定32 | 始终为四个字节。 | int32 | 整型 | 整型 | * int32 |
固定的 | 始终为八个字节。 | int64 | 长 | int / long [3] | * int64 |
布尔 | 布尔 | 布尔值 | 布尔 | *布尔 | |
串 | 字符串必须始终包含UTF-8编码或7位ASCII文本。 | 串 | 串 | unicode(Python 2)或str(Python 3) | *串 |
个字节 | 可以包含任意字节序列。 | 串 | 字节串 | 个字节 | []字节 |
当您在Protocol Buffer Encoding中对消息进行序列化时,您可以找到更多有关如何编码这些类型的信息。
[1]在Java中,无符号的32位和64位整数使用带符号的对等体表示,最高位仅存储在符号位中。
[2]在所有情况下,将值设置为字段都会执行类型检查以确保其有效。
[3] 64位或无符号32位整数在解码时始终表示为long,但是如果在设置字段时给出了int,则可以为int。在所有情况下,该值都必须适合设置时表示的类型。参见[2]。
可选字段和默认值
如上所述,消息描述中的元素可以被标记optional
。格式正确的消息可能包含也可能不包含可选元素。解析消息时,如果消息中不包含可选元素,则解析对象中的相应字段将设置为该字段的默认值。可以将默认值指定为消息描述的一部分。例如,假设你想提供的10为默认值SearchRequest
的result_per_page
值。
optional int32 result_per_page = 3 [default = 10];
枚举
在定义消息类型时,您可能希望其字段之一仅具有一个预定义的值列表之一。例如,假设你想添加一个corpus
字段每个SearchRequest
,其中语料库可以UNIVERSAL
,WEB
,IMAGES
,LOCAL
,NEWS
,PRODUCTS
或VIDEO
。您可以通过enum
在消息定义中添加来非常简单地执行此操作-enum
类型的字段只能使用一组指定的常量作为其值(如果尝试提供其他值,则解析器会将其视为未知值领域)。在以下示例中,我们添加了一个带有所有可能值的enum
被叫项Corpus
,以及一个type字段Corpus
:
message SearchRequest {
required string query = 1;
optional int32 page_number = 2;
optional int32 result_per_page = 3 [default = 10];
enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
optional Corpus corpus = 4 [default = UNIVERSAL];
}
您可以通过将相同的值分配给不同的枚举常量来定义别名。为此,您需要将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.
}
枚举器常量必须在32位整数范围内。由于enum
值在导线上使用varint编码,因此负值效率不高,因此不建议使用。您可以enum
在消息定义内定义,如上面的示例所示,enum
也可以在外部定义-这些s可以在.proto
文件中的任何消息定义中重复使用。您还可以使用enum
语法将一条消息中声明的类型用作另一条消息中字段的类型_MessageType_._EnumType_
。
当您在.proto
使用的上运行协议缓冲区编译器时enum
,生成的代码将具有对应enum
于Java或C ++的代码,或者具有特定EnumDescriptor
于Python的特殊类,用于在运行时生成的类中使用整数值创建一组符号常量。
**注意:**生成的代码可能会受到特定于语言的枚举数限制(一种语言的成千上万个)。请查看您计划使用的语言的限制。
有关如何enum
在应用程序中使用message的更多信息,请参见针对所选语言生成的代码指南。
保留值
如果通过完全删除枚举条目或将其注释掉来更新枚举类型,则将来的用户在自己对类型进行更新时可以重用数值。如果他们以后加载相同版本的旧版本,可能会导致严重的问题.proto
,包括数据损坏,隐私错误等。确保不会发生这种情况的一种方法是,将删除的条目的数字值(和/或名称,也可能导致JSON序列化的问题)指定为reserved
。如果将来有任何用户尝试使用这些标识符,则协议缓冲区编译器会抱怨。您可以使用max
关键字指定保留的数值范围达到最大可能值。
enum Foo {
reserved 2, 15, 9 to 11, 40 to max;
reserved "FOO", "BAR";
}
请注意,您不能在同reserved
一条语句中混合使用字段名和数字值。
使用其他消息类型
您可以使用其他消息类型作为字段类型。例如,假设你想包括Result
每个消息的SearchResponse
消息-要做到这一点,你可以定义一个Result
在同一个消息类型.proto
,然后指定类型的字段Result
中SearchResponse
:
message SearchResponse {
repeated Result result = 1;
}
message Result {
required string url = 1;
optional string title = 2;
repeated string snippets = 3;
}
导入定义
在上面的示例中,Result
消息类型与以下文件定义在同一文件中SearchResponse
–如果要用作字段类型的消息类型已在另一个.proto
文件中定义,该怎么办?
您可以.proto
通过导入其他文件来使用它们的定义。要导入another.proto
的定义,请在文件顶部添加一个import语句:
import "myproject/other_protos.proto";
默认情况下,您只能使用直接导入.proto
文件中的定义。但是,有时您可能需要将.proto
文件移动到新位置。.proto
现在,您可以直接.proto
在原位置放置一个虚拟文件,以使用该import public
概念将所有导入转发到新位置,而不是直接移动文件并一次更改所有呼叫站点。import public
任何导入包含该import public
语句的原型的人都可以过渡地依赖依赖项。例如:
// new.proto
// All definitions are moved here
// old.proto
// This is the proto that all clients are importing.
import public "new.proto";
import "other.proto";
// client.proto
import "old.proto";
// You use definitions from old.proto and new.proto, but not other.proto
协议编译器使用-I
/--proto_path
标志在协议编译器命令行上指定的一组目录中搜索导入的文件。如果未给出标志,它将在调用编译器的目录中查找。通常,应将--proto_path
标志设置为项目的根,并对所有导入使用完全限定的名称。
使用proto3消息类型
可以导入proto3消息类型并在proto2消息中使用它们,反之亦然。但是,不能在proto3语法中使用proto2枚举。
嵌套类型
您可以在其他消息类型中定义和使用消息类型,如以下示例所示–在此处,Result
消息是在SearchResponse
消息内部定义的:
message SearchResponse {
message Result {
required string url = 1;
optional string title = 2;
repeated string snippets = 3;
}
repeated Result result = 1;
}
如果要在其父消息类型之外重用此消息类型,则将其称为_Parent_._Type_
:
message SomeOtherMessage {
optional SearchResponse.Result result = 1;
}
您可以根据需要深度嵌套消息:
message Outer { // Level 0
message MiddleAA { // Level 1
message Inner { // Level 2
required int64 ival = 1;
optional bool booly = 2;
}
}
message MiddleBB { // Level 1
message Inner { // Level 2
required int32 ival = 1;
optional bool booly = 2;
}
}
}
团体
请注意,此功能已弃用,在创建新的消息类型时不应使用–而是使用嵌套的消息类型。
组是在信息定义中嵌套信息的另一种方法。例如,另一种指定SearchResponse
包含多个的Result
的方法如下:
message SearchResponse {
repeated group Result = 1 {
required string url = 2;
optional string title = 3;
repeated string snippets = 4;
}
}
组将嵌套的消息类型和字段简单地组合为一个声明。在您的代码中,您可以像对待此消息一样将其视为具有一个Result
类型字段result
(将后者的名称转换为小写,以便与前者不冲突)一样对待。因此,此示例与SearchResponse
上述示例完全等效,除了该消息具有不同的电汇格式。
更新消息类型
如果现有消息类型不再满足您的所有需求(例如,您希望消息格式具有一个额外的字段),但是您仍然希望使用以旧格式创建的代码,请不要担心!在不破坏任何现有代码的情况下更新消息类型非常简单。只要记住以下规则:
- 不要更改任何现有字段的字段编号。
- 您添加的任何新字段应为
optional
或repeated
。这意味着任何使用“旧”消息格式通过代码序列化的消息都可以被新生成的代码解析,因为它们不会丢失任何required
元素。您应该为这些元素设置合理的默认值,以便新代码可以与旧代码生成的消息正确交互。同样,由新代码创建的消息可以由旧代码解析:旧的二进制文件在解析时只会忽略新字段。但是,未知字段不会被丢弃,并且如果消息在以后进行序列化,则未知字段也会与之一起进行序列化–因此,如果消息传递给新代码,则新字段仍然可用。 - 只要在更新的消息类型中不再使用该字段号,就可以删除不需要的字段。您可能想要重命名该字段,或者添加前缀“ OBSOLETE_”,或者将字段编号保留为,以便将来的用户
.proto
不会意外地重用该编号。 - 只要类型和数字保持不变,就可以将不需要的字段转换为扩展名,反之亦然。
int32
,uint32
,int64
,uint64
,和bool
都是兼容的-这意味着你可以在现场从这些类型到另一种改变不破坏forwards-或向后兼容。如果从对应的类型不适合的导线中解析出一个数字,则将获得与在C ++中将数字强制转换为该类型一样的效果(例如,如果将64位数字读取为int32,它将被截断为32位)。sint32
并且sint64
彼此兼容,但与其他整数类型不兼容。string
并且bytes
只要字节是有效的UTF-8即可兼容。bytes
如果字节包含消息的编码版本,则嵌入式消息与之兼容。fixed32
与兼容sfixed32
,并fixed64
用sfixed64
。- 对于
string
,bytes
和消息字段,optional
与兼容repeated
。给定重复字段的序列化数据作为输入,如果期望此字段optional
是原始类型字段,则期望该字段的客户端将采用最后一个输入值;如果是消息类型字段,则将合并所有输入元素。请注意,这对于数字类型(包括布尔值和枚举)通常并不安全。重复的数字类型字段可以以打包格式序列化,当需要一个optional
字段时,将无法正确解析该格式。 - 只要您记住从未通过网络发送默认值,通常就可以更改默认值。因此,如果程序收到未设置特定字段的消息,则该程序将看到该程序协议版本中定义的默认值。它不会看到在发送者的代码中定义的默认值。
enum
与兼容int32
,uint32
,int64
,和uint64
电线格式条款(请注意,如果他们不适合的值将被截断),但是要知道,客户端代码可以区别对待反序列化的消息时。值得注意的是,enum
当对消息进行反序列化时,无法识别的值将被丢弃,这会使字段的has..
访问器返回false,并且其getter返回enum
定义中列出的第一个值;如果指定了默认值,则返回默认值。对于重复的枚举字段,所有无法识别的值将从列表中删除。但是,整数字段将始终保留其值。因此,在将整数升级为a时,enum
在接收在线上的枚举枚举值方面需要非常小心。- 在当前的Java和C ++实现中,当
enum
去除了无法识别的值时,它们会与其他未知字段一起存储。请注意,如果将此数据序列化然后由识别这些值的客户端重新解析,则可能导致奇怪的行为。对于可选字段,即使在反序列化原始消息后写入了新值,识别该值的客户端仍会读取旧值。在重复字段的情况下,旧值将出现在任何已识别的值和新添加的值之后,这意味着将不保留顺序。 - 将单个
optional
值更改为新 值的成员oneof
是安全且二进制兼容的。如果您确定没有代码一次设置多个optional
字段,那么将多个字段移动到新字段中oneof
可能是安全的。将任何字段移到现有字段中oneof
都是不安全的。 - 在
map<K, V>
和对应的repeated
消息字段之间更改字段是二进制兼容的(有关消息布局和其他限制,请参见下面的Maps)。但是,更改的安全性取决于应用程序:在反序列化和重新序列化消息时,使用repeated
字段定义的客户端将产生语义上相同的结果;但是,使用map
字段定义的客户端可以对条目进行重新排序,并删除具有重复键的条目。
扩展名
通过扩展,您可以声明消息中的字段号范围可用于第三方扩展。扩展名是其类型未由原始.proto
文件定义的字段的占位符。这允许其他.proto
文件通过使用这些字段编号定义某些或所有字段的类型来添加到您的消息定义中。让我们看一个例子:
message Foo {
// ...
extensions 100 to 199;
}
这表示字段编号[100,199] in的Foo
范围保留用于扩展。现在,其他用户可以使用指定范围内的字段编号将新字段添加到Foo
自己的.proto
文件中,以导入您的.proto
,例如:
extend Foo {
optional int32 bar = 126;
}
这会将名称为bar
126的字段添加到的原始定义Foo
。
对用户的Foo
消息进行编码后,有线格式与用户在中定义新字段的方式完全相同Foo
。但是,在应用程序代码中访问扩展字段的方式与访问常规字段略有不同–生成的数据访问代码具有用于处理扩展的特殊访问器。因此,例如,这是您bar
在C ++中设置value的方法:
Foo foo;
foo.SetExtension(bar, 15);
类似地,Foo
类定义模板访问器HasExtension()
,ClearExtension()
,GetExtension()
,MutableExtension()
,和AddExtension()
。所有的语义都与普通字段的相应生成的访问器匹配。有关使用扩展的更多信息,请参见针对所选语言生成的代码参考。
请注意,扩展名可以是任何字段类型,包括消息类型,但不能是oneofs或maps。
嵌套扩展
您可以在另一种类型的范围内声明扩展:
message Baz {
extend Foo {
optional int32 bar = 126;
}
...
}
在这种情况下,访问此扩展的C ++代码为:
foo.SetExtension(Baz :: bar,15);
换句话说,唯一的效果是bar
在的范围内定义的Baz
。
这是造成混淆的常见原因:声明extend
嵌套在消息类型内部的块并不意味着外部类型与扩展类型之间没有任何关系。特别地,上面的示例并不意味着Baz
是的任何子类Foo
。它的意思是符号bar
在范围内声明Baz
; 它只是一个静态成员。
一种常见的模式是在扩展名的字段类型范围内定义扩展名-例如,这Foo
是typeBaz
的扩展名,其中扩展名定义为的一部分Baz
:
message Baz {
extend Foo {
optional Baz foo_ext = 127;
}
...
}
但是,不需要在消息类型的内部定义具有消息类型的扩展名。您也可以这样做:
message Baz {
...
}
// This can even be in a different file.
extend Foo {
optional Baz foo_baz_ext = 127;
}
实际上,为了避免混淆,可能首选此语法。如上所述,嵌套语法经常被不熟悉扩展的用户误认为是子类。
选择分机号码
确保两个用户不要使用相同的字段号将扩展名添加到同一消息类型是非常重要的–如果意外将扩展名解释为错误的类型,则可能导致数据损坏。您可能要考虑为项目定义扩展名编号约定,以防止发生这种情况。
如果您的编号约定可能涉及具有非常大的字段号的扩展名,则可以使用max
关键字指定扩展范围达到最大可能的字段号:
message Foo {
extensions 1000 to max;
}
max
为2 29日- 1,或536870911。
与通常选择字段编号时一样,您的编号约定也需要避免使用字段编号19000到19999(FieldDescriptor::kFirstReservedNumber
到FieldDescriptor::kLastReservedNumber
),因为它们是为协议缓冲区实现保留的。您可以定义一个包括该范围的扩展名范围,但是协议编译器不允许您使用这些数字定义实际的扩展名。
---more---
https://developers.google.cn/protocol-buffers/docs/proto?hl=zh-cn
编码方式
本文档介绍了协议缓冲区消息的二进制线路格式。您无需了解这一点即可在应用程序中使用协议缓冲区,但是了解不同的协议缓冲区格式如何影响编码消息的大小可能非常有用。
一个简单的消息
假设您有以下非常简单的消息定义:
消息Test1 {可选int32 a = 1 ; }
在应用程序中,创建一条Test1
消息并将其设置a
为150。然后将消息序列化为输出流。如果您能够检查编码后的消息,则会看到三个字节:
08 96 01
到目前为止,如此小而数字-但是这意味着什么呢?继续阅读...
基础128种
要了解您的简单协议缓冲区编码,您首先需要了解varints。Varints是一种使用一个或多个字节序列化整数的方法。较小的数字占用较少的字节数。
除了最后一个字节外,varint中的每个字节都设置了最高有效位(msb)-这表明还有其他字节要来。每个字节的低7位用于以7位为一组存储数字的二进制补码表示,最低有效组在前。
因此,例如,这里是数字1 –它是一个字节,因此未设置msb:
0000 0001
讲解
本节中的每个教程都向您展示如何使用您喜欢的语言使用协议缓冲区来实现一个简单的应用程序,向您介绍该语言的协议缓冲区API,并向您展示创建和使用.proto文件的基础知识。还提供了每个应用程序的完整示例代码。
教程不假定您对协议缓冲区有任何了解,但假定您可以用所选语言编写代码,包括使用文件I / O,就很舒服。
协议缓冲区基础:C ++
https://developers.google.cn/protocol-buffers/docs/cpptutorial?hl=zh-cn
本教程提供了使用协议缓冲区的基本C ++程序员介绍。通过创建一个简单的示例应用程序,它向您展示了如何
- 在
.proto
文件中定义消息格式。 - 使用协议缓冲区编译器。
- 使用C ++协议缓冲区API写入和读取消息。
这不是在C ++中使用协议缓冲区的全面指南。有关更多详细的参考信息,请参阅《协议缓冲区语言指南》,《C ++ API参考》,《C ++生成的代码指南》和《编码参考》。
为什么要使用协议缓冲区?
我们将使用的示例是一个非常简单的“地址簿”应用程序,该应用程序可以在文件中读写人的联系方式。通讯录中的每个人都有一个姓名,一个ID,一个电子邮件地址和一个联系电话。
您如何像这样序列化和检索结构化数据?有几种方法可以解决此问题:
- 原始内存中的数据结构可以以二进制形式发送/保存。随着时间的流逝,这是一种脆弱的方法,因为接收/读取代码必须使用完全相同的内存布局,字节序等进行编译。此外,由于文件会以原始格式存储数据,并且为该格式连接的软件副本也会被存储。散布,很难扩展格式。
- 您可以发明一种将数据项编码为单个字符串的临时方法,例如将4个整数编码为“ 12:3:-23:67”。尽管确实需要编写一次性的编码和解析代码,但是这是一种简单且灵活的方法,而且解析带来的运行时成本很小。这对于编码非常简单的数据最有效。
- 将数据序列化为XML。由于XML是人类(一种)可读的,并且存在用于多种语言的绑定库,因此这种方法可能非常有吸引力。如果要与其他应用程序/项目共享数据,这可能是一个不错的选择。但是,众所周知,XML占用大量空间,对它进行编码/解码会给应用程序带来巨大的性能损失。同样,导航XML DOM树比通常导航类中的简单字段要复杂得多。
协议缓冲区是灵活,高效,自动化的解决方案,可以准确地解决此问题。使用协议缓冲区,您可以编写.proto
要存储的数据结构的描述。由此,协议缓冲区编译器创建了一个类,该类以有效的二进制格式实现协议缓冲区数据的自动编码和解析。生成的类为构成协议缓冲区的字段提供获取器和设置器,并以协议为单位来处理读写协议缓冲区的详细信息。重要的是,协议缓冲区格式支持随时间扩展格式的想法,以使代码仍可以读取以旧格式编码的数据。
在哪里可以找到示例代码
示例代码包含在源代码包中的“ examples”目录下。 在这里下载。
定义协议格式
要创建地址簿应用程序,您需要从.proto
文件开始。.proto
文件中的定义很简单:您为要序列化的每个数据结构添加一条消息,然后为消息中的每个字段指定名称和类型。这是.proto
定义您的消息的文件addressbook.proto
。
syntax = "proto2";
package tutorial;
message Person {
required string name = 1;
required int32 id = 2;
optional string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
required string number = 1;
optional PhoneType type = 2 [default = HOME];
}
repeated PhoneNumber phones = 4;
}
message AddressBook {
repeated Person people = 1;
}
如您所见,语法类似于C ++或Java。让我们浏览文件的每个部分,看看它的作用。
该.proto
文件以程序包声明开头,这有助于防止不同项目之间的命名冲突。在C ++中,您生成的类将放置在与程序包名称匹配的名称空间中。
接下来,您将拥有消息定义。消息只是包含一组类型字段的汇总。许多标准的简单数据类型都可以作为字段类型,包括bool
,int32
,float
,double
,和string
。您还可以通过使用其他消息类型作为字段类型来为消息添加进一步的结构-在上面的示例中,Person
消息包含PhoneNumber
消息,而AddressBook
消息包含Person
消息。您甚至可以定义嵌套在其他消息内的消息类型-如您所见,该PhoneNumber
类型在内部定义Person
。enum
如果您希望某个字段具有一个预定义的值列表之一,也可以定义类型-在这里您要指定电话号码可以是MOBILE
,HOME
或WORK
。
每个元素上的“ = 1”,“ = 2”标记标识该字段在二进制编码中使用的唯一“标记”。标签编号1至15与较高的编号相比,编码所需的字节减少了一个字节,因此,为了进行优化,您可以决定将这些标签用于常用或重复的元素,而将标签16和更高的标签用于较少使用的可选元素。重复字段中的每个元素都需要重新编码标签号,因此重复字段是此优化的最佳候选者。
每个字段都必须使用以下修饰符之一进行注释:
required
:必须提供该字段的值,否则该消息将被视为“未初始化”。如果libprotobuf
在调试模式下编译,则序列化未初始化的消息将导致断言失败。在优化的版本中,将跳过检查,并且无论如何将写入消息。但是,解析未初始化的消息将始终失败(通过false
从parse方法返回)。除此之外,必填字段的行为与可选字段完全相同。optional
:可能会或可能不会设置该字段。如果未设置可选字段值,则使用默认值。对于简单类型,您可以指定自己的默认值,就像type
在示例中为电话号码所做的那样。否则,将使用系统默认值:数字类型为零,字符串为空字符串,布尔值为false。对于嵌入式消息,默认值始终是消息的“默认实例”或“原型”,没有设置任何字段。调用访问器以获取未显式设置的可选(或必填)字段的值始终会返回该字段的默认值。repeated
:该字段可以重复任意次(包括零次)。重复值的顺序将保留在协议缓冲区中。将重复字段视为动态大小的数组。
永远是必需的 您将字段标记为时应格外小心required
。如果您希望停止写入或发送必填字段,则将该字段更改为可选字段会很麻烦–老读者会认为没有该字段的邮件是不完整的,可能会无意中拒绝或丢弃它们。您应该考虑为缓冲区编写特定于应用程序的自定义验证例程。Google的一些工程师得出的结论是,使用required
弊大于利。他们更喜欢只使用optional
和repeated
。但是,这种观点并不普遍。
您.proto
可以在“协议缓冲区语言指南”中找到有关编写文件的完整指南,包括所有可能的字段类型。但是,不要去寻找类似于类继承的工具–协议缓冲区不能做到这一点。
Protocol Buffer使用简介
https://www.jianshu.com/p/b1f18240f0c7
1.概览
1.1 什么是protocol buffer
protocol buffer是google的一个开源项目,它是用于结构化数据串行化的灵活、高效、自动的方法,例如XML,不过它比xml更小、更快、也更简单。你可以定义自己的数据结构,然后使用代码生成器生成的代码来读写这个数据结构。你甚至可以在无需重新部署程序的情况下更新数据结构。
2.使用
2.1定义一个消息类型
message SearchRequest
{
required string query = 1;
optional int32 page_number = 2;// Which page number do we want?
optional int32 result_per_page = 3;// Number of results to return per page.
}
该消息定义了三个字段,两个int32类型和一个string类型的字段,每个字段由字段限制,字段类型,字段名和Tag四部分组成.对于C++,每一个.proto
文件经过编译之后都会对应的生成一个.h
和一个.cc
文件.
字段限制
字段限制共有3类:required
:必须赋值的字段optional
:可有可无的字段repeated
:可重复字段(变长字段),类似于数组
由于一些历史原因,repeated
字段并没有想象中那么高效,新版本中允许使用特殊的选项来获得更高效的编码:
repeated int32 samples = 4 [packed=true];
Tags
消息中的每一个字段都有一个独一无二的数值类型的Tag.1到15使用一个字节编码,16到2047使用2个字节编码,所以应该将Tags 1到15留给频繁使用的字段.
可以指定的最小的Tag为1, 最大为2^{29}-1或536,870,911.但是不能使用19000到19999之间的值,这些值是预留给protocol buffer的.
注释
使用C/C++的//
语法来添加字段注释.
2.2 值类型
proto的值类型与具体语言中值类型的对应关系.
2.3 可选字段与缺省值
在消息解析时,如果发现消息中没有包含可选字段,此时会将消息解析对象中相对应的字段设置为默认值,可以通过下面的语法为optional
字段设置默认值:
optional int32 result_per_page = 3 [default = 10];
如果没有指定默认值,则会使用系统默认值,对于string
默认值为空字符串,对于bool
默认值为false,对于数值类型
默认值为0,对于enum
默认值为定义中的第一个元素.
2.4 枚举
message SearchRequest
{
required string query = 1;
optional int32 page_number = 2;
optional int32 result_per_page = 3 [default = 10];
enum Corpus
{
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
optional Corpus corpus = 4 [default = UNIVERSAL];
}
由于枚举值采用varint编码,所以为了提高效率,不建议枚举值取负数.这些枚举值可以在其他消息定义中重复使用.
2.5 使用其他消息类型
可以使用一个消息的定义作为另一个消息的字段类型.
message Result
{
required string url = 1;
optional string title = 2;
repeated string snippets = 3;
}
message SearchResponse
{
repeated Result result = 1;
}
可以使用import
语法来包含另外一个.proto
文件.
import "myproject/other_protos.proto";
2.6 嵌套类型
在protocol中可以定义如下的嵌套类型
message SearchResponse
{
message Result
{
required string url = 1;
optional string title = 2;
repeated string snippets = 3;
}
repeated Result result = 1;
}
如果在另外一个消息中需要使用Result
定义,则可以通过Parent.Type
来使用.
message SomeOtherMessage
{
optional SearchResponse.Result result = 1;
}
protocol支持更深层次的嵌套和分组嵌套,但是为了结构清晰起见,不建议使用过深层次的嵌套,建议通过 2.5 小节提到的方法来实现.
2.7 更新一个数据类型
在更新一个数据类型时更多的是需要考虑与旧版本的兼容性问题:
- 不要改变任何已存在字段的Tag值,如果改变Tag值可能会导致数值类型不匹配,具体原因参加protocol编码
- 建议使用
optional
和repeated
字段限制,尽可能的减少required
的使用. - 不需要的字段可以删除,删除字段的Tag不应该在新的消息定义中使用.
- 不需要的字段可以转换为扩展,反之亦然只要类型和数值依然保留
int32
,uint32
,int64
,uint64
, 和bool
是相互兼容的,这意味着可以将其中一种类型任意改编为另外一种类型而不会产生任何问题sint32
和sint64
是相互兼容的string
和bytes
是相互兼容的fixed32
兼容sfixed32
,fixed64
兼容sfixed64
.optional
兼容repeated
2.8 扩展
extend
特性来让你声明一些Tags值来供第三方扩展使用.
message Foo
{
// ...
extensions 100 to 199;
}
假如你在你的proto
文件中定义了上述消息,之后别人在他的.proto
文件中import你的.proto
文件,就可以使用你指定的Tag范围的值.
extend Foo
{
optional int32 bar = 126;
}
在访问extend中定义的字段和,使用的接口和一般定义的有点不一样,例如set方法:
Foo foo;
foo.SetExtension(bar, 15);
类似的有HasExtension(), ClearExtension(), GetExtension(), MutableExtension(), and AddExtension()
等接口.
2.9 选项
- optimize_for (file option): 可以设置的值有
SPEED
,CODE_SIZE
, 或LITE_RUNTIME
. 不同的选项会以下述方式影响C++, Java代码的生成.T- SPEED (default): protocol buffer编译器将会生成序列化,语法分析和其他高效操作消息类型的方式.这也是最高的优化选项.确定是生成的代码比较大.
- CODE_SIZE: protocol buffer编译器将会生成最小的类,确定是比SPEED运行要慢
- LITE_RUNTIME: protocol buffer编译器将会生成只依赖"lite" runtime library (libprotobuf-lite instead of libprotobuf)的类. lite运行时库比整个库更小但是删除了例如descriptors 和 reflection等特性. 这个选项通常用于手机平台的优化.
option optimize_for = CODE_SIZE;
3.常用API介绍
对于如下消息定义:
// test.proto
message PBStudent
{
optional uint32 StudentID = 1;
optional string Name = 2;
optional uint32 Score = 3;
}
message PBMathScore
{
optional uint32 ClassID = 1;
repeated PBStudent ScoreInf = 2;
}
protocol buffer编译器会为每个消息生成一个类,每个类包含基本函数,消息实现,嵌套类型,访问器等部分.
3.1 基本函数
public:
PBStudent();
virtual ~PBStudent();
PBStudent(const PBStudent& from);
inline PBStudent& operator=(const PBStudent& from) {
CopyFrom(from);
return *this;
}
inline const ::google::protobuf::UnknownFieldSet& unknown_fields() const {
return _unknown_fields_;
}
inline ::google::protobuf::UnknownFieldSet* mutable_unknown_fields() {
return &_unknown_fields_;
}
static const ::google::protobuf::Descriptor* descriptor();
static const PBStudent& default_instance();
void Swap(PBStudent* other);
3.2 消息实现
PBStudent* New() const;
void CopyFrom(const ::google::protobuf::Message& from);
void MergeFrom(const ::google::protobuf::Message& from);
void CopyFrom(const PBStudent& from);
void MergeFrom(const PBStudent& from);
void Clear();
bool IsInitialized() const;
int ByteSize() const;
bool MergePartialFromCodedStream(
::google::protobuf::io::CodedInputStream* input);
void SerializeWithCachedSizes(
::google::protobuf::io::CodedOutputStream* output) const;
::google::protobuf::uint8* SerializeWithCachedSizesToArray(::google::protobuf::uint8* output) const;
int GetCachedSize() const { return _cached_size_; }
private:
void SharedCtor();
void SharedDtor();
void SetCachedSize(int size) const;
3.3 嵌套类型
3.4 访问器
// optional uint32 StudentID = 1;
inline bool has_studentid() const;
inline void clear_studentid();
static const int kStudentIDFieldNumber = 1;
inline ::google::protobuf::uint32 studentid() const;
inline void set_studentid(::google::protobuf::uint32 value);
// optional string Name = 2;
inline bool has_name() const;
inline void clear_name();
static const int kNameFieldNumber = 2;
inline const ::std::string& name() const;
inline void set_name(const ::std::string& value);
inline void set_name(const char* value);
inline void set_name(const char* value, size_t size);
inline ::std::string* mutable_name();
inline ::std::string* release_name();
inline void set_allocated_name(::std::string* name);
// optional uint32 Score = 3;
inline bool has_score() const;
inline void clear_score();
static const int kScoreFieldNumber = 3;
inline ::google::protobuf::uint32 score() const;
inline void set_score(::google::protobuf::uint32 value);
protocol buffer编译器会对每一个字段生成一些get
和set
方法,这些方法的名称采用标识符所有小写加上相应的前缀或后缀组成.生成一个值为Tags的k标识符FieldNum
常量,
3.5 其他函数
除了生成上述类型的方法外, 编译器还会生成一些用于消息类型处理的私有方法. 每一个.proto
文件在编译的时候都会自动包含message.h文件,这个文件声明了很多序列化和反序列化,调试, 复制合并等相关的方法.
3.6 使用例子
在我们平时的使用中,通常一个message对应一个类,在对应的类中定义一个set和create方法来生成和解析PB信息.针对上述消息定义如下类:
// test.h
class CStudent
{
public:
unsigned mStudentID;
unsigned mScore;
string mName;
CStudent()
{
Init();
}
inline void Init()
{
mStudentID = 0;
mScore = 0;
mName = "";
}
}
class CMathScore
{
private:
unsigned mClassID;
CStudent mScoreInf[100];
public:
CMathSCore()
{
Init();
}
~CMathScore() {};
void Init();
void SetFromPB(const PBMathScore* pPB);
void CreatePB(PBMathScore* pPB);
// Get & Set mClassID
...
// Get & set mScoreInf
...
// some other function
...
}
对应的cpp
文件中实现对PB的操作
// test.cpp
void CMathScore::Init()
{
mClassID = 0;
memset(mScoreInf, 0, sizeof(mScoreInf));
}
void CMathScore::SetFromPB(const PBMathScore* pPB)
{
if ( NULL == pPB ) return;
mClassID = pPB->classid();
for(unsigned i = 0; i < (unsigned)pPB->scoreinf_size() && i < 100; ++i)
{
PBStudent* pStu = pPB->mutable_scoreinf(i);
mScoreInf[i].mStudentID = pStu->studentid();
mScoreInf[i].mScore = pStu->score();
mScoreInf[i].mName = pStu->name();
}
}
void CMathScore::CreatePB(PBMathScore* pPB)
{
if ( NULL == pPB ) return;
pPB->set_classid(mClassID);
for(unsigned i = 0; i < 100; ++i)
{
PBStudent* pStu = pPB->add_scoreinf();
pStu->set_studentid(mScoreInf[i].mStudentID)
pStu->set_score(mScoreInf[i].mScore);
pStu->set_name(mScoreInf[i].mName);
}
}
PB文件的读写
// use.cpp
#include<test.h>
#defind MAX_BUFFER 1024 * 1024
int write()
{
CMathScore mMath;
PBMathScore mPBMath;
// use set functions to init member variable
fstream fstm("./math.dat", ios::out | ios::binary);
if ( fstm.is_open() == false )
{
return -1;
}
char* tpBuffer = (char*)malloc(MAX_BUFFER);
if ( NULL == tpBuffer )
{
return -2;
}
mMath.CreatePB(&mPBMath);
if ( mPBMath.SerializeToArray(tpBuffer, mPBMath.ByteSize()) == false )
{
return -3;
}
fstm.write(tpBuffer, mPBMath.ByteSize());
free(tpBuffer);
fstm.close();
return 0;
}
int read()
{
CMathScore mMath;
PBMathScore mPBMath;
fstream fstm.open("./math.dat", ios::out | ios::binary);
if ( fstm.is_open() == false )
{
return -1;
}
char* tpBuffer = (char*)malloc(MAX_BUFFER);
if ( NULL == tpBuffer )
{
return -2;
}
char* tpIdx = tpBuffer;
int tLen;
while ( !fstm.eof() && tLen < MAX_BUFFER )
{
fstm.read(tpIdx, 1);
tpIdx += 1;
tLen++;
}
if ( mPBMath.ParseFromArray(tpBuffer, tLen - 1) == false )
{
return -3;
}
fstm.close();
free(tpBuffer);
tpIdx = NULL;
mMath.SetFromPB(&mPBMath);
// do some thing
return 0;
}
作者:倚楼
链接:https://www.jianshu.com/p/b1f18240f0c7
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。