您的当前位置:首页正文

Protobuf基础知识

2023-06-10 来源:易榕旅网


Protobuf基础知识

约定:为方便书写,ProtocolBuffers在下文中将已Protobuf代替。

1、什么是protocol buffers

Protocol buffers是一个灵活的、高效的、自动化的用于对结构化数据进行序列化的协议,与XML相比,Protocol buffers序列化后的码流更小、速度更快、操作更简单。你只需要将要被序列化的数据结构定义一次(译注:使用.proto文件定义),便可以使用特别生成的源代码(译注:使用protobuf提供的生成工具)轻松的使用不同的数据流完成对这些结构数据的读写操作,即使你使用不同的语言(译注:protobuf的跨语言支持特性)。你甚至可以更新你的数据结构的定义(译注:就是更新.proto文件内容)而不会破坏依赖“老”格式编译出来的程序。

为什么不使用XML?

相对于XML,protocol buffers在序列化结构数据时拥有许多先进的特性:

1、更简单

2、序列化后字节占用空间比XML少3-10倍

3、序列化的时间效率比XML快20-100倍

4、具有更少的歧义性

5、自动生成数据访问类方便应用程序的使用

事物总有两面性,和XML相比protocol buffers并不总是更好的选择,例如,protocol buffers并不适合用来描述一个基于文本的标记型文档(比如HTML),因为你无法轻易的交错文本的结构。另外,XML具有很好的可读性和可编辑性;而protocol buffers,至少在它们的原生形式上并不具备这个特点。XML同时也是可扩展、自描述的。而一个protocol buffer只有在具有message 定义(在.proto文件中定义)时才会有意义。

2、定义一个PB message类型

假如现在需要定义搜索请求的message格式,每条message包含三个字段:搜索语句(query string),需要的返回结果页数(page_number),以及该页上的结果数。可如下定义.proto文件。

message SearchRequest {

required string query = 1;

optional int32 page_number = 2;

optional int32 result_per_page = 3;

}

• 1

• 2

• 3

• 4

• 5

这个”搜索请求”消息指定了三个字段(名称/属性 组合),每一个你想要包含在这类型的信息内的东西,都必须有一个字段,每个字段有一个名称和类型。

2.1.指定字段类型

在上面的示例中,所有的字段都是标量类型(scalar types):两个整数(integers:page_number 和 result_per_page)和一个字符串(string:query:查询的关键字),不过你可以在你的字段内指定符合类型。包括枚举类型(enumerations)和其他的消息类型

2.2.给字段赋值数字标签

如你所见,每个消息的字段都有一个唯一的数字标签,这些标签用来表示你的字段在二进制消息(message binary format)中处的位置。并且一旦指定标签号,在使用过程中是不可以更改的,标记这些标签号在1-15的范围内每个字段需要使用1个字节用来编码这一个字节包括字段所在的位置和字段的类型!(需要更多关于编码的信息请点击Protocol Buffer Encoding)。标签号在16-2047需要使用2个字节来编码。所以你最好将1-15的标签号为频繁使用到的字段所保留。如果将来可能会添加一些频繁使用到的元素,记得留下一些1-15标签号。

最小可指定的标签号为1,最大的标签号为229 - 1或者536870911。不能使用19000-19999

(FieldDescriptor::kFirstReservedNumber

FieldDescriptor::kLastReservedNumber) 这些标签号是为protobuf内部实现所保留的,如果你在.proto文件内使用了这些标签号Protobuf编译器将会报错!

2.3 字段标示符

字段标示符有三个:

message的每一个字段,都要用如下的三个修饰符(modifier)来声明:

1.required:必须赋值,不能为空,否则该条message会被认为是“uninitialized”。build一个 “uninitialized” message会抛出一个RuntimeException异常,解析一条“uninitialized” message会抛出一条IOException异常。除此之外,“required”字段跟“optional”字段并无差别。

2.optional:字段可以赋值,也可以不赋值。假如没有赋值的话,会被赋上默认值。对于简单类型,默认值可以自己设定,例如上例的 PhoneNumber中的PhoneType字段。如果没有自行设定,会被赋上一个系统默认值,数字类型会被赋为0,String类型会被赋为空字符 串,bool类型会被赋为false。对于内置的message,默认值为该message的默认实例或者原型,即其内所有字段均为设置。当获取没有显式 设置值的optional字段的值时,就会返回该字段的默认值。

3.repeated:该字段可以重复任意次数,包括0次。重复数据的顺序将会保存在protocol buffer中,将这个字段想象成一个可以自动设置size的数组就可以了。

由于一些历史原因,数字类型的repeated字段性能有些不尽人意,但是,PB已经做了改进,但是需要再添加一点改动,即在声明后添加[packed=true]例如:

repeated int32 samples = 4 [packed=true];

• 1

