Protocol Buffer 语法(syntax)

本文详细介绍了 Protocol Buffer 的语法,包括定义 message、数据类型、optional 和默认值、枚举、message 嵌套、导入其他.proto 文件、更新 Message、Extensions、Oneof、Maps、packages 以及 Services 的定义。通过实例展示了如何创建和使用 .proto 文件,以构建自定义的 protocol buffer 数据结构。同时,文章讨论了后向兼容性和最佳实践,确保在更新 message 结构时保持兼容性。
摘要由CSDN通过智能技术生成

本文主要描述如何:1)利用protocol buffer来(在 .proto文件中)构建自定义protocol buffer数据结构以及.proto文件语法。2)如何生成一个“接入类”通过它我们可以接入到.proto文件中。

注:本文均翻译自Google Developers中的文档:https://developers.google.com/protocol-buffers/docs/proto

本文提供URL在Google Developer上面的,需要科学上网,推荐使用lantern

如果本文存在翻译错误,难以理解之处以及笔误等,请大家提出,谢谢!

本文会结合实例介绍protobuf语法。并且会给出一个实例训练。

定义message

首先,我们用一个实例说明。在这里我们定义一个搜索请求消息的格式:每一条搜索请求有一个string类型的请求,一个我们希望搜索的页数,以及每一页期望返回多少结果。那么我们可以在.proto文件中定义以下的结构:

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

SearchRequset message定义了我们所期望的3个域(field),每一个域都有name/value对,并且每一个域都有一个name 和一个type。

可自定义域的类型

我们在上面展示的例子中,所有的域都是标量:包含两个整数变量和一个string类型的变量。However,protobuf可以表示更复杂的数据形式。比如说我们可以用枚举表示一些向量,以及通过其他message组合出一些type。直观来讲有点类似C++中多个子类组合成一个类。

分配Tags

从上面的例子可以看出,我们在定义message的时候,每一个field都有一个数字。这时一个唯一标识符(在一个message层次中是unique的。嵌套message可以重新开始)。这些标识符称为tag它们是用来标识这些fields的二进制编码方式(序列化以及解析的时候会用到)。当这些tags的值在[1,15]区间内,则表示这些域的类型和以及tag一起用一个byte编码。在区间[16,2047]则表示用2个bytes进行编码。因此我们需要将[1,15]这些tag留给使用频率高的元素,并且在设计的时候要为未来可能高频使用的元素预留一些[1,15]中的tag。

tag的范围是在 12291 范围内,但是[19000,19999]这些tag是保留的,如果我们使用这些tag那么我们在编译.proto文件的时候会报错。当然如果我们声明了一些保留tag,那么那些tag也是不能使用的。

域(field)的介绍

我们在定义Message的时候protobuf提供了3种可选域:

  • required: message中必须至少包含一个required域,并且在序列化之前必须被赋值。
  • optional: message中需要包含0个以上optional域。
  • repeated:这个域用来保存一些要重复设置的变量,这些变量可以设置0次到多次。并且顺序保存。(用于设置数组)。

由于历史原因repeated域并不会做出非常有效的编码方式,因此我们需要在声明的时候加上一段选项[packed=true],让它以一个更有效的方式编码:

    repeated int32 samples = 4 [packed=true];

一些简单的操作

  • 将相关的Message定义在一个文件中,比如我们希望在SearchRequest之后再定义一个response:
    message SearchRequest{
    ......
    }
    message SearchResponse{
    ......
    }
  • 注释,和C++一样,用//

  • 设置保留域

这个设置的目的主要是考虑一个拓展性,如果我们在更新message的时候,仅仅是删除不需要的域或者是将其注释掉,那么将来我们如果需要在这个message上添加fields的时候就会重复利用这些tag。这会带来一个数据读取问题,以及一些数据隐私问题。因为有的用户可能使用的是旧版本的.proto文件生成的类,那么利用旧版本读取新数据的时候将会得到意想不到的结果。

好在Protocol buffer提供了一个机制(google程序员很牛哔~)。来避免这个的发生。我们将这些域设置成为reserved即“保留起来”。

    message Foo{
    reserved 2, 15, 9 to 11;
    reserved "foo", "var";
    }

