Table of Contents

Protocol Buffers

定义消息类型

指定字段类型

分配字段编号

指定字段规则

添加更多消息类型

添加评论

保留字段

您产生了什么.proto?

标量值类型

可选字段和默认值

枚举

保留值

使用其他消息类型

导入定义

使用proto3消息类型

嵌套类型

团体

更新消息类型

扩展名

嵌套扩展

选择分机号码

编码方式

一个简单的消息

基础128种

讲解

协议缓冲区基础:C ++

为什么要使用协议缓冲区?

在哪里可以找到示例代码

定义协议格式

Protocol Buffer使用简介

1.概览

1.1 什么是protocol buffer

2.使用

2.1定义一个消息类型

字段限制

Tags

注释

2.2 值类型

2.3 可选字段与缺省值

2.4 枚举

2.5 使用其他消息类型

2.6 嵌套类型

2.7 更新一个数据类型

2.8 扩展

2.9 选项

3.常用API介绍

3.1 基本函数

3.2 消息实现

3.3 嵌套类型

3.4 访问器

3.5 其他函数

3.6 使用例子


我们项目中使用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_numberresult_per_page)和一个字符串(query)。但是,您也可以为字段指定复合类型,包括枚举和其他消息类型。

分配字段编号

如您所见,消息定义中的每个字段都有一个唯一的编号。这些数字用于标识消息二进制格式的字段,一旦使用了消息类型,就不应更改这些数字。请注意,范围为1到15的字段编号需要一个字节来编码,包括字段编号和字段的类型(您可以在Protocol Buffer Encoding中找到有关此内容的更多信息)。16到2047之间的字段号占用两个字节。因此,应该为非常频繁出现的消息元素保留字段编号1到15。切记为将来可能添加的频繁出现的元素留出一些空间。

您可以指定最小的场数是1,最大为2 29日- 1,或536870911。您也不能使用数字19000到19999(FieldDescriptor::kFirstReservedNumberFieldDescriptor::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为默认值SearchRequestresult_per_page值。

optional int32 result_per_page = 3 [default = 10];
如果未为可选元素指定默认值,则使用特定于类型的默认值:对于字符串,默认值为空字符串。对于字节,默认值为空字节字符串。对于布尔值,默认值为false。对于数字类型,默认值为零。对于枚举,默认值为枚举类型定义中列出的第一个值。这意味着在将值添加到枚举值列表的开头时必须格外小心。有关如何安全更改定义的指导,请参阅更新消息类型部分。

枚举

在定义消息类型时,您可能希望其字段之一仅具有一个预定义的值列表之一。例如,假设你想添加一个corpus字段每个SearchRequest,其中语料库可以UNIVERSALWEBIMAGESLOCALNEWSPRODUCTSVIDEO。您可以通过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,然后指定类型的字段ResultSearchResponse


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 0message MiddleAA {  // Level 1message Inner {   // Level 2required int64 ival = 1;optional bool  booly = 2;}}message MiddleBB {  // Level 1message Inner {   // Level 2required 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上述示例完全等效,除了该消息具有不同的电汇格式。

更新消息类型

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

  • 不要更改任何现有字段的字段编号。
  • 您添加的任何新字段应为optionalrepeated。这意味着任何使用“旧”消息格式通过代码序列化的消息都可以被新生成的代码解析,因为它们不会丢失任何required元素。您应该为这些元素设置合理的默认值,以便新代码可以与旧代码生成的消息正确交互。同样,由新代码创建的消息可以由旧代码解析:旧的二进制文件在解析时只会忽略新字段。但是,未知字段不会被丢弃,并且如果消息在以后进行序列化,则未知字段也会与之一起进行序列化–因此,如果消息传递给新代码,则新字段仍然可用。
  • 只要在更新的消息类型中不再使用该字段号,就可以删除不需要的字段。您可能想要重命名该字段,或者添加前缀“ OBSOLETE_”,或者将字段编号保留为,以便将来的用户.proto不会意外地重用该编号。
  • 只要类型和数字保持不变,就可以将不需要的字段转换为扩展名,反之亦然。
  • int32uint32int64uint64,和bool都是兼容的-这意味着你可以在现场从这些类型到另一种改变不破坏forwards-或向后兼容。如果从对应的类型不适合的导线中解析出一个数字,则将获得与在C ++中将数字强制转换为该类型一样的效果(例如,如果将64位数字读取为int32,它将被截断为32位)。
  • sint32并且sint64彼此兼容,但与其他整数类型兼容。
  • string并且bytes只要字节是有效的UTF-8即可兼容。
  • bytes如果字节包含消息的编码版本,则嵌入式消息与之兼容。
  • fixed32与兼容sfixed32,并fixed64sfixed64
  • 对于stringbytes和消息字段,optional与兼容repeated。给定重复字段的序列化数据作为输入,如果期望此字段optional是原始类型字段,则期望该字段的客户端将采用最后一个输入值;如果是消息类型字段,则将合并所有输入元素。请注意,这对于数字类型(包括布尔值和枚举)通常并不安全。重复的数字类型字段可以以打包格式序列化,当需要一个optional字段时,将无法正确解析该格式。
  • 只要您记住从未通过网络发送默认值,通常就可以更改默认值。因此,如果程序收到未设置特定字段的消息,则该程序将看到该程序协议版本中定义的默认值。它不会看到在发送者的代码中定义的默认值。
  • enum与兼容int32uint32int64,和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;
}

这会将名称为bar126的字段添加到的原始定义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::kFirstReservedNumberFieldDescriptor::kLastReservedNumber),因为它们是为协议缓冲区实现保留的。您可以定义一个包括该范围的扩展名范围,但是协议编译器不允许您使用这些数字定义实际的扩展名。