注意:应该格外小心定义Required字段。当因为某原因要把Required字段改为 Optional字段是,会有问题,老版本读取器会认为消息中没有该字段不完整,可能会拒绝或者丢弃该字段(Google文档是这么说的,但是我试了一 下,将required的改为optional的,再用原来required时候的解析代码去读,如果字段赋值的话,并不会出错,但是如果字段未赋值,会 报这样错误

Exception

in

thread

“main”

com.google.protobuf.InvalidProtocolBufferException: Message missing required fields:fieldname)。在设计时,尽量将这种验证放在应用程序端的完成。Google的一些工程师对此也很困惑,他们觉 得,required类型坏处大于好处,应该尽量仅适用optional或者repeated的。但也并不是所有的人都这么想。

2.4 同一.proto文件定义多个message

PB支持同一.proto文件定义多个message。这在需要定义相关message的时候非常有用,例如:除了搜索请求message,还需要定义搜索响应message,可以再同一.proto文件中定义:

多个消息类型可以定义在同一个.proto文件内,这对定义多个有关联的消息是是十分有用的。例如,如果你想定义一个用于回复SearchResponse消息,你可以像这样在.proto内添加。

message SearchRequest {

required string query = 1;

optional int32 page_number = 2;

optional int32 result_per_page = 3;

}

message SearchResponse {

...

}

• 1

• 2

• 3

• 4

• 5

• 6

• 7

• 8

• 9

2.5.添加注释

添加注释的方式和C/C++是一样的。使用//

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.

}

• 1

• 2

• 3

• 4

• 5

2.6 编译.proto文件后产生了什么?

当你使用protobuf编译器编译一个.proto文件,它会生成在.proto内你描述的消息类型的操作代码,这些代码是根据你所选择的编程功能语言决定的。这些操作代码内包含了设置字段值 和读取字段值,以及序列化到输出流 和 从输入流反序列化。

C++:编译器会按照每个.proto文件生成与其对应的.h和.cc文件,每个消息类似都有独立的消息操作类。

Java:编译器将会生成一个.java文件和一个操作类,此操作类为所有消息类型所共有, 使用一个特别的Builder类为每个消息类型实例化.

Python:有一点不同 – 编译器会为每个消息生成一个模块每个模块有一个静态描述符, 该模块与一个元类在运行时创建一个需要的数据操作类。

从你所选择语言的例程,你可以找到更多关于API的内容, 需要关于API的详细信

息, 参考: API reference.

3、标量值类型

一个消息的字段如果要使用标量可使之为以下类型 –这个表格显示了在.proto文件内可以指定的类型, 与自动生成的相对类型!

你可以在Protocol Buffer Encoding.找到更多关于.这些类型如何编码,如何序列化定义消息的信息!

[1] 在Java中, 无符号32位和64位整数与其有符号相对应, 最高位用来保存符号!

[2] 在所有情况下, 设置某个字段的值将会执行类型检查确保其值是合法的!

[3] 64位或32位无符号整数在解码中会以long来解码, 给字段赋值的时候可以是int.但是在所有情况下,赋值的时候会转变为其目标类型 . 详见 [2].

[4] Python的字符串在解码时候会以unicode来描述,但是同样的可以给其赋值为ascii字符串 (此乃弦外之音).

4、Optional 字段与其默认值

如上所述,在描述一个消息的时候可以用optional指定字段约束,一个消息可以包含也可以不包含optional元素。当一个消息被解析,如果其没有一个optional字段,被解析的消息对象就会将其相对的字段设置为其字段的默认值。这个默认值可以在描述消息的时候被指定。例如。比如你想设置SearchRequest的 result_per_page的默认值为10.

optional int32 result_per_page = 3 [default = 10];

• 1

如果一个optional字段没有被指定其默认值。其默认值被自动替换为:

1.字符串:为空字符串.

2.bool:为false.

3.数字类型:为0;

4.枚举值:为第一个枚举值

5. 枚举类型

当你定义消息格式的时候, 也许你希望其中的一个字段的的值为一个预定义的值类表中的一个. 比方说, 在SearchRequest消息中你想定义一个 corpus 字段, corpus字段的值可以为:” UNIVERSAL, WEB, IMAGES, LOCAL, NEWS, PRODUCTS 或者 VIDEO”. 你可以非常简单的给你的消息添加一个枚举类型 - 一个枚举字段类型其值指定被指定为一个常量的集合 (如果你尝试赋值一个不一样的值, 解析器将会认为这个字段为未知字段). 在下面的例子中 我们给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];

}

• 1

• 2

• 3

• 4

• 5

• 6

• 7

• 8

• 9

• 10

• 11

• 12

• 13

• 14

• 15

还可以给枚举值设置别名,仅需将相同的数字标签设置给不同的名称即可。这里,必须得设置allow_alias为true,否则PB编译器会报错。

