当前位置: 首页 > 后端技术 > Java

ProtocolBuffers系列(三)-proto2.proto语法指南

时间:2023-04-01 23:37:39 Java

本文介绍了如何使用ProtocolBuffers语言构造protocolbuffer数据,包括.proto文件语法以及如何从.proto文件生成数据访问类。它涵盖了ProtocolBuffers语言的proto2版本。本文只是一个参考指南,以后会有Java语言教程。如何使用本指南?工作中遇到,可以通过查询关键词找到自己需要的知识点。定义消息类型首先,我们从一个简单的示例开始,假设我们要构建一个搜索请求消息格式。每条搜索消息包括三个参数:查询字符串指定的页码和结果数下面是这个.proto消息对应的消息格式SearchRequest{requiredstringquery=1;可选的int32page_number=2;optionalint32result_per_page=3;}SearchRequest消息定义指定了三个字段(也称为名称或键值对),每个字段都有一个名称和一个类型。指定字段类型在上面的例子中,可以看到有两种类型:两种整数(页码和每页的结果数),和字符串(查询条件)。当然,我们也可以将字段定义为复合类型,包括枚举和其他消息类型。分配字段编号在上面的例子中,可以看到每个字段都有一个唯一的编号,用于在二进制消息中标识我们的字段,相当于字段的别名。1到15范围内的字段用一个字节表示。16~2047范围内的字段用两个字节表示。因此,为了提高性能,我们尽量将出现频率很高的字段保持在1~15的范围内。最小的数是1,最大的数是2^29-1,即536,870,911。请注意,数字19000到19999不能使用,因为它们是为ProtocolBuffers实现保留的-如果在.proto中使用这些保留数字之一,ProtocolBuffers编译器会报错。同一条消息中的数字不能重复,这一点也需要注意。指定字段规则required:简单理解为必填字段,个数为1。optional:可选,个数不超过1。repeated:修改的字段可以重复任意次数,包括0次。重复值的顺序也被记录下来。由于历史原因,标量数字类型(例如int32、int64、枚举)的重复字段没有像它们应该的那样有效地编码。新代码应该使用特殊选项[packed=true]来提高编码效率。例如:repeatedint32samples=4[packed=true];重复的ProtoEnum结果=5[packed=true];packed是压缩字段,后续文章会解释。required也意味着永远,所以我们在设置字段的时候,需要尽可能的考虑字段的范围。添加更多消息类型我们可以在一个.proto文件中设置多种消息类型。messageSearchRequest{要求的字符串查询=1;可选的int32page_number=2;可选int32result_per_page=3;}messageSearchResponse{...}虽然可以在单个.proto文件中定义多种消息类型(例如message、enum和service),但在定义大量具有不同依赖项的消息时也会导致依赖项膨胀单个文件。官方建议在每个.proto文件中包含尽可能少的消息类型,但这个值并没有给出具体范围,需要根据实际情况评估。Reservedfields当我们更新消息结构,删除某个字段或者注释掉某个字段时,以后的用户可以使用我们删除的字段对应的编号,这是没有问题的。但是一旦后期加载.proto修改前的旧版本,就会因为数字冲突出现问题,导致数据混乱,所以可以引入保留字段。消息Foo{保留2、15、9到11;reserved"foo","bar";}保留字段编号的范围包括:2,15,9,10,11,也可以用9到max来保留后面的所有编号。注意字段名和字段号不能在同一个保留语句中混用。.proto文件自动生成了什么?对于java,编译器生成一个.java文件,其中包含每个消息类型的类,以及用于创建消息类实例的特殊Builder类。类型比较表。原型类型描述java类型doubledoublefloatfloatint32使用可变长度编码。编码负数效率低下-如果您的字段可以有负值,请改用sint32。intint64使用可变长度编码。编码负数效率低下-如果您的字段可以有负值,请改用sint64。longuint32使用可变长度编码。intuint64使用可变长度编码。longsint32使用可变长度编码。一个带符号的int值。这些编码负数比常规int32更有效。intsint64使用可变长度编码。一个带符号的int值。这些编码负数比常规int64更有效。longfixed32总是4个字节。如果值通常大于2^28,则比uint32更有效。intfixed64总是8个字节。如果值通常大于2^56,则比uint64更有效。longsfixed32始终为4个字节。intsfixed64总是8个字节。longboolbooleanstring必须始终包含UTF-8编码的文本。Stringbytes可以包含任意字节序列。ByteString可选字段和默认值当一个字段设置为可选时,我们的消息可能包含也可能不包含该字段。我们可以为不包含的可选字段设置默认值。可选int32result_per_page=3[默认=10];如果未指定默认值,则为系统默认值。string为空字符串,bool为false,integer为0,枚举为枚举类型的第一个值。因此,在设置枚举类型时,要特别注意。枚举我们可以在消息内部定义一个枚举,并为它设置一个数字,比如我们设置一个语料库枚举。messageSearchRequest{要求的字符串查询=1;可选的int32page_number=2;可选int32result_per_page=3[默认=10];枚举语料库{UNIVERSAL=0;网络=1;图像=2;本地=3;新闻=4;=5;视频=6;}可选语料库语料库=4[默认=通用];}在同一个枚举中。如果我们想让多个枚举值对应一个数字,我们可以使用别名。在下面的例子中,上面的例子不会有问题,但是下面的例子会有错误提示。注意:如果枚举名称不同,但枚举值相同,也会报错,但如果在不同的消息中,则不会报错。enumEnumAllowingAlias{optionallow_alias=true;未知=0;开始=1;RUNNING=1;}enumEnumNotAllowingAlias{未知=0;开始=1;//运行=1;//取消注释这一行会导致谷歌内部出现编译错误,外部会出现警告信息。}使用其他消息类型您可以使用其他消息类型作为字段类型。例如,假设您想在每个SearchResponse消息中包含一个Result消息——为此,您可以在同一个.proto中定义一个Result消息类型,然后在SearchResponse中指定一个Result类型的字段:messageSearchResponse{repeated结果result=1;}messageResult{要求的字符串url=1;可选字符串标题=2;repeatedstringsnippets=3;}import定义当我们需要在另一个.proto文件中使用消息时,我们可以导入其他.proto文件定义来使用它们。要导入另一个.proto的定义,请在文件顶部添加导入语句:import"myproject/other_protos.proto";默认情况下,我们只能使用直接导入的.proto文件中的定义。但是,有时我们可能需要将.proto文件移动到新位置。我们可以在旧位置放置一个占位符.proto文件,而不是直接移动.proto文件并在一次更改中更新所有调用站点,以使用导入通用文件的概念将所有导入转发到新位置。//new.proto//所有定义都移到这里//old.proto//这是所有客户端正在导入的原型。importpublic"new.proto";import"other.proto";//client.protoimport"old.proto";//你使用old.proto和new.proto的定义,但不使用other.proto导入包含importpublic语句的proto的任何代码都可以传递依赖于importpublic依赖项。嵌套类型我们可以在其他消息类型中定义和使用消息类型,如下例所示-这里的结果消息在SearchResponse消息中定义:messageSearchResponse{messageResult{requiredstringurl=1;可选字符串标题=2;重复的字符串片段=3;}repeatedResultresult=1;}如果需要在其他消息中引用result,需要指定result外围类(父类)的编号。messageSomeOtherMessage{optionalSearchResponse.Resultresult=1;}您可以根据需要嵌套消息。在下面的示例中,请注意两个名为Inner的嵌套类型是完全独立的,因为它们在不同的消息中定义:messageOuter{//Level0messageMiddleAA{//Level1messageInner{//Level2optionalint64ival=1;可选布尔booly=2;}}messageMiddleBB{//Level1messageInner{//Level2optionalstringname=1;可选布尔标志=2;}}}updatemessageif现有的消息类型不再满足我们所有的需求——例如,我们希望消息格式有一个额外的字段——但是我们想使用之前用旧格式创建的代码,不用担心!在不破坏任何现有代码的情况下更新消息类型非常简单。请记住以下规则:不要更改任何现有字段的字段编号。您添加的任何新字段都应该是可选的或重复的。这意味着任何由使用“旧”消息格式的代码序列化的消息都可以被新生成的代码解析,因为它们不会丢失任何必需的元素。您应该为这些元素设置合理的默认值,以便新代码可以正确地与旧代码生成的消息进行交互。同样,新代码创建的消息可以被旧代码解析:旧二进制文件在解析时忽略新字段。但是,未知字段不会被丢弃,如果消息稍后被序列化,未知字段也会随之序列化——所以如果消息被传递给新代码,新字段仍然可用。如果更新的消息类型中不再使用某些字段编号,则可以删除非必填字段。您可能想要重命名该字段,也许添加前缀“OBSOLETE_”,或者保留字段编号,以便您的.proto的未来用户不会意外地重复使用该编号。只要类型和编号保持不变,非必填字段就可以转换为扩展(扩展将在后面讨论),反之亦然。int32、uint32、int64、uint64和bool都是兼容的——这意味着您可以将字段从这些类型中的一种更改为另一种,而不会破坏向前或向后兼容性。sint32和sint64彼此兼容,但与其他整数类型不兼容。只要字节采用有效的UTF-8编码格式,字符串和字节就可以兼容。fixed32与sfixed32兼容,fixed64与sfixed64兼容。可选与字符串、字节和消息字段的重复兼容。给定一个重复字段的序列化数据作为输入,如果它是原始类型字段,则期望此字段是可选的客户端将采用最后一个输入值,或者如果它是消息类型字段元素,则合并所有输入。请注意,这对于数字类型(包括布尔值和枚举)通常是不安全的。numeric类型的重复字段可以序列化为packed(packedlater)格式,当需要可选字段时,将无法正确解析。更改默认值通常没问题,但请记住,默认值永远不会通过网络发送。因此,如果某个程序收到一条消息,指出某个特定字段未设置,则该程序将看到在该程序的协议版本中定义的默认值。它不会看到发件人代码中定义的默认值。尽量不要修改枚举值,否则会出现一些奇怪的问题。在map和相应的重复消息字段之间更改字段是二进制兼容的(有关消息布局和其他限制,请参阅下面的地图)。然而,更改的安全性取决于应用程序:使用重复字段定义的客户端在反序列化和重新序列化消息时将产生语义相同的结果;但是,使用映射字段定义的客户端可能会重新排序条目并删除具有重复键的条目,因为映射的键不能重复。ExtensionsExtensions扩展允许我们在消息中为第三方扩展声明一系列字段编号。扩展是原始.proto文件中未定义类型字段的占位符。这允许通过使用这些字段编号来定义字段类型,将其他.proto文件添加到我们的消息定义中。我们看一个例子:messageFoo{//...extensions100to199;}这意味着Foo中的字段编号范围[100,199]是为扩展保留的。其他用户现在可以在他们自己的导入我们的.proto的.proto文件中向Foo添加新字段,使用我们指定范围内的字段编号-例如:extendFoo{optionalint32bar=126;}这将在添加一个名为字段编号为126的bar到Foo的原始定义。具体操作请参考后续Java开发指南。请注意,扩展可以是任何字段类型,包括消息类型,但不能是oneofs或Maps。嵌套扩展我们在消息Baz{extendFoo{optionalint32bar=126;中声明扩展消息}...}唯一的区别是bar是在Baz的范围内定义的。这是一个常见的混淆来源:在消息中声明一个嵌套的扩展块并不意味着外部类型和扩展类型之间有任何关系。特别是,上面的例子并不意味着Baz是Foo的任何子类。这意味着符号bar是在Baz的范围内声明的;它只是一个静态成员。一种常见的模式是在扩展字段类型的范围内定义扩展-例如,这里是Baz类型的Foo的扩展,其中扩展被定义为Baz的一部分:messageBaz{extendFoo{optionalBazfoo_ext=127;}...}但是,不要求在类型中定义具有消息类型的扩展。你也可以这样做:messageBaz{...}//这甚至可以在不同的文件中。extendFoo{optionalBazfoo_baz_ext=127;事实上,为了避免混淆,最好使用这种语法。如上所述,嵌套语法经常被不熟悉扩展的用户误认为是子类化。Oneof当我们的消息包含多个可选字段,并且最多分配一个时,我们可以使用oneOf。除了共享内存中的所有字段,其中oneof字段类似于可选字段,最多可以同时设置一个字段。设置oneof的其中一个成员会自动清除所有其他成员。根据我们选择的语言,可以使用特殊的case()或WhichOneof()方法检查在oneof中设置的值(如果有)。要在.proto中定义一个oneof,请使用oneof关键字后跟oneof名称,在本例中为test_oneof:messageSampleMessage{oneoftest_oneof{stringname=4;SubMessagesub_message=9;}}请注意,required、optional、repeated修改oneof字段。如果需要向oneof添加重复字段,可以使用从包含重复字段的消息生成的代码,oneof字段与常规可选方法具有相同的getters和setters。我们还有一种特殊的方法来检查在其中设置了哪个值(如果有的话)。oneof属性设置一个oneof字段将自动清除oneof的所有其他成员。所以如果你设置了多个oneof字段,只有你设置的最后一个字段还有值。SampleMessage消息;message.set_name("name");CHECK(message.has_name());message.mutable_sub_message();//将清除名称field.CHECK(!message.has_name());解析oneof时,则只会使用最后一个有值的成员。oneof不支持扩展。其中一个不能重复。反射API使用其中一个字段。如果设置了默认值,它将在序列化时被解析。添加或删除其中一个字段时,需要注意。如果勾选oneof字段,返回none或者not_set,说明我们没有设置oneof字段,或者设置了不同版本的oneof字段。此时无法区分该字段是否存在并被清除或不存在。.oneof在特定场景下非常方便,比如满足这些条件,one不为空。所以考虑场景,避免修改,控制好版本。Maps提供了映射键值对的快捷方式mapmap_field=N;其中key_type可以是任何整数或字符串类型(因此,除浮点类型和字节类型之外的任何标量)。请注意,枚举不是有效的key_type。value_type可以是除另一个映射之外的任何类型。因此,例如,如果您想创建一个项目映射,其中每个项目消息都与一个字符串键相关联,您可以这样定义它:mapprojects=3;Mapattributedoesnotsupportextensionscannotbe重复,可选,需要修改。大多数情况下,它是乱序的,不能作为排序标准。生成文本格式的.proto文件时,会按键排序。对于重复键,使用最后一次看到的键。常用的功能几乎都涵盖了。如果遇到没有提到的用法,可以参与评论,我会更新的~谢谢支持~