通过这种方法,我们就不能使用这些域名字和这些tag了。

数据类型

.proto文件中必须使用这些数据类型,在编译过后这些数据类型会编译成为对应的类中的数据类型。

.proto中的类型注意C++中类型java中类型
doubledoubledouble
floatfloatfloat
int32自动调整编码长度,如果需要保存负数,请使用sint32int32int
int64自动调整编码长度,如果需要保存负数,请使用sint64int64long
uint32自动调整编码长度uint32int
uint64自动调整编码长度uint64long
sint32自动调整编码长度,表示有符号数,负数的编码效率高于int32int32int
sint64自动调整编码长度,表示有符号数,负数的编码效率高于int64int64long
fixed32固定使用4bytes编码,在编码大数( 228 )的时候比uint32更有效率int32int
fixed32固定使用8bytes编码,在编码大数( 256 )的时候比uint64更有效率int42long
sfixed32固定使用4bytes编码int32int
sfixed64固定使用8bytes编码int64long
boolboolboolean
stringstring只能包含UTF-8和7-bit ASCII文本stringString
bytes包含任意长度的bytesstringByteString

optional域和默认值

我们在上面提到,message中的元素可以用optional域来描述。一个message可以包含0个或者多个opational域。

在解析message时,如果不存在optional域那么就会用默认值来替代,如果没有设置默认值。那么就用系统的默认值替代。protobuf设计了很多默认值,比如:string的默认值是空,bool默认值是false,对于数来说所有的默认值都是0。对于所有的枚举类型,默认值均为第一个枚举值。设置optional域默认值的语法是:

optional int32 restult_per_page = 3 [default = 10];

枚举

当我们需要定义Message中的一个域,这个域的取值是我们预定义的一个集合中的一个元素。比如说,我们要在SearchRequest中添加一个域,这个域的取值是:UNIVERSAL, WEB, IMAGES, LOCAL, NEWS, PRODUCTS或者VIDEO中的一个,这时我们就需要枚举来帮忙了。

通过使用枚举,我们可以在message中添加一个枚举域,在这个域中定义一些常量。示例代码:

    message SearchRequest{
        required string query = 1;
        optional int32 page_numer =2;
        optional int32 result_per_page = 3[default = 10];
        enum Corpus{
            UNIVERAL = 0;
            WEB = 1;
            IMAGES = 2;
            LOCAL = 3;
            NEWS = 4;
            PRODUCTS = 5;
            VIDEO = 6;
        }
        optional Corpus corpus = 4 [default = UNIVERSAL];
    }

我们还可以在枚举中设置多个name对应一个相同的值,这是protocol buffer的别名机制(alias)。当然,我们必须首先设置这个别名,即,在enum中将allow_alias的选项设置为true。代码示例:

    enum EnumAllowingAlias{
        option allow_alias = true;
        UNKOWN = 0;
        STARTED = 1;
        RUNNING = 1;    
    }
    enum EnumNotAllowingAlias{
        UNKNOWN = 0;
        STARTED = 1;
    }

枚举类型必须能够用32bit整数表示,并且由于编码效率原因不建议使用负数。我们还可以在.proto文件中利用已经声明的枚举类型来声明一个域的类型。在不同message中可以使用相同的枚举类型。通过MessageType.EnumType使用(与C++中对象使用的方式类似)。

message嵌套

例如,我们希望在一个message中使用另一个message类型,则可以使用如下方法:

    message SearchResponse{
        repeated Result result = 1;
    }

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

导入其他.proto文件

上面的嵌套例子中,Result和SearchResponse在一个文件中,那么如果SearchResponse和Result不在一个文件中。应该怎么办呢?在我们的.proto文件顶部添加一个”import”即可导入其他文件,示例代码:

    import "myproject/other_protos.proto"

这个是默认情况下的导入,也是直接导入。protobuf使用了一种间接导入的机制(import public),将A.proto间接导入到B.proto,B.proto直接导入到C.proto。则C.proto也可以使用A.proto中的定义。这解决了什么问题呢?假设我们在原来.proto文件中做修改,那么必须通知所有客户端修改这份文件。如果使用间接导入的机制,我们只需要增加一个间接导入文件即可。

新增加文件:

    //new.proto
    //我们需要增加的定义放在这里