---more---

https://developers.google.cn/protocol-buffers/docs/proto?hl=zh-cn

编码方式

  • 一个简单的消息
  • 基础128种
  • 讯息结构
  • 更多值类型
  • 嵌入式消息
  • 可选和重复元素
  • 现场订单

本文档介绍了协议缓冲区消息的二进制线路格式。您无需了解这一点即可在应用程序中使用协议缓冲区,但是了解不同的协议缓冲区格式如何影响编码消息的大小可能非常有用。

一个简单的消息

假设您有以下非常简单的消息定义:

消息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 ++教程
  • C#教程
  • 飞镖教程
  • 入门教程
  • Java教程
  • Python教程

协议缓冲区基础: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 ++中,您生成的类将放置在与程序包名称匹配的名称空间中。

接下来,您将拥有消息定义。消息只是包含一组类型字段的汇总。许多标准的简单数据类型都可以作为字段类型,包括boolint32floatdouble,和string。您还可以通过使用其他消息类型作为字段类型来为消息添加进一步的结构-在上面的示例中,Person消息包含PhoneNumber消息,而AddressBook消息包含Person消息。您甚至可以定义嵌套在其他消息内的消息类型-如您所见,该PhoneNumber类型在内部定义Personenum如果您希望某个字段具有一个预定义的值列表之一,也可以定义类型-在这里您要指定电话号码可以是MOBILEHOMEWORK

每个元素上的“ = 1”,“ = 2”标记标识该字段在二进制编码中使用的唯一“标记”。标签编号1至15与较高的编号相比,编码所需的字节减少了一个字节,因此,为了进行优化,您可以决定将这些标签用于常用或重复的元素,而将标签16和更高的标签用于较少使用的可选元素。重复字段中的每个元素都需要重新编码标签号,因此重复字段是此优化的最佳候选者。

每个字段都必须使用以下修饰符之一进行注释:

  • required:必须提供该字段的值,否则该消息将被视为“未初始化”。如果libprotobuf在调试模式下编译,则序列化未初始化的消息将导致断言失败。在优化的版本中,将跳过检查,并且无论如何将写入消息。但是,解析未初始化的消息将始终失败(通过false从parse方法返回)。除此之外,必填字段的行为与可选字段完全相同。
  • optional:可能会或可能不会设置该字段。如果未设置可选字段值,则使用默认值。对于简单类型,您可以指定自己的默认值,就像type在示例中为电话号码所做的那样。否则,将使用系统默认值:数字类型为零,字符串为空字符串,布尔值为false。对于嵌入式消息,默认值始终是消息的“默认实例”或“原型”,没有设置任何字段。调用访问器以获取未显式设置的可选(或必填)字段的值始终会返回该字段的默认值。
  • repeated:该字段可以重复任意次(包括零次)。重复值的顺序将保留在协议缓冲区中。将重复字段视为动态大小的数组。

永远是必需的 您将字段标记为时应格外小心required。如果您希望停止写入或发送必填字段,则将该字段更改为可选字段会很麻烦–老读者会认为没有该字段的邮件是不完整的,可能会无意中拒绝或丢弃它们。您应该考虑为缓冲区编写特定于应用程序的自定义验证例程。Google的一些工程师得出的结论是,使用required弊大于利。他们更喜欢只使用optionalrepeated。但是,这种观点并不普遍。

.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 更新一个数据类型

在更新一个数据类型时更多的是需要考虑与旧版本的兼容性问题:

  1. 不要改变任何已存在字段的Tag值,如果改变Tag值可能会导致数值类型不匹配,具体原因参加protocol编码
  2. 建议使用optionalrepeated字段限制,尽可能的减少required的使用.
  3. 不需要的字段可以删除,删除字段的Tag不应该在新的消息定义中使用.
  4. 不需要的字段可以转换为扩展,反之亦然只要类型和数值依然保留
  5. int32, uint32, int64, uint64, 和bool是相互兼容的,这意味着可以将其中一种类型任意改编为另外一种类型而不会产生任何问题
  6. sint32sint64是相互兼容的
  7. stringbytes是相互兼容的
  8. fixed32 兼容 sfixed32, fixed64 兼容 sfixed64.
  9. 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编译器会对每一个字段生成一些getset方法,这些方法的名称采用标识符所有小写加上相应的前缀或后缀组成.生成一个值为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 variablefstream 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 thingreturn 0;
}