enum EnumAllowingAlias {

option allow_alias = true;

UNKNOWN = 0;

STARTED = 1;

RUNNING = 1;

}

enum EnumNotAllowingAlias {

UNKNOWN = 0;

STARTED = 1;

// RUNNING = 1; //不注释这行的话会引发一个错误异常

• 1

• 2

• 3

• 4

• 5

• 6

• 7

• 8

• 9

• 10

枚举值的范围必须在32位整数之内.枚举值的编码使用可变长度的整数,负数会非常低效所以,不推荐使用。你可以在一个消息内部定义一个枚举类型,比如上面的例子。或者也可以在消息的外部定义。这些枚举类型是可以在.proto文件内中重用的,你可以在消息内定义个枚举类型。然后在不同的消息类型中使用它!可以使用 MessageType.EnumType来访问。当你运行编译器编译.proto文件中的枚举类型时,生成的代码会有一个相对应的枚举值(JAVA 或者C++),或者有一个特别的EnumDescriptor类(python)用于在运行时生成一个符号常量集合。

更多关于枚举类型的信息查询 generated code guide 选择你使用的语言。

6. 使用其他Message类型作为filed类型

PB允许使用message类型作为filed类型。例如,在搜索相应message中,包含一个结果message。此时,只需要定义一个结果 message,然后再.proto文件中,在搜索结果message中新增一个字段,该字段的类型设置为结果message即可。如下:

message SearchResponse {

repeated Result result = 1;

}

message Result {

required string url = 1;

optional string title = 2;

repeated string snippets = 3;

}

• 1

• 2

• 3

• 4

• 5

• 6

• 7

• 8

• 9

6.1 导入定义

在上例中,Result message类型与SearchResponse 定义在同一个文件中,假如有这么一种情况,这里所要使用的Resultmessage已经在其他的.proto文件中定义了呢?

可以通过导入其他.proto文件来使用其内的定义。为达此目的,需要在现.proto文件前增加一条import语句:

import \"myproject/other_protos.proto\";

• 1

7. 嵌套类型

PB支持message内嵌套message,如下例子中,Result message 定义在了SearchResponse内:

message SearchResponse {

message Result {

required string url = 1;

optional string title = 2;

repeated string snippets = 3;

}

repeated Result result = 1;

}

• 1

• 2

• 3

• 4

• 5

• 6

• 7

• 8

如果想要在父Message外复用该message的话,可以用Parent.Type格式来引用。

message SomeOtherMessage {

optional SearchResponse.Result result = 1;

}

• 1

• 2

• 3

PB支持无限深层次的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;

}

}

}

• 1

• 2

• 3

• 4

• 5

• 6

• 7

• 8

• 9

• 10

• 11

• 12

• 13

• 14

8. 更新Message类型

如果现有message类型不能在满足业务需求,例如,需要新增一个字段,但是我们却希望依然能够使用原来的.proto生成的代码。完全没有问题,仅需记住如下规则:

1.千万不要修改现有字段后边的数值标签

2.只能新增optional或者repeated字段

3.可以删除非必须字段,但是他们的数字标签不能再被使用。最好的方法是不删除,而是修改名字,比如在前缀上加OBSOLETE_,这样就可以避免后人尽量少的出错。

4.非required字段可以转化成extension字段,反之亦然,同时保留原类型和数字标签

5.int32, uint32, int64, uint64, 和bool是兼容的。即这些字段可以相互切换,在代码处理的时候,不会出错,但是小心范围小的数据接收范围大的数据会发生截断

6.sint32, sint64是相互兼容的,但是不与其他整型类型兼容

7.string和bytes是兼容的,因为bytes也是合法的UTF-8

8.Embedded messages are compatible with bytes if the bytes contain an encoded version of the message(不知道怎么翻译了)

9.fixed32与 sfixed32兼容, fixed64 与sfixed64兼容

10.optional与repeated兼容,也存在数据截断,假如讲一个repeated的序列化后的数据作为输入给客户端,客户端会截取最后一个原子类型的字节。或者,如果是一个message类型的字段的话,合并所有的元素。

11.可以修改字段默认值

9. Package

PB建议在.proto文件开头添加一个package说明符来避免不同message类型的名字冲突:

package foo.bar;

message Open { ... }

• 1

• 2

• 3

这样,就可以使用该package标示符来定义该message类型的字段:

message Foo {

...

required foo.bar.Open open = 1;

...

}

• 1

• 2

• 3

• 4

• 5

不同语言,因为添加package标示符,生成的代码也会有所不同,Java中,该package将会被用作java文件的package。如果不想这样的话,也可在.proto文件中显式指明package,该字段是:java_package。

参考:http://www.cnblogs.com/shitouer/archive/2013/04/12/protocol-

buffers-language-guide.html

因篇幅问题不能全部显示,请点此查看更多更全内容