|
定义
protocol buffers 是一种语言无关、平台无关、可扩展的序列化结构数据的方法,它可用于(数据)通信协议、数据存储等。通信时所传递的信息是通过Protobuf定义的message数据结构进行打包,然后编译成二进制的码流再进行传输或者存储。
Protocol buffers 是一种灵活,高效,自动化机制的结构数据序列化方法-可类比 XML,但是比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更为简单。
Protobuf 使用的时候必须写一个 IDL(Interface description language)文件,在里面定义好数据结构,只有预先定义了的数据结构,才能被序列化和反序列化。其中,序列化是将对象转换二进制数据,反序列化是将二进制数据转换成对象。
编码
ProtoBuf 编码格式类似于TLV 格式(Tag | Length | Value),Tag为字段唯一标识,Length为Value域的长度,Value为数据本身。其中,Tag由field_number和wire_type两个部分组成,field_number是message定义字段时指定的字段编号,wire_type是ProtoBuf 编码类型,根据这个类型选择不同的 Value 编码方案。wire_type最多可表达8种编码类型(因为是3bit的),目前已经定义了六种(Varint,64-bit,Length-delimited,Start group,End group,32-bit,其中Start group和End group已经弃用)。
在ProtoBuf 中,Length是可选的,即有些类型的数据结构编码后格式是(Tag | Value),如Varint,64-bit和32-bit。为确定这种格式的数据边界引入了Varint编码。
使用过程
- 第一步,创建 .proto 文件,定义数据结构。
- 第二步,protoc 编译 .proto 文件生成读写接口
- 第三步,调用接口实现序列化、反序列化以及读写
python应用(proto3)
当我们在一个.proto运行该protocol buffers编译器时,编译器会用我们选择的语言生成代码,包括获取和设置字段值,输出流序列化消息,以及从输入流解析消息。其中,Python编译器会在.proto中生成一个带有每种消息类型的静态描述符的模块,然后与元类一起使用,在运行时创建必要的Python数据访问类。
有proto2和proto3两个语法版本,在此以proto3为例,汉化google官方文档Language Guide(proto3)来介绍,官网链接为
以及
定义消息类型
首先让我们看一个非常简单的例子。假设我们想要定义一个搜索请求消息格式,其中每个搜索请求都有一个查询字符串、我们感兴趣的特定结果页面以及每页的结果。下面是用来定义消息类型的.proto文件。
文件的第一行指定我们使用的是proto3语法(如果你不这样做,protocol buffers编译器会默认使用的是proto2)。SearchRequest消息定义指定了三个字段(名称-值对),每个字段对应希望包含在这种类型的消息中的数据。每个字段都有一个名称和一个类型。
syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
在上面的示例中,所有字段都是标量类型:两个整数(page_number和result_per_page)和一个字符串(query)。但是,我们也可以为字段指定复合类型,包括枚举和其他消息类型。
消息字段可以是以下内容之一:
- 单数:格式良好的消息可以有0个或1个此字段(但不能多于1个)。这是proto3语法的默认字段规则。
- repeat:该字段可以在格式良好的消息中重复任何次数(包括零)。重复值的顺序将被保留。
在proto3中,标量数值类型的重复字段默认使用打包编码。
可以在一个.proto文件中定义多个消息类型。这在你定义多个相关的消息时很有用——例如,如果你想定义与你的SearchResponse消息类型相对应的应答消息格式,你可以将它添加到相同的.proto中:
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
message SearchResponse {
...
}
要在.proto文件中添加注释,请使用C/ C++风格的//和/… /语法。
/* SearchRequest represents a search query, with pagination options to
* indicate which results to include in the response. */
message SearchRequest {
string query = 1;
int32 page_number = 2; // Which page number do we want?
int32 result_per_page = 3; // Number of results to return per page.
}
一条消息可以在另一条消息中声明。例如:message Foo { message Bar { } }
在本例中,Bar类被声明为Foo的静态成员,因此可以将其引用为Foo.Bar。
对于任何消息,可以调用Pack()将指定的消息打包到当前的Any消息中,或者调用Unpack()将当前的Any消息解打包到指定的消息中。例如:
any_message.Pack(message)
any_message.Unpack(message)Unpack()还检查传入的消息对象的描述符与存储的描述符,如果不匹配且不尝试任何解包,则返回False否则返回True。 另外,也可以调用Is()方法来检查Any消息是否代表给定的协议缓冲区类型。例如:
assert any_message.Is(message.DESCRIPTOR)
时间戳消息可以通过ToJsonString()/FromJsonString()方法转换为/转换自RFC 3339日期字符串格式(JSON字符串)。例如:
timestamp_message.FromJsonString("1970-01-01T00:00:00Z")
assert timestamp_message.ToJsonString() == "1970-01-01T00:00:00Z"也可以调用GetCurrentTime()来用当前时间填充Timestamp消息:
timestamp_message.GetCurrentTime()要转换自epoch以来的其他时间单位,你可以调用ToNanoseconds(), FromNanoseconds(), tommicroseconds (), FromMicroseconds(), tomillseconds (), FromMilliseconds(), ToSeconds()或FromSeconds()。生成的代码也有ToDatetime()和FromDatetime()方法来在Python datetime对象和时间戳之间进行转换。
Duration消息具有与Timestamp相同的方法,可以在JSON字符串和其他时间单位之间进行转换。要在timedelta和Duration之间转换,可以调用ToTimedelta()或FromTimedelta。
Struct消息允许直接获取和设置item,例如:
struct_message["key1"] = 5
struct_message["key2"] = "abc"
struct_message["key3"] = True要获取或创建一个列表/结构,可以调用get_or_create_list()/get_or_create_struct()。例如:
struct.get_or_create_struct("key4")["subkey"] = 11.0
struct.get_or_create_list("key5")
一个ListValue消息就像一个Python序列,可以让你做以下事情:
list_value = struct_message.get_or_create_list("key")
list_value.extend([6, "seven", True, None])
list_value.append(False)
assert len(list_value) == 5
assert list_value[0] == 6
assert list_value[1] == "seven"
assert list_value[2] == True
assert list_value[3] == None
assert list_Value[4] == False要添加ListValue/Struct,调用add_list()/add_struct()。例如:
list_value.add_struct()["key"] = 1
list_value.add_list().extend([1, "two", True])
给出一个消息定义:
message MyMessage {
map<int32, int32> mapfield = 1;
}
Repeated字段表示为一个类似于Python序列的对象。与embedded消息一样不能直接分配字段,但可以操作它。例如,给定下面的消息定义:
message Foo {
repeated int32 nums = 1;
}可以进行如下操作:
foo = Foo()
foo.nums.append(15) # Appends one value
foo.nums.extend([32, 47]) # Appends an entire list
assert len(foo.nums) == 3
assert foo.nums[0] == 15
assert foo.nums[1] == 32
assert foo.nums == [15, 32, 47]
foo.nums[:] = [33, 48] # Assigns an entire list
assert foo.nums == [33, 48]
foo.nums[1] = 56 # Reassigns a value
assert foo.nums[1] == 56
for i in foo.nums: # Loops and print
print(i)
del foo.nums[:] # Clears list (works just like in a Python list)
在解析消息时,如果已编码的消息不包含特定的奇异元素,则解析对象中相应的字段将被设置为该字段的默认值。这些默认值是特定类型的:
- 对于字符串,默认值是空字符串。
- 对于bytes,默认值为空bytes。
- 对于bool类型,默认值为false。
- 对于数值类型,默认值为零。
- 对于枚举,默认值是第一个定义的枚举值,它必须为0。
Python中,枚举只是整数。一组整型常量被定义为对应于枚举的定义值。例如:
message Foo {
enum SomeEnum {
VALUE_A = 0;
VALUE_B = 5;
VALUE_C = 1234;
}
optional SomeEnum bar = 1;
}常量VALUE_A、VALUE_B和VALUE_C分别用值0、5和1234定义。如果需要,可以访问SomeEnum。如果enum是在外部作用域中定义的,则其值为模块常量;如果它是在消息中定义的(像上面那样),它们将成为该消息类的静态成员。
例如,你可以通过以下三种方式来访问proto中以下enum的值:
value_a = myproto_pb2.SomeEnum.VALUE_A
# or
myproto_pb2.VALUE_A
# or
myproto_pb2.SomeEnum.Value(&#39;VALUE_A&#39;)enum类型的工作方式与标量类型类似:
foo = Foo()
foo.bar = Foo.VALUE_A
assert foo.bar == 0
assert foo.bar == Foo.VALUE_A如果枚举的名称(或枚举值)是一个Python关键字,那么它的对象(或枚举值的属性)将只能通过getattr()访问。
如果通过完全删除枚举条目或将其注释掉来更新enum类型,以后的用户可以在更新类型时重用该数值。如果他们后来加载相同的.proto的旧版本,这可能会导致严重的问题,包括数据损坏、隐私错误等。确保不会发生这种情况的一种方法是指定删除条目的数值(和/或名称,这也会导致JSON序列化的问题)被reserved。如果将来有用户试图使用这些标识符,协议缓冲区编译器将会报错。此时可以使用max关键字指定保留的数字值范围到可能的最大值。
enum Foo {
reserved 2, 15, 9 to 11, 40 to max;
reserved &#34;FOO&#34;, &#34;BAR&#34;;
}
给定一个带有Oneof的消息:
message Foo {
oneof test_oneof {
string name = 1;
int32 serial_number = 2;
}
}与Foo相对应的Python类将有名为name和serial_number的常数字段。但是,与常数字段不同的是,一个中的最多一次可以设置一个字段,这是由运行时保证的。例如:
message = Foo()
message.name = &#34;Bender&#34;
assert message.HasField(&#34;name&#34;)
message.serial_number = 2716057
assert message.HasField(&#34;serial_number&#34;)
assert not message.HasField(&#34;name&#34;)
我们可以使用其他消息类型作为字段类型。例如,假设想要在每个SearchResponse消息中包含Result消息来实现这一点,我们可以在相同的.proto中定义一个Result消息类型,然后在SearchResponse中指定一个Result类型的字段.
message SearchResponse {
repeated Result results = 1;
}
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}导入定义
如果我们想作为字段类型使用的消息类型已经在另一个.proto文件中定义了,可以通过导入其他.proto文件来使用它们的定义。要导入另一个.proto的定义,需要在文件的顶部添加一个import语句:
import &#34;myproject/other_protos.proto&#34;;默认情况下,只能从直接导入的.proto文件中使用定义。但是,有时可能需要将.proto文件移动到新的位置。与直接移动.proto文件并在一次更改中更新所有调用位置不同,您可以在旧位置放置一个占位符.proto文件,以便使用import public概念将所有导入转发到新位置。
任何导入包含Import公共语句的原型的代码都可以传递地依赖于Import公共依赖项。例如:
// new.proto
// All definitions are moved here
// old.proto
// This is the proto that all clients are importing.
import public &#34;new.proto&#34;;
import &#34;other.proto&#34;;
// client.proto
import &#34;old.proto&#34;;
// You use definitions from old.proto and new.proto, but not other.proto定义服务
如果想在RPC(远程过程调用)系统中使用我们的消息类型,可以在.proto文件中定义RPC服务接口,protocol buffer compiler将用我们选择的语言生成服务接口代码和stubs。所以,如果我们想定义一个RPC服务的方法,它接受你的SearchRequest并返回一个SearchResponse,可以在.proto文件中这样定义它:
service SearchService {
rpc Search(SearchRequest) returns (SearchResponse);
}与协议缓冲区一起使用的最直接的RPC系统是gRPC:在谷歌开发的一种语言和平台无关的开源RPC系统。gRPC与协议缓冲区工作得特别好,它允许你使用一个特殊的协议缓冲区编译器插件直接从你的.proto文件生成相关的RPC代码。
JSON Mapping
Proto3支持JSON中的规范编码,使得在系统之间共享数据更加容易。
如果json编码的数据中缺少一个值,或者它的值为空,那么在解析到协议缓冲区时,它将被解释为适当的默认值。如果一个字段在协议缓冲区中有默认值,它将在json编码的数据中被默认省略,以节省空间。实现可以提供选项,在json编码的输出中发出带有默认值的字段。
编译器调用
当使用--python_out=命令行标志调用时,协议缓冲区编译器生成Python输出。--python_out=选项的参数是希望编译器写入Python输出的目录。编译器为每个.proto文件输入创建一个.py文件。输出文件的名称是通过获取.proto文件的名称并进行两次更改来计算的:
- 扩展名(.proto)被_pb2.py替换。
- proto路径(用--proto_path=或-I命令行标志指定)被替换为输出路径(用--python_out=flag)。
协议编译器的python调用例子如下:
protoc --proto_path=src --python_out=build/gen src/foo.proto src/bar/baz.proto编译器将读取src/foo.proto和src /bar/baz.proto,并生成两个输出文件:build/gen/foo_pb2.py和build/gen/bar/baz_pb2.py。如果不存在build/gen/bar目录,编译器会自动创建,但build或build/gen必须已经存在。如果.proto文件或它的路径包含任何不能在Python模块名称中使用的字符(例如,连字符),它们将被替换为下划线。如,文件foo-bar.proto变成Python文件foo_bar_pb2.py。
本文参考了各位大佬的文章,大家如果想详细了解关于ProtoBuf的相关信息可以看看: |
|