天天看點

google ProtoBuf 開發者指南

轉自:http://blog.csdn.net/program_think/article/details/4229773

今天來介紹一下“Protocol Buffers”(以下簡稱protobuf)這個玩意兒。本來俺在構思“生産者/消費者模式 ”系列的下一個文章:關于生産者和消費者之間的資料傳輸格式。由于裡面扯到了protobuf,想想幹脆單獨開一個文章算了。

  ★protobuf是啥玩意兒?

  為了照顧從沒聽說過的同學,照例先來掃盲一把。

  首先,protobuf是一個開源項目(官方站點在“這裡 ”),而且是背景很硬的開源項目。網上現有的大部分(至少80%)開源項目,要麼是某人單幹、要麼是幾個閑雜人等合夥搞。而protobuf則不然,它是鼎鼎大名的Google公司開發出來,并且在Google内部久經考驗的一個東東。由此可見,它的作者絕非一般閑雜人等可比。

  那這個聽起來牛X的東東到底有啥用處捏?簡單地說,這個東東幹的事兒其實和XML差不多,也就是把某種資料結構的資訊,以某種格式儲存起來。主要用于資料存儲、傳輸協定格式等場合。有同學可能心理犯嘀咕了:放着好好的XML不用,幹嘛重新發明輪子啊?!先别急,後面俺自然會有說道。

  話說到了去年(大約是08年7 月),Google突然大發慈悲,把這個好東西貢獻給了開源社群。這下,像俺這種喜歡撿現成的家夥可就有福啦!貌似喜歡撿現成的家夥還蠻多滴,再加上Google的号召力,開源後不到一年,protobuf的人氣就已經很旺了。是以俺為了與時俱進,就單獨開個文章來忽悠一把。

  ★protobuf有啥特色?

  掃盲完了之後,就該聊一下技術方面的話題了。由于這玩意兒釋出的時間較短(未滿周歲),是以俺接觸的時間也不長。今天在此是先學現賣,列位看官多多包涵 :-)

  ◇性能好/效率高

  現在,俺就來說說Google公司為啥放着好端端的XML不用,非要另起爐竈,重新造輪子。一個根本的原因是XML性能不夠好。

  先說時間開銷:XML格式化(序列化)的開銷倒還好;但是XML解析(反序列化)的開銷就不敢恭維啦。俺之前經常碰到一些時間性能很敏感的場合,由于不堪忍受XML解析的速度,棄之如敝履。

  再來看空間開銷:熟悉XML文法的同學應該知道,XML格式為了有較好的可讀性,引入了一些備援的文本資訊。是以空間開銷也不是太好(不過這點缺點,俺不常碰到)。

  由于Google公司賴以吹噓的就是它的海量資料和海量處理能力。對于幾十萬、上百萬機器的叢集,動不動就是PB級的資料量,哪怕性能稍微提高0.1%也是相當可觀滴。是以Google自然無法容忍XML在性能上的明顯缺點。再加上Google從來就不缺造輪子的牛人,是以protobuf也就應運而生了。

  Google對于性能的偏執,那可是出了名的。是以,俺對于Google搞出來protobuf是非常滴放心,性能上不敢說是最好,但肯定不會太差。

  ◇代碼生成機制

  除了性能好,代碼生成機制是主要吸引俺的地方。為了說明這個代碼生成機制,俺舉個例子。

  比如有個電子商務的系統(假設用C++實作),其中的子產品A需要發送大量的訂單資訊給子產品B,通訊的方式使用socket。

假設訂單包括如下屬性:

--------------------------------

  時間:time(用整數表示)

  客戶id:userid(用整數表示)

  交易金額:price(用浮點數表示)

  交易的描述:desc(用字元串表示)

--------------------------------

  如果使用protobuf實作,首先要寫一個proto檔案(不妨叫Order.proto),在該檔案中添加一個名為"Order"的message結構,用來描述通訊協定中的結構化資料。該檔案的内容大緻如下:

--------------------------------

message Order

{

  required int32 time = 1;

  required int32 userid = 2;

  required float price = 3;

  optional string desc = 4;

}

--------------------------------

  然後,使用protobuf内置的編譯器編譯 該proto。由于本例子的子產品是C++,你可以通過protobuf編譯器的指令行參數(看“這裡 ”),指定它生成C++語言的“訂單包裝類”。(一般來說,一個message結構會生成一個包裝類)

  然後你使用類似下面的代碼來序列化/解析該訂單包裝類:

--------------------------------

// 發送方

Order order;

order.set_time(XXXX);

order.set_userid(123);

order.set_price(100.0f);

order.set_desc("a test order");

string sOrder;

order.SerailzeToString(&sOrder);

// 然後調用某種socket的通訊庫把序列化之後的字元串發送出去

// ......

--------------------------------

// 接收方

string sOrder;

// 先通過網絡通訊庫接收到資料,存放到某字元串sOrder

// ......

Order order;

if(order.ParseFromString(sOrder)) // 解析該字元串

{

  cout << "userid:" << order.userid() << endl

  << "desc:" << order.desc() << endl;

}

else

{

  cerr << "parse error!" << endl;

}

--------------------------------

  有了這種代碼生成機制,開發人員再也不用吭哧吭哧地編寫那些協定解析的代碼了(幹這種活是典型的吃力不讨好)。

  萬一将來需求發生變更,要求給訂單再增加一個“狀态”的屬性,那隻需要在Order.proto檔案中增加一行代碼。對于發送方(子產品A),隻要增加一行設定狀态的代碼;對于接收方(子產品B)隻要增加一行讀取狀态的代碼。哇塞,簡直太輕松了!

  另外,如果通訊雙方使用不同的程式設計語言來實作,使用這種機制可以有效確定兩邊的子產品對于協定的處理是一緻的。

  順便跑題一下。

  從某種意義上講,可以把proto檔案看成是描述通訊協定的規格說明書(或者叫接口規範)。這種伎倆其實老早就有了,搞過微軟的COM程式設計或者接觸過CORBA的同學,應該都能從中看到IDL(詳細解釋看“這裡 ”)的影子。它們的思想是相通滴。

  ◇支援“向後相容”和“向前相容”

  還是拿剛才的例子來說事兒。為了叙述友善,俺把增加了“狀态”屬性的訂單協定成為“新版本”;之前的叫“老版本”。

  所謂的“向後相容”(backward compatible),就是說,當子產品B更新了之後,它能夠正确識别子產品A發出的老版本的協定。由于老版本沒有“狀态”這個屬性,在擴充協定時,可以考慮把“狀态”屬性設定成非必填 的,或者給“狀态”屬性設定一個預設值(如何設定預設值,參見“這裡 ”)。

  所謂的“向前相容”(forward compatible),就是說,當子產品A更新了之後,子產品B能夠正常識别子產品A發出的新版本的協定。這時候,新增加的“狀态”屬性會被忽略。

  “向後相容”和“向前相容”有啥用捏?俺舉個例子:當你維護一個很龐大的分布式系統時,由于你無法同時 更新所有 子產品,為了保證在更新過程中,整個系統能夠盡可能不受影響,就需要盡量保證通訊協定的“向後相容”或“向前相容”。

  ◇支援多種程式設計語言

  俺開博以來點評的幾個開源項目(比如“Sqlite ”、“cURL ”),都是支援很多種 程式設計語言滴,這次的protobuf也不例外。在Google官方釋出的源代碼中包含了C++、Java、Python三種語言(正好也是俺最常用的三種,真爽)。如果你平時用的就是這三種語言之一,那就好辦了。

  假如你想把protobuf用于其它語言,咋辦捏?由于Google一呼百應的号召力,開源社群對protobuf響應踴躍,近期冒出很多其它程式設計語言的版本(比如ActionScript、C#、Lisp、Erlang、Perl、PHP、Ruby等),有些語言還同時搞出了多個開源的項目。具體細節可以參見“這裡 ”。

  不過俺有義務提醒一下在座的各位同學。如果你考慮把protobuf用于上述這些語言,一定認真評估對應的開源庫。因為這些開源庫不是Google官方提供的、而且出來的時間還不長。是以,它們的品質、性能等方面可能還有欠缺。

  ★protobuf有啥缺陷?

  前幾天剛剛在“光環效應 ”的文章裡強調了“要同時評估優點和缺點”。是以俺最後再來批判一下這玩意兒的缺點。

  ◇應用不夠廣

  由于protobuf剛公布沒多久,相比XML而言,protobuf還屬于初出茅廬。是以,在知名度、應用廣度等方面都遠不如XML。由于這個原因,假如你設計的系統需要提供若幹對外的接口給第三方系統調用,俺奉勸你暫時不要考慮protobuf格式。

  ◇二進制格式導緻可讀性差

  為了提高性能,protobuf采用了二進制格式進行編碼。這直接導緻了可讀性差的問題(嚴格地說,是沒有可讀性)。雖然protobuf提供了TextFormat這個工具類(文檔在“這裡 ”),但終究無法徹底解決此問題。

  可讀性差的危害,俺再來舉個例子。比如通訊雙方如果出現問題,極易導緻扯皮(都不承認自己有問題,都說是對方的錯)。俺對付扯皮的一個簡單方法就是直接抓包并dump成log,能比較容易地看出錯誤在哪一方。但是protobuf的二進制格式,導緻你抓包并直接dump出來的log難以看懂。

  ◇缺乏自描述

  一般來說,XML是自描述的,而protobuf格式則不是。給你一段二進制格式的協定内容,如果不配合相應的proto檔案,那簡直就像天書一般。

  由于“缺乏自描述”,再加上“二進制格式導緻可讀性差”。是以在配置檔案方面,protobuf是肯定無法取代XML的地位滴。

  ★為什麼俺會用上protobuf?

  俺自從前段時間接觸了protobuf之後,就着手把俺負責的産品中的部分 資料傳輸協定替換成protobuf。可能有同學會問,和protobuf類似的東東也有不少,為啥獨獨相中protobuf捏?由于今天寫的篇幅已經蠻長了,俺賣個關子,把這個話題留到“生産者/消費者模式[5]:如何選擇傳輸協定及格式?”。俺會在後續的這個文章裡對比各種五花八門的協定格式,并談談俺的淺見。

以下來自:http://www.cppblog.com/woaidongmao/archive/2009/06/23/88391.aspx

1   概覽

歡迎來到protocol buffer的開發者指南文檔,一種語言無關、平台無關、擴充性好的用于通信協定、資料存儲的結構化資料串行化方法。

本文檔面向希望使用protocol buffer的Java、C++或Python開發者。這個概覽介紹了protocol buffer,并告訴你如何開始,你随後可以跟随程式設計指導( http://code.google.com/apis/protocolbuffers/docs/tutorials.html )深入了解protocol buffer編碼方式( http://code.google.com/apis/protocolbuffers/docs/encoding.html )。API參考文檔( http://code.google.com/apis/protocolbuffers/docs/reference/overview.html )同樣也是提供了這三種程式設計語言的版本,不夠協定語言( http://code.google.com/apis/protocolbuffers/docs/proto.html )和樣式( http://code.google.com/apis/protocolbuffers/docs/style.html )指導都是編寫 .proto 檔案。

1.1   什麼是protocol buffer

ProtocolBuffer是用于結構化資料串行化的靈活、高效、自動的方法,有如XML,不過它更小、更快、也更簡單。你可以定義自己的資料結構,然後使用代碼生成器生成的代碼來讀寫這個資料結構。你甚至可以在無需重新部署程式的情況下更新資料結構。

1.2   他們如何工作

你首先需要在一個 .proto 檔案中定義你需要做串行化的資料結構資訊。每個ProtocolBuffer資訊是一小段邏輯記錄,包含一系列的鍵值對。這裡有個非常簡單的 .proto 檔案定義了個人資訊:

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;

}

有如你所見,消息格式很簡單,每個消息類型擁有一個或多個特定的數字字段,每個字段擁有一個名字和一個值類型。值類型可以是數字(整數或浮點)、布爾型、字元串、原始位元組或者其他ProtocolBuffer類型,還允許資料結構的分級。你可以指定可選字段,必選字段和重複字段。你可以在( http://code.google.com/apis/protocolbuffers/docs/proto.html )找到更多關于如何編寫 .proto 檔案的資訊。

一旦你定義了自己的封包格式(message),你就可以運作ProtocolBuffer編譯器,将你的 .proto 檔案編譯成特定語言的類。這些類提供了簡單的方法通路每個字段(像是 query() 和 set_query() ),像是通路類的方法一樣将結構串行化或反串行化。例如你可以選擇C++語言,運作編譯如上的協定檔案生成類叫做 Person 。随後你就可以在應用中使用這個類來串行化的讀取封包資訊。你可以這麼寫代碼:

Person person;

person.set_name("John Doe");

person.set_id(1234);

person.set_email("[email protected]");

fstream.output("myfile",ios::out | ios::binary);

person.SerializeToOstream(&output);

然後,你可以讀取封包中的資料:

fstream input("myfile",ios::in | ios:binary);

Person person;

person.ParseFromIstream(&input);

cout << "Name: " << person.name() << endl;

cout << "E-mail: " << person.email() << endl;

你可以在不影響向後相容的情況下随意給資料結構增加字段,舊有的資料會忽略新的字段。是以如果使用ProtocolBuffer作為通信協定,你可以無須擔心破壞現有代碼的情況下擴充協定。

你可以在API參考( http://code.google.com/apis/protocolbuffers/docs/reference/overview.html )中找到完整的參考,而關于ProtocolBuffer的封包格式編碼則可以在( http://code.google.com/apis/protocolbuffers/docs/encoding.html )中找到。

1.3   為什麼不用XML?

ProtocolBuffer擁有多項比XML更進階的串行化結構資料的特性,ProtocolBuffer:

·   更簡單

·   小3-10倍

·   快20-100倍

·   更少的歧義

·   可以友善的生成資料存取類

例如,讓我們看看如何在XML中模組化Person的name和email字段:

<person>

    <name>John Doe</name>

    <email>[email protected]</email>

</person>

對應的ProtocolBuffer封包則如下:

#ProtocolBuffer的文本表示

#這不是正常時使用的二進制資料

person {

    name: "John Doe"

    email: "[email protected]"

}

當這個封包編碼到ProtocolBuffer的二進制格式( http://code.google.com/apis/protocolbuffers/docs/encoding.html )時(上面的文本僅用于調試和編輯),它隻需要28位元組和100-200ns的解析時間。而XML的版本需要69位元組(除去空白)和5000-10000ns的解析時間。

當然,操作ProtocolBuffer也很簡單:

cout << "Name: " << person.name() << endl;

cout << "E-mail: " << person.email() << endl;

而XML的你需要:

cout << "Name: "

     << person.getElementsByTagName("name")->item(0)->innerText()

     << endl;

cout << "E-mail: "

     << person.getElementsByTagName("email")->item(0)->innerText()

     << end;

當然,ProtocolBuffer并不是在任何時候都比XML更合适,例如ProtocolBuffer無法對一個基于标記文本的文檔模組化,因為你根本沒法友善的在文本中插入結構。另外,XML是便于人類閱讀和編輯的,而ProtocolBuffer則不是。還有XML是自解釋的,而 ProtocolBuffer僅在你擁有封包格式定義的 .proto 檔案時才有意義。

1.4   聽起來像是為我的解決方案,如何開始?

下載下傳包( http://code.google.com/p/protobuf/downloads/ ),包含了Java、Python、C++的ProtocolBuffer編譯器,用于生成你需要的IO類。建構和安裝你的編譯器,跟随README的指令就可以做到。

一旦你安裝好了,就可以跟着程式設計指導( http://code.google.com/apis/protocolbuffers/docs/tutorials.html )來選擇語言-随後就是使用ProtocolBuffer建立一個簡單的應用了。

1.5   一點曆史

ProtocolBuffer最初是在Google開發的,用以解決索引伺服器的請求、響應協定。在使用ProtocolBuffer之前,有一種格式用以處理請求和響應資料的編碼和解碼,并且支援多種版本的協定。而這最終導緻了醜陋的代碼,有如:

if (version==3) {

    ...

}else if (version>4) {

    if (version==5) {

        ...

    }

    ...

}

通信協定是以變得越來越複雜,因為開發者必須確定,送出請求的人和接受請求的人必須同時相容,并且在一方開始使用新協定時,另外一方也要可以接受。

ProtocolBuffer設計用于解決這一類問題:

·   很友善引入新字段,而中間伺服器可以忽略這些字段,直接傳遞過去而無需了解所有的字段。

·   格式可以自描述,并且可以在多種語言中使用(C++、Java等)

然而使用者仍然需要手寫解析代碼。

随着系統的演化,他需要一些其他的功能:

·   自動生成編碼和解碼代碼,而無需自己編寫解析器。

·   除了用于簡短的RPC(Remote Procedure Call)請求,人們使用ProtocolBuffer來做資料存儲格式(例如BitTable)。

·   RPC伺服器接口可以作為 .proto 檔案來描述,而通過ProtocolBuffer的編譯器生成存根(stub)類供使用者實作伺服器接口。

ProtocolBuffer現在已經是Google的混合語言資料标準了,現在已經正在使用的有超過48,162種封包格式定義和超過12,183個 .proto 檔案。他們用于RPC系統和持續資料存儲系統。

2   語言指導

本指導描述了如何使用ProtocolBuffer語言來定義結構化資料類型,包括 .proto 檔案的文法和如何生成存取類。

這是一份指導手冊,一步步的例子使用文檔中的多種功能,檢視入門指導( http://code.google.com/apis/protocolbuffers/docs/tutorials.html )選擇你的語言。

2.1   定義一個消息類型

@waiting …

2.2   值類型

@waiting …

2.3   可選字段與預設值

@waiting …

2.4   枚舉

@waiting …

2.5   使用其他消息類型

@waiting …

2.6   嵌套類型

@waiting …

2.7   更新一個資料類型

@waiting …

2.8   擴充

@waiting …

2.9   包

@waiting …

2.10   定義服務

@waiting …

2.11   選項

@waiting …

2.12   生成你的類

@waiting …

3   代碼風格指導

本文檔提供了 .proto 檔案的代碼風格指導。按照慣例,你将會,你将會生成一些便于閱讀和一緻的ProtocolBuffer定義檔案。

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的串行化二進制資料格式定義。你如果僅僅是在應用中使用ProtocolBuffer,并不需要知道這些,但是這些會對你定義高效的格式有所幫助。

4.1   一個簡單的消息

@waiting …

4.2   基于128的Varints

@waiting …

4.3   消息結構

@waiting …

4.4   更多的值類型

@waiting …

4.5   内嵌消息

@waiting …

4.6   可選的和重複的元素

@waiting …

4.7   字段順序

@waiting …

5   ProtocolBuffer基礎:C++

@waiting …

6   ProtocolBuffer基礎:Java

@waiting …

7   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 )。

7.1   為什麼使用ProtocolBuffer?

下面的例子”位址本”應用用于讀寫人的聯系資訊。每個人有name、ID、email,和聯系人電話号碼。

如何串行化和讀取結構化資料呢?有如下幾種問題:

·   使用Python的pickle,這是語言内置的預設方法,不過沒法演化,也無法讓其他語言支援。

·   你可以發明一種資料編碼方法,例如4個整數”12:3-23:67″,這是簡單而靈活的方法,不過你需要自己寫解析器代碼,且隻适用于簡單的資料。

·   串行化資料到XML。這種方法因為可讀性和多種語言的相容函數庫而顯得比較吸引人,不過這也不是最好的方法,因為XML浪費空間是臭名昭著的,編碼解碼也很浪費時間。而XML DOM樹也是很複雜的。

ProtocolBuffer提供了靈活、高效、自動化的方法來解決這些問題。通過ProtocolBuffer,隻需要寫一個 .proto 資料結構描述檔案,就可以編譯到幾種語言的自動編碼解碼類。生成的類提供了setter和getter方法來控制讀寫細節。最重要的是 ProtocolBuffer支援後期擴充協定,而又確定舊格式可以相容。

7.2   哪裡可以找到例子代碼

源碼發行包中已經包含了,在”example”檔案夾。

7.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;

}

有如你所見的,文法類似于C++或Java。讓我們分塊了解他們。

@waiting …

7.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 到目标目錄。

7.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 )。

7.5.1   枚舉

枚舉在元類中定義為一些符号常量對應的數字。例如常量 addressbook_pb2.Person.WORK 擁有值2。

7.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 )。

7.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生成類包裝的很适合于你的應用,并隐藏一些資料和方法,暴露有用的函數等等。 你不可以通過繼承來給自動生成的類添加行為。 這會破壞他們的内部工作機制。

7.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()

7.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)

7.8   擴充ProtocolBuffer

在你發不了代碼以後,可能會想要改進ProtocolBuffer的定義。如果你想新的資料結構向後相容,而你的舊資料可以向前相容,那麼你就找對了東西了,不過有些規則需要遵守。在新版本的ProtocolBuffer中:

·   必須不可以改變已經存在的标簽的數字。

·   必須不可以增加或删除必須(required)字段。

·   可以删除可選(optional)或重複(repeated)字段。

·   可以添加新的可選或重複字段,但是必須使用新的标簽數字,必須是之前的字段所沒有用過的。

這些規則也有例外( http://code.google.com/apis/protocolbuffers/docs/proto.html#updating ),不過很少使用。

如果你遵從這些規則,舊代碼會很容易的讀取新的消息,并簡單的忽略新的字段。而對舊的被删除的可選字段也會簡單的使用他們的預設值,被删除的重複字段會自動為空。新的代碼也會透明的讀取舊的消息。然而,需要注意的是新的可選消息不會在舊的消息中顯示,是以你需要使用 has_ 嚴格的檢查他們是否存在,或者在 .proto 檔案中提供一個預設值。如果沒有預設值,就會有一個類型相關的預設預設值:對于字元串就是空字元串;對于布爾型則是false;對于數字類型預設為0。同時要注意的是如果你添加了新的重複字段,你的新代碼不會告訴你這個字段為空(新代碼)也不會,也不會(舊代碼)包含 has_ 标志。

7.9   進階使用

ProtocolBuffer不僅僅提供了資料結構的存取和串行化。檢視Python API參考( http://code.google.com/apis/protocolbuffers/docs/reference/python/index.html )了解更多功能。

一個核心功能是通過消息類的映射(reflection)提供的。你可以通過它周遊消息的所有字段,和管理他們的值。關于映射的一個很有用的地方是轉換到其他編碼,如XML或JSON。一個使用映射的更進階的功能是尋找同類型兩個消息的差異,或者開發出排序、正規表達式等功能。使用你的創造力,還可以用ProtocolBuffer實作比你以前想象的更多的問題。

映射是通過消息接口提供的。

8   參考概覽

@waiting …

9   C++代碼生成

@waiting …

10   C++ API

@waiting …

11   Java代碼生成

@waiting …

12   Java API

@waiting …

13   Python代碼生成

本頁提供了Python生成類的相關細節。你可以在閱讀本文檔之前檢視語言指導。

Python的ProtocolBuffer實作與C++和Java的略有不同,編譯器隻輸出建構代碼的描述符來生成類,而由Python的元類來執行工作。本文檔描述了元類開始生效以後的東西。

13.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檔案名。

13.2   包

Python代碼生成根本不在乎包的名字。因為Python使用目錄名來做包名。

13.3   消息

先看看一個簡單的消息聲明:

message Foo {}

ProtocolBuffer編譯器會生成類Foo,它是 google.protobuf.Message 的子類。這個實體類,不含有虛拟方法。不像C++和Java,Python生成類對優化選項不感冒;實際上Python的生成代碼已經為代碼大小做了優化。

你不能繼承Foo的子類。生成類被設計不可以被繼承,否則會被打破一些設計。另外,繼承本類也是不好的設計。

Python的消息類沒有特定的公共成員,而是定義接口,極其嵌套的字段、消息和枚舉類型。

一個消息可以在另外一個消息中聲明,例如 message Foo { message Bar {}} 。在這種情況下,Bar類定義為Foo的一個靜态成員,是以你可以通過 Foo.Bar 來引用。

13.4   字段

對于消息類型中的每一個字段,都有對應的同名成員。

13.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")

13.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")

13.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() 。

13.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

13.4.5   枚舉類型

@waiting …

13.4.6   擴充

@waiting …

13.5   服務

13.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 :傳回指定方法的請求和響應類。

13.5.2   存根(Stub)

ProtocolBuffer編譯器也會為每個服務接口提供一個存根實作,用于用戶端發送請求到伺服器。對于Foo服務,存根實作是 Foo_Stub 。

Foo_Stub 是Foo的子類,他的構造器是一個 RpcChannel 。存根會實作調用每個服務方法的 CallMethod() 。

ProtocolBuffer哭并不包含RPC實作。然而,它包含了你構造服務類的所有工具,不過選擇RPC實作則随你喜歡。你隻需要提供 RpcChannel 和 RpcController 的實作即可。

14   Python API

@waiting …

15   其他語言

http://www.cppblog.com/liquidx

下一篇: fenby C語言P24

繼續閱讀