一:如何定义proto讯息格式
二:使用protocol buffer编译器
三:视同protocol buffer读写信息
为什么要使用protocol buffer?
这个例子使用一个非常简单的联络薄,允许读写。每笔资料代表一个人的名字,ID,邮箱地址及电话号码。
要如何把这些资料序列化及读取?我们有以下几个方法:
使用Python Pickle。这是Python预设的方法,可是这个方法不能有效处理Schema改动,而且要与C++及Java程序交换资料很难。
发明一个方法把资料变成字串,举例,4个整数可以变成“12:3”-23:67“。虽然这歌方法不能提供一次性编码及解析代码,而且解析需要一点运行成本,可是这个方法很简单有弹性,适合一些很简单的资料。
把资料序列化为XML,这很吸引因为XML是人类可读的,而且很多不同语言都支持。如果你要把资料分享给其他应用,XML是一个很好的选择。可是,XML空间要求巨大,早就臭名远播,而且编码对性能的负面影响很,XML DOM树明显比简单的读取一个class field来的复杂很多。
Protocol Buffer刚好可以解决这个问题。有了Protocol buffer,我们可以把资料结构写进一个proto描述档案,使用protocol buffer编译器从这个proto档产生class档案并且拥有一个高效率二进制而自动产生出来的编码及解析代码。产生出来的class提供getter及setter,处理读写细节。更重要的是,protocol buffer格式支持随着时间的格式延伸,代码可以读取使用就格式编码的资料。
定义你的协议格式
要建立一个联络薄应用,首先要从proto档案开始,首先加为每个资料结构加入一个message,然后为每个属性建立一个名字跟形态。看以下例子。
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; }
首先,一开始写一个package宣告,用来避免将来有可能出现的名字冲突。在Python里面,packages一般用资料夹结构去决定,所以在这个proto档案里使用package并不会影响产生出来的代码。可是,你还是应该宣告一个package去避免在各个protocol buffer名字空间的名字冲突以及避免Python意外的语言的名字冲突。
然后,进入messsage定义部分。一个message集合了一堆有形态的属性。很多的简单形态都可以使用,包括bool,int32,float,double,string。你也可以使用自定义形态。上面的例子Person就包含了PhoneNumber这个形态。你也可以定义enum形态去限制PhoneNumber的type为MOBILE,HOME,WORK其中之一。
你看到的“=1”,“=2”标记是用来识别独一无二的tag并用于二进制编码里面。tag号码1-15比正常的编码使用省1字节,因此如果要要优化的话我们可以决定把常用或者重复的元素予以1-15的tag,而16以上的tag留给不常见可有可无的元素。每个重复的元素要求把tag号码重新编码,所以重复的属性特别适合使用这个优化方法。
每个属性必须要用一下其中一个方法去描述一下:
required:必须提供一个数值给这个属性,否则这个message会被认为是未初始化。序列化一个未初始化的message会引起一个例外。解析一个未初始化的message会失败。除此以外,required属性跟optional属性一模一样。
optional:这个属性可以设定也可以不。如果没设定的话,预设值会被使用。简单的类型,我们可以指明预设值。举上例,phone number type预设使用HOME。如果没有指明预设值,系统预设值会被使用:数值类型用0,字串类型用空字串,布朗值用否。使用embedded messages的话,预设值永远是message的“default instance“或者”prototype“并且不会设置任何一个属性。如果用accessor向一个为设定的属性取值的话,会返回那个属性的预设值。
repeated:这个属性可能会重复0次或多次,就是可有可无的意思。重复的数值的顺序会被保留。可以把重复属性想象成动态大小的数列。
Required代表永远
使用required的时候要特别小心,如果某天打算停止写入及读取某个required属性,想把它从required改成optional将会产生很大问题。旧的读取者会认为缺少这个属性的message为不完整并且有可能漫无目的地拒绝或者扔掉这些messages。我们应该考虑使用特别为应用建立的检查有效性的代码取而代之。Google某些工程师得出以下结论:使用required斃多于利,他们倾向只使用optional及repeated。可是这个观点并不能一概而论。
把protocol buffer编译
看完proto档案怎么编写,现在可以编译了。首先你要看一大堆文章把protoc编译器安装好。直到有一天你在command line可以顺利跑protoc --version,你便算是安装好编译器了。
复杂点:protoc -I=$SRC_DIR --python_out=$DST_DIR $SRC_DIR/addressbook.proto
简单点:protoc --python_out=. addressbook.proto
然后就会看见产生出来的档案addressbook_pb2.py
Protocol Buffer API
与C++及Java的代码产生不同,Python protocol buffer编译器不会为你直接产生资料提取代码,却会为所有messages,enums,fields及某些神秘的空类产生特别的描述器。
class Person(message.Message): __metaclass__ = reflection.GeneratedProtocolMessageType class PhoneNumber(message.Message): __metaclass__ = reflection.GeneratedProtocolMessageType DESCRIPTOR = _PERSON_PHONENUMBER DESCRIPTOR = _PERSON class AddressBook(message.Message): __metaclass__ = reflection.GeneratedProtocolMessageType DESCRIPTOR = _ADDRESSBOOK其中最重要的一行__metaclass__ = reflection.GeneratedProtocolMessageType详细如何工作不再此述,我们可以把它暂时把它想象成一个建立类的模板。在载入的时候,GeneratedProtocolMessageType元类视同特定的描述器为所有我们需要用到的讯息的产生相应的Python方法及相关的类。
你可以使用Person类犹如每个field已经定义在Message一样,例如
import addressbook_pb2 person = addressbook_pb2.Person() person.id = 1234 person.name = "John Doe" person.email = "jdoe@example.com" phone = person.phone.add() phone.number = "555-4321" phone.type = addressbook_pb2.Person.HOME
留意以上这些assingments并不是单纯把一些随意的field加到generic Python物件里面的。如果你尝试assign一些还没有在proto档定义的field,系统会抛出AttributeError。如果assign错type,系统会抛出TypeError。读取未设定的field将会返回预设值。
person.no_such_field = 1 # raises AttributeError person.id = "1234" # raises TypeError
Enums
addressbook_pb2.Person.WOKR拥有2的值。
Standard Message Methods
每个讯息类别都有一堆方法让你检查及操作这个类的。
IsInitialized():检查是不是所有fields都已经赋值了
__str__():返回human-readable的字串,侦错时特有效,例如str(message) or pirnt(message)
CopyFrom(other_msg):覆盖当前讯息
Clear():清空为空的状态
这几个方法都是implement了class Message的界面。
Parsing and Serialization
每个protocol buffer class都有使用protocol buffer binary format方法去读写。
SerializeToString():讯息变字串。留意那些字节都是binary而不是text。我们仅仅用str type作为一个方便的container
ParseFromString():string变讯息
Protocol Buffers and OO Design
Protocol Buffer class 仅仅是资料类别,不要当成object model。如果想要更丰富的behaviour,最好的方法是wrap掉protocol buffer class成为applicationg-specific class。如果对于proto档案没有控制权的开发者,warpping也是很好的方法,因为我们可以隐藏某些不扼要的资料跟方法,并且暴露有用的方法。记住,永远不要用继承产生出来的class去增加或者改变class的行为,这样会打破内部机制。
Writing A Message
以下例子首先从档案读取AddressBook,添加一个Person,把新的AddressBook写进档案。黄色的是跟protocol compiler产生出来的代码有关的。
往后当我们需要改进protocol buffer定义时候,如果需要让新buffer往前兼容并且让旧buffer往前兼容,有些规矩需要遵守的。
1. 一定不可以更改现有的任何tag number
2. 一定不可以加减任何required fields
3. 可以删除optional或者repeated fields
4. 可以增加新的optional或者repeated fields但是一定要用全新的tag numbers, 全新的意思是那些永远未被使用过的tag numbers,连删除了的fields也不曾使用过的
如果乖乖的遵守以上规条,旧代码会快乐的读取新讯息以及忽略新fields,因为对于旧代码来说,删除了的optional fields会有default values,而删除了的repeated fields会变成empty。新代码会读取就信息。可是,旧讯息一定不会存在任何新的optional fields,所以,要嘛用has_故意检查他们是否已经设定好,要嘛用[default=value]去提供一个合理的预设值。如果optional fields没有提供预设值,系统会给一个:空,否,零。如果新加了repeated field,新代码不能判断到底是新代码设定为空的,还是就代码没有设定,因为repeated field没有 has_ field。
addressbook_pb2.Person.WOKR拥有2的值。
Standard Message Methods
每个讯息类别都有一堆方法让你检查及操作这个类的。
IsInitialized():检查是不是所有fields都已经赋值了
__str__():返回human-readable的字串,侦错时特有效,例如str(message) or pirnt(message)
CopyFrom(other_msg):覆盖当前讯息
Clear():清空为空的状态
这几个方法都是implement了class Message的界面。
Parsing and Serialization
每个protocol buffer class都有使用protocol buffer binary format方法去读写。
SerializeToString():讯息变字串。留意那些字节都是binary而不是text。我们仅仅用str type作为一个方便的container
ParseFromString():string变讯息
Protocol Buffers and OO Design
Protocol Buffer class 仅仅是资料类别,不要当成object model。如果想要更丰富的behaviour,最好的方法是wrap掉protocol buffer class成为applicationg-specific class。如果对于proto档案没有控制权的开发者,warpping也是很好的方法,因为我们可以隐藏某些不扼要的资料跟方法,并且暴露有用的方法。记住,永远不要用继承产生出来的class去增加或者改变class的行为,这样会打破内部机制。
Writing A Message
以下例子首先从档案读取AddressBook,添加一个Person,把新的AddressBook写进档案。黄色的是跟protocol compiler产生出来的代码有关的。
#! /usr/bin/python import addressbook_pb2 import sys # This function fills in a Person message based on user input. 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." # Main procedure: Reads the entire address book from a file, # adds one person based on user input, then writes it back out to the same # file. if len(sys.argv) != 2: print "Usage:", sys.argv[0], "ADDRESS_BOOK_FILE" sys.exit(-1) address_book = addressbook_pb2.AddressBook() # Read the existing address book. try: f = open(sys.argv[1], "rb") address_book.ParseFromString(f.read()) f.close() except IOError: print sys.argv[1] + ": Could not open file. Creating a new one." # Add an address. PromptForAddress(address_book.person.add()) # Write the new address book back to disk. f = open(sys.argv[1], "wb") f.write(address_book.SerializeToString()) f.close()Extending a Protocol Buffer
往后当我们需要改进protocol buffer定义时候,如果需要让新buffer往前兼容并且让旧buffer往前兼容,有些规矩需要遵守的。
1. 一定不可以更改现有的任何tag number
2. 一定不可以加减任何required fields
3. 可以删除optional或者repeated fields
4. 可以增加新的optional或者repeated fields但是一定要用全新的tag numbers, 全新的意思是那些永远未被使用过的tag numbers,连删除了的fields也不曾使用过的
如果乖乖的遵守以上规条,旧代码会快乐的读取新讯息以及忽略新fields,因为对于旧代码来说,删除了的optional fields会有default values,而删除了的repeated fields会变成empty。新代码会读取就信息。可是,旧讯息一定不会存在任何新的optional fields,所以,要嘛用has_故意检查他们是否已经设定好,要嘛用[default=value]去提供一个合理的预设值。如果optional fields没有提供预设值,系统会给一个:空,否,零。如果新加了repeated field,新代码不能判断到底是新代码设定为空的,还是就代码没有设定,因为repeated field没有 has_ field。