旧文件:

    //old.proto
    //这是客户端import的.proto文件
    import public "new.proto"
    import "other.proto"

客户端文件:

    //client.proto
    import "old.proto"
    //这样就可以在这里使用old.proto和new.proto中的定义,但是不可以使用other.proto中的定义

protocol 的编译器会在编译的时候搜索import文件目录,这里要通过编译选项-I/–proto_path (之后会介绍)设置。如果没有设置这个选项,则protocol编译器会自动在编译器所在目录进行搜索。

proto3 Message类型

由于历史原因protocol buffer有着不同版本,之后我们会介绍Proto3,因此本节仅仅做一个简单介绍。
这里就介绍一点,poto3可以使用proto2语法,但是反之不成立。也就是说我们可以在proto2文件中Import .proto3文件,但是不能再proto3中导入proto2文件。

嵌套(nested)和组合(group)

我们可以在Message中定义Message,这就是嵌套。Protocol可以有多层的嵌套,并且可以在其他的message(message_other)中使用嵌套在message_a中的Message。

在Message中定义一个Message并使用:

message SearchResponse {
  message Result {
    required string url = 1;
    optional string title = 2;
    repeated string snippets = 3;
  }
  repeated Result result = 1;
}

在其他地方使用上面定义的Message

message SomeOtherMessage {
  optional SearchResponse.Result result = 1;
}

嵌套定义Message

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;
    }
  }
}

我们可以将很多元素集中在Message中的一个域,这就是组合。However,组合已经是过时(deprcated)的用法了,现在我们定义一些新的Message都是用嵌套的方式。

组合的语法:

message SearchResponse {
  repeated group Result = 1 {
    required string url = 2;
    optional string title = 3;
    repeated string snippets = 4;
  }
}

更新Message

很多时候,随着项目的扩展,我们都需要在Message中增添一些额外的域,但是我们也希望之前的版本兼容新的proto文件。protocol buffer中的更新机制提供的一些兼容性设计,以下是一些更新规则:

  • 不可以修改已存在域中的tag
  • 所有新增添的域必须是 optional 或者 repeated。这就意味着,只要不遗失已经定义的required域,旧序列化信息可以被新的代码解析。为了新代码可以正常解析旧代码提供的序列,我们需要在更新的时候向新域中添加一些默认值(默认值是不会被传递的,仅仅在本地保存,也就是说不同版本的代码对同一个域可能读取不同的值)。同样,新代码产生的序列化文件也可以被旧代码解析,解析时旧代码会忽略新添加的域,这些域在解析时不会被丢弃,在旧代码序列化时,这些域会跟随其他域一起被序列化。将序列化之后的文件传递给新代码。这些新域也是有效的。
  • 非required域可以被删除。但是这些被删除域的tag不可以被重用。所以我们需要将其添加至reserved中,防止被意外使用。
  • 非required域可以被转化,转化时可能发生扩展或者截断,此时tag和name都是不变的。
  • int32, uint32, int64, uint64 和bool都是相互兼容的。这就意味着这些类型之间可以相互转换,并且都是向前,向后兼容的。如果我们设定的类别不足以表示数字,那么就会发生类似于C++的截断。
  • sint32和sint64是相互兼容的,并且不和其他整数类型兼容
  • string 在UTF-8编码时与bytes相互兼容
  • 嵌套Message在编码方式相同的情况下兼容bytes
  • fixed32兼容sfixed32。 fixed64兼容sfixed64。
  • optional兼容repeated。发送端发送repeated域,用户使用optional域读取,将会读取repeated域的最后一个元素。
  • 改变默认值通常来说是没有问题的。但值得注意的是:只要用户的版本没有更新,那么在没有设置这个域的情况下,用户依旧是读取原来的默认值。
  • 枚举类型兼容int32, uint32, int64, uint64(注意如果值和类型不匹配则会发生截断),所以我们需要注意,用户端可能在解析之后对这些值进行非enum的一些处理。通常,没能识别(不在enum定义的集合内)的enum值将会被丢弃,此时如果使用has方法,则会返回false。用getter方法读取这个值,会返回enum 列表的第一个值,如果定义了default则返回default的值。在repeated enum域里面所有不可识别的元素都将被移除这个列表,除非是整数域,它会保留整数域的值。因此,我们在提升一个整数类型为enum类型的时候要注意是否超过其值域。
  • 在现有的java和c++应用中,当不能识别的enum被移除时,它其实是和其他不可识别的元素一起保存在了“未知域(unknown fields)”。当客户端在此解析未知域的时候,如果能够识别,则有可能发生一些奇怪行为。在optional域中,在原来消息解析完毕并写入新的数据之后,用户依然可以读取原来的消息。在repeated域中,旧的值将会出现在新添加的值之后,因此其顺序不是固定的。

