賴勇浩(http://laiyonghoa.com
)
註:前幾天 GAE SDK 1.5.1 發布,其中一個新特性是 Python SDK 增加了 ProtoRPC API,我對 GAE 興趣不大,但最近正好自己也在寫基於 google protobuf 的 RPC(不同的是我的 RPC 基於 TCP 的),所以很有興趣地看了一下 ProtoRPC 的 overview,後來心血來潮就把它簡單譯了一下,不過不是逐句對譯,所以如有困惑,敬請參詳原文(原文也在變化之中,我譯的是 2011 年 6 月 25 日的版本,如果後來更新了,對不上號,概不負責):http://code.google.com/appengine/docs/python/tools/protorpc/overview.html
ProtoRPC 發送和接收基於 HTTP 的遠端程序呼叫(RPC)服務的簡單方案。所謂 RPC 服務就是提供結構化地擴充應用程式與 Web 應用程式互動的一堆訊息類型和遠程方法。因為只能夠用 Python 程式設計語言來定義訊息和服務,所以更容易開發服務、測試服務和在 App Engine 上獲得更好的伸縮性。
當把 ProtoRPC 用以任何基於 HTTP 的 RPC 時,一些常見的應用情境包括:
- 發布第三方使用的 web APIs
- 建立結構化的 Ajax 後端
- 複製長期啟動並執行伺服器互動
可以在單一 Python 類中定義任意數量的 ProtoRPC 遠程方法,每個遠程方法接受一個請求(包含一些特定的參數集合),並返回一個特定的響應。所有的請求和響應都是使用者定義的類,這些類又稱為訊息。
實驗性的!
ProtoRPC 是一個實驗性的、全新的、快速變化的 App Engine 的新特性,請勿用於商業關鍵的應用程式,當它完全可用時,我們會告訴大家的。
ProtoRPC 的 Hello World
這個小節示範一下從遠端接受一個訊息(包含了使用者名稱 HelloRequest.my_name),並返回一個響應(HelloResponse.hello)。
from google.appengine.ext import webapp<br />from google.appengine.ext.webapp import util<br />from protorpc import messages<br />from protorpc.webapp import service_handlers<br />from protorpc import remote<br />package = 'hello'<br /># Create the request string containing the user's name<br />class HelloRequest(messages.Message):<br /> my_name = messages.StringField(1, required=True)<br /># Create the response string<br />class HelloResponse(messages.Message):<br /> hello = messages.StringField(1, required=True)<br /># Create the RPC service to exchange messages<br />class HelloService(remote.Service):<br /> @remote.remote(HelloRequest, HelloResponse)<br /> def hello(self, request):<br /> return HelloResponse(hello='Hello there, %s!' %<br /> request.my_name)<br /># Map the RPC service and path (/hello)<br />service_mappings = service_handlers.service_mapping(<br /> [('/hello', HelloService),<br /> ])<br /># Apply the service mappings to Webapp<br />application = webapp.WSGIApplication(service_mappings)<br />def main():<br /> util.run_wsgi_app(application)<br />if __name__ == '__main__':<br /> main()
開始 ProtoRPC 之旅
在這裡我們使用 App Engine Getting Started Guide (Python) 的留言板樣本(http://code.google.com/appengine/docs/python/gettingstarted/)。使用者可以線上訪問這個留言板(已經包含在 Python SDK 中),編寫留言,查看所有使用者的留言。
接下來將把 ProtoRPC 應用到這個基本的留言板中,使得 Web 應用程式能夠存取其資料。但這個教程只涵蓋了使用 ProtoRPC 來擴充留言板功能,其實你可以做到更多,比如編寫一個工具讀取使用者提交的所有訊息或建立一個每天有多少條留言的圖表。根據特定的應用,可以隨意使用 ProtoRPC,重要的是 ProtoRPC 可以讓你隨便折騰你的資料。
萬事之始,是建立一個名為 postservice.py 的檔案,所有存取留言板應用資料的遠程方法都在其中實現。
建立 PostService 模組
第一步,在應用程式目錄下建立一個名為 postservice.py 的檔案,它將實現兩個方法:一個用以遠程提交資料,另一個用以遠程擷取資料。
燃燒吧,訊息!
訊息是 ProtoRPC 的基礎資料型別 (Elementary Data Type),通過聲明一個 Message 繼承下來的子類來定義訊息,然後定義與訊息欄位相應的類屬性。
留言板服務能夠讓使用者提交一個留言,那麼就定義一個訊息來表示一個留言吧:
from protorpc import messages<br />class Note(messages.Message):<br /> text = messages.StringField(1, required=True)<br /> when = messages.IntegerField(2)
Note 訊息定義了兩個欄位,text 和 when。每一個欄位都有自己的類型,比如 text 欄位的類型提 unicode 字串,用以表示使用者通過留言板頁面提交的內容,而 when 欄位就是一個整數,用以表示使用者提交的時間戳記。除此之外:
- 每個欄位有一個唯一的數值(如 text 是 1,when 是 2),這是底層網路通訊協定要用到的欄位標識
- 定義 text 為必要欄位。每個欄位預設是可選的,所以要用 required=True 來定製為必要欄位。
可以通過 Note 類的構建函數來給欄位賦值:
# Import the standard time Python library to handle the timestamp.<br />import time<br />note_instance = Note(text=u'Hello guestbook!', when=int(time.time())
也可以操作普通的 Python 屬性一樣讀、寫欄位,比如下面的代碼可以改變訊息:
print note_instance.text<br />note_instance.text = u'Good-bye guestbook!'<br />print note_instance.text<br /># Which outputs the following<br />>>><br />Hello guestbook!<br />Good-bye guestbook!
定義服務
一個服務就是指一個從 Service 基類繼承的類,服務的遠程方法由 remote 裝飾器標識。服務的每個方法都接受單個訊息作為參數,並返回一個訊息作為響應。
現在來定義 PostService 的第一個方法。如果你還沒有準備好,記得先在你應用目錄下建立 postservice.py 檔案,或如果你覺得有必要的話就讀一下留言板教程(http://code.google.com/appengine/docs/python/gettingstarted/)。PostService 與留言板教程一樣,使用 guestbook.Greeting 來儲存提交的資料。
import datetime<br />from protorpc import message_types<br />from protorpc import remote<br />import guestbook<br />class PostService(remote.Service):<br /> # Add the remote decorator to indicate the service methods<br /> @remote.remote(Note, message_types.VoidMessage)<br /> def post_note(self, request):<br /> # If the Note instance has a timestamp, use that timestamp<br /> if request.when is not None:<br /> when = datetime.datetime.utcfromtimestamp(request.when)<br /> # Else use the current time<br /> else:<br /> when = datetime.datetime.now()<br /> note = guestbook.Greeting(content=request.text, date=when)<br /> note.put()<br /> return message_types.VoidMessage()
remote 裝飾器有兩個參數:
- 請求類型,post_note() 接受一個 Note 執行個體作為請求
- 響應類型,ProtoRPC 有一個內建類型叫人 VoidMessage(在 protorpc.message_types 模組中定義),它表示沒有欄位的訊息,所以 post_note() 其實並沒有返回任何有意義的東西給調用方。
因為 Note.when 是一個可選欄位,所以調用方可能並沒有對它賦值,這時 when 的值就是 None,當 Note.when 的值為 None,post_note() 以接收到訊息的時間作為時間戳記。
響應訊息由遠程方法執行個體化,一般是遠程方法需要返回的時候會這樣做。
註冊服務
可以使用 App Engine 的 webapp 架構(http://code.google.com/appengine/docs/python/tools/webapp/)發布新的服務。ProtoRPC 有一個很小的庫(protorpc.service_handlers)可以簡化這個事兒。在應用目錄建立一個 services.py 的檔案,把下面的代碼拷進去:
from google.appengine.ext import webapp<br />from google.appengine.ext.webapp import util<br />from protorpc import service_handlers<br />import PostService<br /># Register mapping with application.<br />application = webapp.WSGIApplication(<br /> service_handlers.service_mapping(<br /> [('/PostService', PostService.PostService)]),<br /> debug=True)<br />def main():<br /> util.run_wsgi_app(application)<br />if __name__ == '__main__':<br /> main()
然後把下面的代碼加到 app.yaml 檔案中:
- url: /PostService.*<br /> script: services.py
可以建立你的基本 webapp 的服務啦~
通過命令列測試服務
建立服務後,可以使用 curl 或相似的命令列工具進行測試:
# After starting the development web server:<br />% curl -H /<br /> 'content-type:application/json' /<br /> -d {"text": "Hello guestbook!"}'/</p><p>http://localhost:8080/PostService.post_note
當返回一個空的 JSON 表示留言提交成功,可以通過瀏覽器(http://localhost:8080/)查看這個留言。
增加訊息欄位
現在可以向 PostService 提交留言了,接下來再增加一個新的方法。先在 postservice.py 中定義一個請求訊息,它有一些預設值,還有之前沒有接觸過的枚舉欄位(用來告訴伺服器如何對留言排序)。讓我們把下面的代碼加到 PostService 類之前:
class GetNotesRequest(messages.Message):<br /> limit = messages.IntegerField(1, default=10)<br /> on_or_before = messages.IntegerField(2)<br /> class Order(messages.Enum):<br /> WHEN = 1<br /> TEXT = 2<br /> order = messages.EnumField(Order, 3, default=Order.WHEN)
訊息中 limit 欄位表示最大的請求的留言數量,預設為 10 條(通過 default=10 關鍵字參數指定)。
order 欄位引入了一個 EnumField 類,它能夠讓 enum 欄位類型的取值嚴格地限定在已定義的符號值範圍內。在這裡,伺服器如何排序顯示中的留言是由 order 欄位指定的。要定義枚舉值,需要建立 Enum 類的子類,它的每一個類屬性都應為唯一的數字,然後被轉換為一個可以通過類來存取的枚舉類型的執行個體。
print 'Enum value Order.%s has number %d' % (Order.WHEN.name,<br /> Order.WHEN.number)
除了訪問它的 name 和 number 屬性,enum 值還有一個“特殊技術”能夠更方便地轉換它的 name 和 number,比如轉換某個值到字串或整數:
print 'Enum value Order.%s has number %d' % (Order.WHEN,<br /> Order.WHEN)
枚舉欄位的聲明與其它欄位是類似的,除了需要在第一個參數標明它的枚舉類型,而且枚舉欄位也可以有預設值。
定義響應訊息
現在定義一下 get_notes() 的響應訊息。這個響應顯然應該包含一組 Note 訊息,訊息能夠包含其它的消:
class Notes(messages.Message):<br /> notes = messages.MessageField(Note, 1, repeated=True)
Notes.notes 欄位是一個重複欄位(通過 repeated=True 關鍵字參數說明),重複欄位的值是一個列表,在這個例子裡,Notes.notes 就是包含多個 Note 執行個體的列表,列表是自動建立的,並且不能賦值為 None。
來個如何建立 Notes 對象的例子:
response = Notes(notes=[Note(text='This is note 1'),<br /> Note(text='This is note 2')])<br />print 'The first note is:', response.notes[0].text<br />print 'The second note is:', response.notes[1].text
實現 get_notes
現在把 get_notes() 方法加到 PostService 類中:
import datetime<br />from protorpc import remote<br />class PostService(remote.Service):<br /> ...<br /> @remote.remote(GetNotesRequest, Notes)<br /> def get_notes(self, request):<br /> query = guestbook.Greeting.all().order('-date')<br /> if request.on_or_before:<br /> when = datetime.datetime.utcfromtimestamp(<br /> request.on_or_before)<br /> query.filter('date <=', when)<br /> notes = []<br /> for note_model in query.fetch(request.limit):<br /> if note_model.date:<br /> when = int(time.mktime(note_model.date.utctimetuple()))<br /> else:<br /> when = None<br /> note = Note(text=note_model.content, when=when)<br /> notes.append(note)<br /> if request.order == GetNotesRequest.Order.TEXT:<br /> notes.sort(key=lambda note: note.text)<br /> return Notes(notes=notes)