作者:倚楼
链接:https://www.jianshu.com/p/b1f18240f0c7
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

什么是Protocol Buffers / protobuf / protobuffer?一种服务器和客户端的消息交互方式相关推荐

  1. protobuf在java中使用_记录:Protocol Buffers(protobuf)在Java开发中使用

    1.编写一个.proto文件命名为:addressbook.proto,该文件内容来自protocal-buffers官网 2.使用protoc-2.6.0-win32.zip解压后的protoc.e ...

  2. 谷歌protobuf(Protocol buffers)的使用

    谷歌protobuf的使用 一.概述 二.安装 三.protobuf中的限定符 四.protobuf支持的数据类型 五.编译 1. 将proto文件编译成 C++ 文件 2. 将编译好的文件与代码一起 ...

  3. JSON与Protocol Buffers的一些比较

    转载来源:http://www.cnblogs.com/zhubo/archive/2011/07/06/JSON_And_ProtocolBuffers.html JSON与Protocol Buf ...

  4. Protocol Buffers简明教程

    随着微服务架构的流行,RPC框架渐渐地成为服务框架的一个重要部分.在很多RPC的设计中,都采用了高性能的编解码技术,Protocol Buffers就属于其中的佼佼者.Protocol Buffers ...

  5. Google Protocol Buffers介绍

    Google Protocol Buffers(简称Protobuf),是Google的一个开源项目,它是一种结构化数据存储格式,是Google公司内部的混合语言数据标准,是一个用来序列化(将对象的状 ...

  6. 架构师之路 — 分布式系统 — Protocol Buffers 序列化协议

    目录 文章目录 目录 Protocol Buffers 序列化协议 定义数据结构 序列化传输 Protocol Buffers 序列化协议 Protocol Buffers 是一种高性能的.语言无关的 ...

  7. 序列化--反序列化:Schema evolution in Avro, Protocol Buffers and Thrift

    当想要数据, 比如对象或其他类型的, 存到文件或是通过网络传输, 需要面对的问题是序列化问题 对于序列化, 当然各个语言都提供相应的包, 比如, Java serialization, Ruby's ...

  8. Protocol Buffers 在 iOS 中的使用

    翻译自:Introduction to Protocol Buffers on iOS 对大多数的应用来说,后台服务.传输和存储数据都是个重要的模块.开发者在给一个 web service 写接口时, ...

  9. spring 项目中集成 Protocol Buffers 示例

    http://blog.csdn.net/fangzhangsc2006/article/details/8687388 本文适用于了解spring框架,同时想在spring项目中使用Protocol ...

最新文章

  1. 第十二周项目一-实现复数类中的运算符重载(3)
  2. MySQL能够运行于多种操作系统平台_快速的掌握可以运行MySQL的操作系统
  3. .net 调用java service 代理类方法
  4. 5种ASP.NET页面间传递参数实例代码
  5. 02 JRE与JDK
  6. Django-组件拾遗
  7. 《物流系统_SSM》项目研发总结
  8. NPOI遍历excel表格
  9. Markdown从入门到放弃
  10. vue2百度地图修改比例尺位置
  11. c语言编写程序p1207.c,《C语言程序设计》(卷)考核班级
  12. 好文分享:一切都是最好的安排
  13. 教程篇(7.0) 08. FortiGate安全 Web过滤 ❀ Fortinet 网络安全专家 NSE 4
  14. java div截图_Html网页DIV截图功能
  15. 删除后一页数据后,分页显示上一页数据
  16. 关于 邵雍 渔樵问对 千古奇闻的思考
  17. 计算机应用 审稿时间,计算机应用研究发表要多长时间
  18. 从TCP拥塞本质看BBR算法及其收敛性(附CUBIC的改进/NCL机制)
  19. Vue 路由懒加载根据根路由合并chunk块
  20. 硬盘出现故障的5种表现

热门文章

  1. npm 运行报错“Cannot find module ‘@vue/component-compiler-utils/package.json‘”
  2. leetcode题解173-二叉搜索树迭代器
  3. CentOS 下重装 jdk
  4. markdown备忘
  5. 微信公众号开发--.Net Core实现微信消息加解密
  6. sublime3使用笔记
  7. 百度地图——判断用户是否在配送范围内解决方案
  8. 铁道部2012年版全国72个铁路枢纽城市
  9. Fluent Ribbon项目出现“命名空间“clr-namespace:Fluent;assembly=Fluent”中不存在“RibbonWindow”名称”的解决方法...
  10. eclipse查看android源码包(eclipse导入android源码包)