Extensions

Extensions可以让我们在Message中定义一些域,这些域可以交由第三方进行扩展我们定义的Message。
示例代码:

message Foo{
    //.....
    extensions 100 to 199;
    }

上述代码通过extensions为之后的扩展预留了[100,199]的tag。其他人可以扩展这个Message,并通过这些预留的tag定义自己的域。

extend Foo{
    optional int32 bar = 123;
}

这样我们就通过扩展Foo定义了一个域bar。
添加扩展之后的Message和我们自定义的Message(创建者自己添加域到中间)的编码结果是一样的。但是我们通过代码接入的方式略有差别,我们在编译.proto的时候会产生特殊的接入方法,例如在C++中:

Foo foo;
foo.SetExtension(bar,15);

类似的,Foo的类会提供一些其他的接入方式查看Foo的一些属性,如:HasExtensions(), ClearExtensions(), GetExtensions(), MutableExtension(), 以及AddExtension()。这些方法的功能和语义一致。其他语言的接口可以在protocol buffer的相关文档中找到。

**注意:**extensions可以定义为所有的域,包括Message但是不能定义为oneofs 和 maps

嵌套extensions

我们可以在其他域中定义嵌套的extensions:

message Baz{
    extend Foo{
        optional int32 bar = 126;
    }
    ...
}

这种情况下C++接入的方式为

Foo foo;
foo.SetExtension(Baz::bar, 15);

换句话说,这仅仅作用于Baz的bar域。

extensions很容易让人误以为是对原来域的扩展,其实从上例可以看出,,这种嵌入式的定义,并不表明二者存在子集的关系,并不表明Baz是Foo的子集。在Foo中使用extend表明,符号bar是在Baz中定义

一种比较常规的定义方式是将extensions定义在其中一个域的范围内,例如我们将Foo的extensions定义在Baz内,

message Baz{
    extend Foo{
        optional Baz foo_ext = 127;
    }
}

当然语法中也没有规定必须像上例一样,将extensions定义在它的域的范围内。所以也可以这样定义:

message Baz{
...
}

//这个可以在另外的文件中
extend Foo{
    optional Baz foo_baz_ext = 127; 
}

这样的定义也许可以更清晰展示其中的关系,而嵌套定义的方式会使得初学者误认为存在一个子集的关系。

选择extensions的tags

在extension中的同一个Message中切忌使用同一个tag。否则,将会造成数据损坏。所以设计的时候需要考虑一种tag序号约定,以免造成以上结果。

如果我们设计的tag序号约定包含了一些很大的tag序号,那么我们可以这么做:选择一个起始tag序号,将其范围设置到最大值(这时需要利用max关键字)

message Foo{
    extensions 1000 to max;
}

max是 2191 即,536,870,911

在定义tag序号约定时候,我们需要避免使用[19000,1999]这些序号,因为这些序号是为protocol buffer预留的。虽然定义的tag需要约束在这个范围内不会报错,但是在protocol 编译的时候不会让我们使用这写保留的tag的。

Oneof

如果我们定义的Message包含很多optional域,但是我们仅仅会set这些optional中的最多一个。我们就可以使用oneof来保证这种set机制,并且会节省memory。(注:并不是说optional域在oneof中,这里只是举个例子)

oneof域和optional域仅有一个不同,就是oneof域中的所有域是共享memory的,并且每一次只能够设置一个域,我们在设置oneof中的任意一个域,将会自动清除其他设置。(很容易理解,因为只有一个使用空间)。如果oneof域中设置了其中一个域的时候,使用case()或者是WhichOneof()方法(这要根据我们使用的语言选择)来查看这个域。

