在閱讀Chromium Remoting源代碼時,遇到了.proto
文件。經過搜索,在Google Code上找到了對這種文件的詳細說明——Protocol Buffer。這裏,對這個機制做一簡介。本文將以Python語言爲例,介紹:
.proto
文件的格式- 使用Protocol Buffer編譯器
- 使用Python語言的Protocol Buffer API來讀寫消息
更多內容,請參閱Protocol Buffer官方網站。
爲什麼要用Protocol Buffer?
對於結構化的數據,我們有很多方式來序列化/反序列化:
- 使用原始的二進制數據:即使用數據在內存中的映射作爲數據流,但這種方式的兼容性非常差。
- 使用自定義的編碼方式:例如,用字符串「12:3:-23:67」的方式保存這4個整數,對於簡單的數據,這種方式還是比較好用的 。
- 使用XML進行序列化:這種方法兼容性很好,但顯而易見的是,XML的開銷很大。
相比之下,Protocol Buffer看起來要靈活很多。只需編寫.proto
文件來描述數據結構,即可使用Google提供的框架來完成數據的序列化/反序列化。此外,Protocol Buffer格式還支持數據的擴展,即使增加了新的域,通常也能夠保證兼容性。
定義一個協議(Protocol)
這裏以一個電話本程序爲例,設計一個.proto
文件。定義一個.proto
文件很簡單:對每種需要序列化的數據結構編寫一個_message_,然後給每個字段指明名稱和類型即可。本例如下:
package tutorial;
message Person {
required string name = 1;
required int32 id = 2;
optional string email = 3;
enum PhoneType {GO
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;
}
.proto
文件首先由package聲明開始,用於防止項目間的名稱衝突。在Python中,這個就不那麼重要了,因爲Python是以目錄結構作爲包劃分。但是,爲了跨語言訪問,即使用Python,還是應該在文件的開始部分聲明package。
接下來,我們定義了一個message。message是一些域的集合,域的類型可以是簡單的數據類型,例如bool
,int32
,float
,double
以及string
。域的類型還可以是複雜類型,例如上例中的Person
類型的message中包含了PhoneNumber
類型的message。此外,也可以定義枚舉(enum
)類型,例如這裏的PhoneType
。
「=1」,「=2」這樣的記號是每個字段唯一的標記。簡單來說,在傳輸時,傳輸的數據是這個標記,而不是域的名字。1-15在編碼時使用較少的字節數,因此,對於常用的域,應當考慮使用1-15作爲標記。
每個域必須包含一下修飾符之一:
required
:字段值不能爲空,否則會產生異常。optional
:字段值如果沒有設置,則使用默認值,如果沒有指定默認值,則使用系統默認值,例如0,空字符串等。repeated
:字段可以重複出現任意自然數次。值的順序會被保留。
關於.proto
文件寫法的完整規則,請參見Protocol Buffer Language Guide。
編譯Protocol Buffer
完成.proto
文件後,接下來需要生成可以讀寫AddressBook
的類。這一工作通過使用protoc
程序完成:
- 安裝編譯器。下載安裝包,按照README的提示操作。
- 運行編譯器:
$ protoc --python_out=. addressbook.proto
這樣會在當前目錄下生成addressbook_pb2.py
文件。
Protocol Buffer API
Python版的編譯器並不生成讀寫數據的代碼,而是生成message的描述符,通過Google提供的庫來訪問。
可以這樣使用Protocol Buffer:
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
枚舉類型
枚舉類型的值和.proto
文件中指定的值一樣。因此,以addressbook_pb2.Person.WORK
爲例,其值爲2。
Message的一般方法
IsInitialized()
:檢查所有的required字段是否都已賦值CopyFrom(other_msg)
:用other_msg替換當前的messageClear()
:清空message
更多信息,請參見Message的API文檔。
解析和序列化
SerializeToString()
:將message序列化,並得到一個字符串。注意,這裏的字符串只作爲二進制數據的容器。ParseFromString(data)
:把字符串解析爲message。
最後,以兩個例子(寫、讀message)作爲本文的結束:
示例一
#! /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()
示例二
#! /usr/bin/python
import addressbook_pb2
import sys
# Iterates though all people in the AddressBook and prints info about them.
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 address:", 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
# Main procedure: Reads the entire address book from a file and prints all
# the information inside.
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.
f = open(sys.argv[1], "rb")
address_book.ParseFromString(f.read())
f.close()
ListPeople(address_book)