1.简介
ProtocolBuffer(PB) 是一种轻便高效的结构化数据存储格式,可以用于结构化数据的序列化。
类似xml和json,但PB比前两者更高效和省空间,在移动开发中更为用户省流量。
PB如何做到更省流量的? 这得从它的编码方式来看,PB采用Zigzag 编码并充分利用Varint技术,从而实现二级制级的空间节省。
编译protobuf:protoc.exe -I .\protobuf --cpp_out .\protobuf .\protobuf\serviceInteraction.proto
文件位置 输出文件位置 文件名
protoc addressbook.proto --cpp_out ./
message PhoneNumber {
required string number = 1;
optional PhoneType type = 2 [default = HOME];是默认值
}
repeated PhoneNumber phone = 4; repeated 是重复值的意思。
2.Protobuf定义
消息由至少一个字段组合而成,类似于C语言中的结构。每个字段都有一定的格式。
字段格式:
限定修饰符① | 数据类型② | 字段名称③ | = | 字段编码值④ | [字段默认值⑤]
①.限定修饰符包含 required\optional\repeated
Required: 表示是一个必须字段,1个元素,在发送消息之前必须设置该字段的值,对于接收方,必须能够识别该字段的意思。发送之前没有设置required字段或者无法识别required字段都会引发编解码异常,导致消息被丢弃。
Optional:表示是一个可选字段,0-1个元素,在发送消息时,可以有选择性的设置或者不设置该字段的值。对于接收方,如果能够识别可选字段就进行相应的处理,如果无法识别,则忽略该字段,消息中的其它字段正常处理。---因为optional字段的特性,很多接口在升级版本中都把后来添加的字段都统一的设置为optional字段,这样老的版本无需升级程序也可以正常的与新的软件进行通信,只不过新的字段无法识别而已,因为并不是每个节点都需要新的功能,因此可以做到按需升级和平滑过渡。
Repeated:表示该字段可以包含0~N个元素。其特性和optional一样,但是每一次可以包含多个值。可以看作是在传递一个数组的值。
②.数据类型
Protobuf定义了一套基本数据类型。几乎都可以映射到C++\Java等语言的基础数据类型.
N值 除了bool类型打包的字节为1个字节外,其他类型打包的字节并不是固定。而是根据数据的大小或者长度确定。
例如int32,如果数值比较小,在0~127时,使用一个字节打包。
关于枚举的打包方式和uint32相同。
关于message,类似于C语言中的结构包含另外一个结构作为数据成员一样。
关于 fixed32 和int32的区别。fixed32的打包效率比int32的效率高,但是使用的空间一般比int32多。因此一个属于时间效率高,一个属于空间效率高。根据项目的实际情况,一般选择fixed32,如果遇到对传输数据量要求比较苛刻的环境,可以选择int32.
③.字段名称
字段名称的命名与C、C++、Java等语言的变量命名方式几乎是相同的。
protobuf建议字段的命名采用以下划线分割的驼峰式。例如 first_name 而不是firstName.
④.字段编码值
有了该值,通信双方才能互相识别对方的字段。当然相同的编码值,其限定修饰符和数据类型必须相同。
编码值的取值范围为 1~2^32(4294967296)。
其中 1~15的编码时间和空间效率都是最高的,编码值越大,其编码的时间和空间效率就越低(相对于1-15),当然一般情况下相邻的2个值编码效率的是相同的,除非2个值恰好实在4字节,12字节,20字节等的临界区。比如15和16.
1900~2000编码值为Google protobuf 系统内部保留值,建议不要在自己的项目中使用。
protobuf 还建议把经常要传递的值把其字段编码设置为1-15之间的值。
消息中的字段的编码值无需连续,只要是合法的,并且不能在同一个消息中有字段包含相同的编码值。
建议:项目投入运营以后涉及到版本升级时的新增消息字段全部使用optional或者repeated,尽量不实用required。如果使用了required,需要全网统一升级,如果使用optional或者repeated可以平滑升级。
⑤.默认值。当在传递数据时,对于required数据类型,如果用户没有设置值,则使用默认值传递到对端。当接受数据是,对于optional字段,如果没有接收到optional字段,则设置为默认值。
关于import
protobuf 接口文件可以像C语言的h文件一个,分离为多个,在需要的时候通过 import导入需要对文件。其行为和C语言的#include或者java的import的行为大致相同。
关于package
避免名称冲突,可以给每个文件指定一个package名称,对于java解析为java中的包。对于C++则解析为名称空间。
关于message
支持嵌套消息,消息可以包含另一个消息作为其字段。也可以在消息内定义一个新的消息。
关于enum
枚举的定义和C++相同,但是有一些限制。
枚举值必须大于等于0的整数。
使用分号(;)分隔枚举变量而不是C++语言中的逗号(,)
3.1 消息与字段名
使用骆驼风格的大小写命名,即单词首字母大写,来做消息名。使用GNU的全部小写,使用下划线分隔的方式定义字段名:
message SongServerRequest {
required string song_name=1;
}
使用这种命名方式得到的名字如下:
C++:
const string& song_name() {...}
void set_song_name(const string& x) {...}
Java:
public String getSongName() {...}
public Builder setSongName(String v) {...}
3.2 枚举
使用骆驼风格做枚举名,而用全部大写做值的名字:
enum Foo {
FIRST_VALUE=1;
SECOND_VALUE=2;
}
3.3 服务
如果你的 .proto 文件定义了RPC服务,你可以使用骆驼风格:
service FooService {
rpc GetSomething(FooRequest) returns (FooResponse);
}
4. ProtocolBuffer基础:Python
本指南给Python程序员一个快速使用的ProtocolBuffer的指导。通过一些简单的例子来在应用中使用ProtocolBuffer,它向你展示了如何:
· 定义 .proto 消息格式文件
· 使用ProtocolBuffer编译器
· 使用Python的ProtocolBuffer编程接口来读写消息
这并不是一个在Python中使用ProtocolBuffer的完整指导。更多细节请参考手册信息,查看语言指导( http://code.google.com/apis/protocolbuffers/docs/proto.html ),Python API( http://code.google.com/apis/protocolbuffers/docs/reference/python/index.html ),和编码手册( http://code.google.com/apis/protocolbuffers/docs/encoding.html )。
4.1 为什么使用ProtocolBuffer?
下面的例子”地址本”应用用于读写人的联系信息。每个人有name、ID、email,和联系人电话号码。
如何串行化和读取结构化数据呢?有如下几种问题:
· 使用Python的pickle,这是语言内置的缺省方法,不过没法演化,也无法让其他语言支持。
· 你可以发明一种数据编码方法,例如4个整数”12:3-23:67″,这是简单而灵活的方法,不过你需要自己写解析器代码,且只适用于简单的数据。
· 串行化数据到XML。这种方法因为可读性和多种语言的兼容函数库而显得比较吸引人,不过这也不是最好的方法,因为XML浪费空间是臭名昭著的,编码解码也很浪费时间。而XML DOM树也是很复杂的。
ProtocolBuffer提供了灵活、高效、自动化的方法来解决这些问题。通过ProtocolBuffer,只需要写一个 .proto 数据结构描述文件,就可以编译到几种语言的自动编码解码类。生成的类提供了setter和getter方法来控制读写细节。最重要的是 ProtocolBuffer支持后期扩展协议,而又确保旧格式可以兼容。
4.2 哪里可以找到例子代码
源码发行包中已经包含了,在”example”文件夹。
4.3 定义你的协议格式
想要创建你的地址本应用,需要开始于一个 .proto 文件。定义一个 .proto 文件很简单:添加一个消息到数据结构,然后指定一个和一个类型到每一个字段,如下是本次例子使用的 addressbook.proto
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 phone=4;
}
message AddressBook {
repeated Person person=1;
}
4.4 编译你的ProtocolBuffer
现在已经拥有了 .proto 文件,下一步就是编译生成相关的访问类。运行编译器 protoc 编译你的 .proto 文件。
1. 如果还没安装编译器则下载并按照README的安装。
2. 运行编译器,指定源目录和目标目录,定位你的 .proto 文件到源目录,然后执行:
protoc -I=$SRC_DIR --python_out=$DST_DIR addressbook.proto
因为需要使用Python类,所以 –python_out 选项指定了特定的输出语言。
这个步骤会生成 addressbook_pb2.py 到目标目录。
4.5 ProtocolBuffer API
不像生成的C++和Java代码,Python生成的类并不会直接为你生成存取数据的代码。而是(有如你在 addressbook_pb2.py 中见到的)生成消息描述、枚举、和字段,还有一些神秘的空类,每个对应一个消息类型:
class Person(message.Message):
__metaclass__=reflection.GeneratedProtocolMessageType
class PhoneNumber(message.Message):
__metaclass__=reflection.GeneratedProtocolMessageType
DESCRIPTION=_PERSON_PHONENUMBER
DESCRIPTOR=_PERSON
class AddressBook(message.Message):
__metaclass__=reflection.GeneratedProtocolMessageType
DESCRIPTOR=_ADDRESSBOOK
这里每个类最重要的一行是 __metaclass__=reflection.GeneratedProtocolMessageType 。通过Python的元类机制工作,你可以把他们看做是生成类的模板。在载入时, GeneratedProtocolMessageType 元类使用特定的描述符创建Python方法。随后你就可以使用完整的功能了。
最后就是你可以使用 Person 类来操作相关字段了。例如你可以写:
import addressbook_pb2
person=addressbook_pb2.Person()
person.id=1234
person.name="John Doe"
person.email="
[email protected]"
phone=person.phone.add()
phone.number="555-4321"
phone.type=addressbook_pb2.Person.HOME
需要注意的是这些赋值属性并不是简单的增加新字段到Python对象,如果你尝试给一个 .proto 文件中没有定义的字段赋值,就会抛出 AttributeError 异常,如果赋值类型错误会抛出 TypeError 。在给一个字段赋值之前读取会返回缺省值:
person.no_such_field=1 #raise AttributeError
person.id="1234" #raise TypeError
更多相关信息参考( http://code.google.com/apis/protocolbuffers/docs/reference/python-generated.html )。
4.5.1 枚举
枚举在元类中定义为一些符号常量对应的数字。例如常量 addressbook_pb2.Person.WORK 拥有值2。
4.5.2 标准消息方法
每个消息类包含一些其他方法允许你检查和控制整个消息,包括:
· IsInitialized() :检查是否所有必须(required)字段都已经被赋值了。
· __str__() :返回人类可读的消息表示,便于调试。
· CopyFrom(other_msg) :使用另外一个消息的值来覆盖本消息。
· Clear() :清除所有元素的值,回到初识状态。
这些方法是通过接口 Message 实现的,更多消息参考( http://code.google.com/apis/protocolbuffers/docs/reference/python/google.protobuf.message.Message-class.html )。
4.5.3 解析与串行化
最后,每个ProtocolBuffer类有些方法用于读写消息的二进制数据( http://code.google.com/apis/protocolbuffers/docs/encoding.html )。包括:
· SerializeToString() :串行化,并返回字符串。注意是二进制格式而非文本。
· ParseFromString(data) :解析数据。
他们是成对使用的,提供二进制数据的串行化和解析。另外参考消息API参考( http://code.google.com/apis/protocolbuffers/docs/reference/python/google.protobuf.message.Message-class.html )了解更多信息。
Note
ProtocolBuffer与面向对象设计
ProtocolBuffer类只是用于存取数据的,类似于C++中的结构体,他们并没有在面向对象方面做很好的设计。如果你想要给这些类添加更多的行为,最好的方法是包装(wrap)。包装同样适合于复用别人写好的 .proto 文件。这种情况下,你可以把ProtocolBuffer生成类包装的很适合于你的应用,并隐藏一些数据和方法,暴露有用的函数等等。 你不可以通过继承来给自动生成的类添加行为。 这会破坏他们的内部工作机制。
4.6 写消息
现在开始尝试使用ProtocolBuffer的类。第一件事是让地址本应用可以记录联系人的细节信息。想要做这些需要先创建联系人实例,然后写入到输出流。
这里的程序从文件读取地址本,添加新的联系人信息,然后写回新的地址本到文件。
#! /usr/bin/python
import addressbook_pb2
import sys
#这个函数使用用户输入填充联系人信息
def PromptForAddress(person):
person.id=int(raw_input("Enter person ID number: "))
person.name=raw_input("Enter name: ")
email=raw_input("Enter email address (blank for none): ")
if email!="":
person.email=email
while True:
number=raw_input("Enter a phone number (or leave blank to finish): ")
if number=="":
break
phone_number=person.phone.add()
phone_number.number=number
type=raw_input("Is this a mobile, home, or work phone? ")
if type=="mobile":
phone_number.type=addressbook_pb2.Person.MOBILE
elif type=="home":
phone_number.type=addressbook_pb2.Person.HOME
elif type=="work":
phone_number.type=addressbook_pb2.Person.WORK
else:
print "Unknown phone type; leaving as default value."
#主函数,从文件读取地址本,添加新的联系人,然后写回到文件
if len(sys.argv)!=2:
print "Usage:",sys.argv[0],"ADDRESS_BOOK_FILE"
sys.exit(-1)
address_book=addressbook_pb2.AddressBook()
#读取已经存在的地址本
try:
f=open(sys.argv[1],"fb")
address_book.ParseFromString(f.read())
f.close()
except OSError:
print sys.argv[1]+": Count open file. Creating a new one."
#添加地址
PromptFromAddress(address_book.person.add())
#写入到文件
f=open(sys.argv[1],"wb")
f.write(address_book.SerializeToString())
f.close()
4.7 读消息
当然,一个无法读取的地址本是没什么用处的,这个例子读取刚才创建的文件并打印所有信息:
#! /usr/bin/python
import addressbook_pb2
import sys
#遍历地址本中所有的人并打印出来
def ListPeople(address_book):
for person in address_book.person:
print "Person ID:",person.id
print " Name:",person.name
if person.HasField("email"):
print " E-mail:",person.email
for phone_number in person.phone:
if phone_number.type==addressbook_pb2.Person.MOBILE:
print " Mobile phone #:",
elif phone_number.type==addressbook_pb2.Person.HOME:
print " Home phone #:",
elif phone_number.type==addressbook_pb2.Person.WORK:
print " Work phone #:",
print phone_number.number
#主函数,从文件读取地址本
if len(sys.argv)!=2:
print "Usage:",sys.argv[0],"ADDRESS_BOOK_FILE"
sys.exit(-1)
address_book=addressbook_pb2.AddressBook()
#读取整个地址本文件
f=open(sys.argv[1],"rb")
address_book.ParseFromString(f.read())
f.close()
ListPeople(address_book)
4.8 扩展ProtocolBuffer
在你发不了代码以后,可能会想要改进ProtocolBuffer的定义。如果你想新的数据结构向后兼容,而你的旧数据可以向前兼容,那么你就找对了东西了,不过有些规则需要遵守。在新版本的ProtocolBuffer中:
· 必须不可以改变已经存在的标签的数字。
· 必须不可以增加或删除必须(required)字段。
· 可以删除可选(optional)或重复(repeated)字段。
· 可以添加新的可选或重复字段,但是必须使用新的标签数字,必须是之前的字段所没有用过的。
这些规则也有例外( http://code.google.com/apis/protocolbuffers/docs/proto.html#updating ),不过很少使用。
如果你遵从这些规则,旧代码会很容易的读取新的消息,并简单的忽略新的字段。而对旧的被删除的可选字段也会简单的使用他们的缺省值,被删除的重复字段会自动为空。新的代码也会透明的读取旧的消息。然而,需要注意的是新的可选消息不会在旧的消息中显示,所以你需要使用 has_ 严格的检查他们是否存在,或者在 .proto 文件中提供一个缺省值。如果没有缺省值,就会有一个类型相关的默认缺省值:对于字符串就是空字符串;对于布尔型则是false;对于数字类型默认为0。同时要注意的是如果你添加了新的重复字段,你的新代码不会告诉你这个字段为空(新代码)也不会,也不会(旧代码)包含 has_ 标志。
4.9 高级使用
ProtocolBuffer不仅仅提供了数据结构的存取和串行化。查看Python API参考( http://code.google.com/apis/protocolbuffers/docs/reference/python/index.html )了解更多功能。
一个核心功能是通过消息类的映射(reflection)提供的。你可以通过它遍历消息的所有字段,和管理他们的值。关于映射的一个很有用的地方是转换到其他编码,如XML或JSON。一个使用映射的更高级的功能是寻找同类型两个消息的差异,或者开发出排序、正则表达式等功能。使用你的创造力,还可以用ProtocolBuffer实现比你以前想象的更多的问题。
映射是通过消息接口提供的。
5 Python代码生成
本页提供了Python生成类的相关细节。你可以在阅读本文档之前查看语言指导。
Python的ProtocolBuffer实现与C++和Java的略有不同,编译器只输出构建代码的描述符来生成类,而由Python的元类来执行工作。本文档描述了元类开始生效以后的东西。
5.1 编译器的使用
ProtocolBuffer通过编译器的 –python_out= 选项来生成Python的相关类。这个参数实际上是指定输出的Python类放在哪个目录下。编译器会为每个 .proto 文件生成一个对应的 .py 文件。输出文件名与输入文件名相关,不过有两处修改:
· 扩展名 .proto 改为 .py 。
· 路径名的修改。
如果你按照如下调用编译器:
protoc --proto_path=src --python_out=build/gen src/foo.proto src/bar/baz.proto
编译器会自动读取两个 .proto 文件然后产生两个输出文件。在需要时编译器会自动创建目录,不过 –python_out 指定的目录不会自动创建。
需要注意的是,如果 .proto 文件名或路径包含有无法在Python中使用的模块名(如连字符),就会被自动转换为下划线。所以文件 foo-bar.proto 会变成 foo_bar_pb2.py 。
Note
在每个文件后缀的 _pb2.py 中的2代表ProtocolBuffer版本2。版本1仅在Google内部使用,但是你仍然可以在以前发布的一些代码中找到它。自动版本2开始,ProtocolBuffer开始使用完全不同的接口了,从此Python也没有编译时类型检查了,我们加上这个版本号来标志Python文件名。
5.2 包
Python代码生成根本不在乎包的名字。因为Python使用目录名来做包名。
5.3 消息
先看看一个简单的消息声明:
message Foo {}
ProtocolBuffer编译器会生成类Foo,它是 google.protobuf.Message 的子类。这个实体类,不含有虚拟方法。不像C++和Java,Python生成类对优化选项不感冒;实际上Python的生成代码已经为代码大小做了优化。
你不能继承Foo的子类。生成类被设计不可以被继承,否则会被打破一些设计。另外,继承本类也是不好的设计。
Python的消息类没有特定的公共成员,而是定义接口,极其嵌套的字段、消息和枚举类型。
一个消息可以在另外一个消息中声明,例如 message Foo { message Bar {}} 。在这种情况下,Bar类定义为Foo的一个静态成员,所以你可以通过 Foo.Bar 来引用。
5.4 字段
对于消息类型中的每一个字段,都有对应的同名成员。
5.4.1 简单字段
如果你有一个简单字段(包括可选的和重复的),也就是非消息字段,你可以通过简单字段的方式来管理,例如foo字段的类型是int32,你可以:
message.foo=123
print message.foo
注意设置foo的值,如果类型错误会抛出TypeError。
如果foo在赋值之前就读取,就会使用缺省值。想要检查是否已经赋值,可以用 HasField() ,而清除该字段的值用 ClearField() 。例如:
assert not message.HasField("foo")
message.foo=123
assert message.HasField("foo")
message.ClearField("foo")
assert not message.HasField("foo")
5.4.2 简单消息字段
消息类型工作方式略有不同。你无法为一个嵌入消息字段赋值。而是直接操作这个消息的成员。因为实例化上层消息时,其包含的子消息同时也实例化了,例如定义:
message Foo {
optional Bar bar=1;
}
message bar {
optional int32 i=1;
}
你不可以这么做,因为不能做消息类型字段的赋值:
foo=Foo()
foo.bar=Bar() #WRONG!
而是可以直接对消息类型字段的成员赋值:
foo=Foo()
assert not foo.HasField("bar")
foo.bar.i=1
assert foo.HasField("bar")
注意简单的读取消息类型字段的未赋值成员只不过是打印其缺省值:
foo=Foo()
assert not foo.HasField("bar")
print foo.bar.i #打印i的缺省值
assert not foo.HasField("bar")
5.4.3 重复字段
重复字段表现的像是Python的序列类型。如果是嵌入的消息,你无法为字段直接赋值,但是你可以管理。例如给定的定义:
message Foo {
repeated int32 nums=1;
}
你就可以这么做:
foo=Foo()
foo.nums.append(15)
foo.nums.append(32)
assert len(foo.nums)==2
assert foo.nums[0]==15
assert foo.nums[1]==32
for i in foo.nums:
print i
foo.nums[1]=56
assert foo.nums[1]==56
作为一种简单字段,清除该字段必须使用 ClearField() 。
5.4.4 重复消息字段
重复消息字段工作方式与重复字段很像,除了 add() 方法用于返回新的对象以外。例如如下定义:
message Foo {
repeated Bar bar=1;
}
message Bar {
optional int32 i=1;
}
你可以这么做:
foo=Foo()
bar=foo.bars.add()
bar.i=15
bar=foo.bars.add()
bar.i=32
assert len(foo.bars)==2
assert foo.bars[0].i==15
assert foo.bars[1].i==32
for bar in foo.bars:
print bar.i
foo.bars[1].i=56
assert foo.bars[1].i==56
5.5 服务
5.5.1 接口
一个简单的接口定义:
service Foo {
rpc Bar(FooRequest) returns(FooResponse);
}
ProtocolBuffer的编译器会生成类 Foo 来展示这个服务。 Foo 将会拥有每个服务定义的方法。在这种情况下 Bar 方法的定义是:
def Bar(self,rpc_controller,request,done)
参数等效于 Service.CallMethod() ,除了隐含的 method_descriptor 参数。
这些生成的方法被定义为可以被子类重载。缺省实现只是简单的调用 controller.SetFailed() 而抛出错误信息告之尚未实现。然后调用done回调。在实现你自己的服务时,你必须继承生成类,然后重载各个接口方法。
Foo继承了 Service 接口。ProtocolBuffer编译器会自动声响相关的实现方法:
· GetDescriptor :返回服务的 ServiceDescriptor 。
· CallMethod :检测需要调用哪个方法,并且直接调用。
· GetRequestClass 和 GetResponseClass :返回指定方法的请求和响应类。
5.5.2 存根(Stub)
ProtocolBuffer编译器也会为每个服务接口提供一个存根实现,用于客户端发送请求到服务器。对于Foo服务,存根实现是 Foo_Stub 。
Foo_Stub 是Foo的子类,他的构造器是一个 RpcChannel 。存根会实现调用每个服务方法的 CallMethod() 。
ProtocolBuffer哭并不包含RPC实现。然而,它包含了你构造服务类的所有工具,不过选择RPC实现则随你喜欢。你只需要提供 RpcChannel 和 RpcController 的实现即可。