如何使用Oneof

在.proto文件中定义oneof,需要使用oneof关键字,并在其后设置 oneof的名字:

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

我们可以在Oneof域中定义oneof域,并且可以在oneof中添加任意的域,但是不可以使用required,optional,repeated关键字(但是其中出现的自定义结构可以含有这些)。

在proto编译之后的代码中,我们可以看到Oneof的getter和setter方法和普通的optional域生成出来的是一样的,但是它还会提供一写特殊的方法来查看某一个域是不是设置好了。在编写这个的时候可能会需要根据语言选择API,API传送门:https://developers.google.com/protocol-buffers/docs/reference/overview

oneof的特性

  • 设置oneof的域,会自动清除之前的设置(毕竟只有一个空间嘛!),所以当我们对oneof进行一系列设置的时候,仅会保留最后一个值。
SampleMessage message;
message.set_name("Mallock");
CHECK(message.has_name());
message.set_mutable_sub_message();//注意,这条语句会清除之前的设置!
CHECK(!message.has_name());
  • 如果解析器在一个oneof中解析到了多个值,那么仅仅会使用最后一个解析值
  • extensions不支持oneof
  • oneof域中不可以出现repeated
  • 反射机制的API可以在oneof域上工作
  • 如果使用C++编程,需要注意避免代码出现memory crashes。以下代码会崩溃,因为sub_message在调用set_name()的时候已经删除。
SampleMessage message;
Submessage *sub_message = message.mutable_sub_message();
message.set_name("Mallock") ;//注意,这里sub_message已经被删除了
sub_message->set_...//Ops~这里崩溃了
  • 又是C++,如果对两个oneof使用了Swap()方法,那么这两个message将会相互交换设置域。如下,msg1将会有sub_message域,msg2将会有name域。
SampleMessage msg1;
msg1.set_name("Mallock");
SampleMessage msg2;
msg2.mutable_sub_message();
msg1.swap(&msg2);
CHECK(msg1.has_sub_message());
CHECK(msg2.has_name());

后向兼容

谨记,在添加和删除域的时候要格外注意(其实proto的修改都要注意这个问题)。如果我们在检测oneof的值的时候返回了None/NOT_SET,这表示,oneof还没有被设置,或者是oneof在不同的版本中被设置。没有方法区分这两种情况,由于没法检测这个未知的域是不是oneof中的域。

Tag的重用问题:
* 在oneof中添加或者删除optional域:在Message被序列化或解析之后,有可能会丢失一些信息(一些域可能会被清除)
* 删除一个oneof域或者是把这个域添加回来:在Message被序列化或解析之后,可能会清除现在已经设置的域。
* 分裂或者融合oneof:和第一个类似。

Maps

protocol buffer提供了map关键字,我们可以在数据结构中定义一个关联映射的结构:

map<key_type, value_type> map_field = N;

key_type是一种整数或者string类型(除了浮点和bytes类型的标量均可)。
value_type可以是任意类型。

我们需要定义一个map,它是string类型和Project的关系映射(Project是一个Message)。那么可以定义成为如下形式:

map<string, Project> projects = 3;

Maps的特性

  • map不支持extensions
  • Maps不能定义为repeated, optional, 或者required
  • 在编码之后map中值的顺序是未定义的,遍历map的value时,这些value并不会遵循某一特定顺序。
  • 为proto文件生成的文档中,maps是按照key来排序的,如果key是蒸煮,那么就按照整数顺序排序。
  • 如果在解析序列化文件的时候出现多个Key的情况,那么将会使用最后一个。如果在解析文本文件的时候出现多个key,那么将会报错。

后向兼容

map的语法和下面的代码生成的数据格式是等价的。因此不支持Map的protocol buffer也可以利用下面的语法来完成map的工作

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

repeated MapFieldEntry map_field = N;

packages

通过使用packages选项,可以避免一个.proto文件中不同Message中名字冲突问题。

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

我们可以利用package来定义Message中的类型:

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

package 和名字解析

这里的类型名字解析类似于C++,先从最内部开始解析。然后向外层解析。每一个package相对于其父package都是内层。添加前导‘.’表示从外层开始解析(如.foo.bar.Baz)。

定义Services

to be continued…

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值