第六部分:抽象地利用Twisted
第六部分:抽象地利用Twisted
你可以從這裡從頭開始閱讀這個系列。
打造可以複用的詩歌下載客戶端
我們在實現客戶端上已經花了大量的工作。最新版本的(2.0)客戶端使用了Transports,Protocols和Protocol Factories,即整個Twisted的網路框架。但仍有大的改進空間。2.0版本的客戶端只能在命令列裡下載詩歌。這是因為PoetryClientFactory不僅要下載詩歌還要負責在下載完畢後關閉程式。但這對於”PeotryClientFactory“的確是一項分外的工作,因為它除了做好生成一個PoetryProtocol的例項和收集下載完畢的詩歌的工作外最好什麼也別做。
我需要一種方式來將詩歌傳給開始時請求它的函式。在同步程式中我們會宣告這樣的API:
def get_poetry(host, post):
"""Return a poem from the poetry server at the given host and port."""
當然了,我們不能這樣做。詩歌在沒有全部下載完前上面的程式是需要被阻塞的,否則的話,就無法按照上面的描述那樣去工作。但是這是一個互動式的程式,因此對於阻塞在socket是不會允許的。我們需要一種方式來告訴呼叫者何時詩歌下載完畢,無需在詩歌傳輸過程中將其阻塞。這恰好又是Twisted
def get_poetry(host, port, callback):
"""
Download a poem from the given host and port and invoke
callback(poem)
when the poem is complete.
"""
現在我們有一個可以與Twisted一起使用的非同步API,剩下的工作就是來實現它了。
前面說過,我們有時會採用非Twisted的方式來寫我們的程式。這是一次。你會在第七和八部分看到真正的Twisted方式(當然,它使用了抽象)。先簡單點講更晚讓大家明白其機制。
客戶端3.0
可以在twisted-client-3/get-poetry.py看到3.0版本。這個版本實現了get_poetry方法:
def get_poetry(host, port, callback):
from twisted.internet import reactor
factory = PoetryClientFactory(callback)
reactor.connectTCP(host, port, factory)
這個版本新的變動就是將一個回撥函式傳遞給了PoetryClientFactory。這個Factory用這個回撥來將下載完畢的詩歌傳回去。
class PoetryClientFactory(ClientFactory):
protocol = PoetryProtocol
def __init__(self, callback):
self.callback = callback
def poem_finished(self, poem):
self.callback(poem)
值得注意的是,這個版本中的工廠因其不用負責關閉reactor而比2.0版本的簡單多了。它也將處理連線失敗的工作除去了,後面我們會改正這一點。PoetryProtocol無需進行任何變動,我們就直接複用2.1版本的:
class PoetryProtocol(Protocol):
poem = ''
def dataReceived(self, data):
self.poem += data
def connectionLost(self, reason):
self.poemReceived(self.poem)
def poemReceived(self, poem):
self.factory.poem_finished(poem)
通過這一變動,get_poetry,PoetryClientFactory與PoetryProtocol類都完全可以複用了。它們都僅僅與詩歌下載有關。所有啟動與關閉reactor的邏輯都在main中實現:
def poetry_main():
addresses = parse_args()
from twisted.internet import reactor
poems = []
def got_poem(poem):
poems.append(poem)
if len(poems) == len(addresses):
reactor.stop()
for address in addresses:
host, port = address
get_poetry(host, port, got_poem)
reactor.run()
for poem in poems:
print poem
因此,只要我們需要,就可以將這些可複用部分放在任何其它想實現下載詩歌功能的模組中。
順便說一句,當你測試3.0版本客戶端時,可以重配置詩歌下載伺服器來使用詩歌下載的快點。現在客戶端下載的速度就不會像前面那樣讓人”應接不暇“了。
討論
我們可以用圖11來形象地展示回撥的整個過程:
圖10 :回撥過程
圖11是值得好好思考一下的。到現在為止,我們已經完整描繪了一個一直到向我們的程式碼發出訊號的整個回撥鏈條。但當你用Twisted寫程式時,或其它互動式的系統時,這些回撥中會包含一些我們的程式碼來回調其它的程式碼。換句話說,互動式的程式設計方式不會在我們的程式碼處止步(Dave的意思是說,我們的回撥函式中可能還會回撥其它別人實現的程式碼,即互動方式不會止步於我們的程式碼,這個方式會繼續深入到框架的程式碼或其它第三方的程式碼)。
當你在選擇Twisted實現你的工程時,務必記住下面這幾條。當你作出決定:
I'm going to use Twisted!
即代表你已經作出這樣的決定:
我將要構造我的程式如由reactorz牽引的一系列的非同步回撥鏈
現在也許你還不會像我一樣大聲地喊出,但它確實是這樣的。那就是Twisted的工作方式。
貌似大部分Python程式與Python模組都是同步的。如果我們正在寫一個同樣需要下載詩歌的同步方式的程式,我可能會通過在我們的程式碼中新增下面幾句來實現我們的同步方式的下載詩歌客戶端版本:
...
import poetrylib # I just made this module name up
poem = poetrylib.get_poetry(host, port)
...
然後我們繼續。如果我們決定不需要這個這業務那我們可以將這幾行程式碼去掉就OK了。如果我們真的要用Twisted版本的get_poetry來實現同步程式,那麼我們需要對非同步方式中的回撥進行大的改寫。這裡,我並不想說改寫程式不好。而是想說,簡單地將同步與非同步的程式混合在一直是不行的。
如果你是一個Twisted新手或初次接觸非同步程式設計,建議你在試圖複用其它非同步程式碼時先寫點非同步Twisted的程式。這樣你不用去處理因需要考慮各個模組互動關係而帶來的複雜情況下,感受一下Twisted的執行機制。
如果你的程式原來就是非同步方式,那麼使用Twisted就再好不過了。Twisted與pyGTK和pyQT這兩個基於reactor的GUI工具包實現了很好的可互動性。
異常問題的處理
在版本3.0中,我們沒有去檢測與伺服器的連線失敗的情況,這比在1.0版本中出現時帶來的麻多得多。如果我們讓3.0版本的客戶端到一個不存在的伺服器上下載詩歌,那麼不是像1.0版本那樣立刻程式崩潰掉而是永遠處於等待狀態中。clientConncetionFailed回撥仍然會被呼叫,但是因為其在ClientFactory基類中什麼也沒有實現(若子類沒有重寫基類函式則使用基類的函式)。因此,got_poem回撥將永遠不會被啟用,這樣一來,reactor也不會停止了。我們已經在第2部分也遇到過這樣一個不做任何事情的函數了。
因此,我們需要解決這一問題,在哪兒解決呢?連線失敗的資訊會通過clientConnectionFailed函式傳遞給工廠物件,因此我們就從這個函式入手。但這個工廠是需要設計成可複用的,因此如何合理處理這個錯誤是依賴於工廠所使用的場景的。在一些應用中,丟失詩歌是很糟糕的;但另外一些應用場景下,我們只是儘量嘗試,不行就從其它地方下載 。換句話說,使用get_poetry的人需要知道會在何時出現這種問題,而不僅僅是什麼情況下會正常執行。在一個同步程式中,get_poetry可能會丟擲一個異常並呼叫含有try/excep表示式的程式碼來處理異常。但在一個非同步互動的程式中,錯誤資訊也必須非同步的傳遞出去。總之,在取得get_poetry之前,我們是不會發現連線失敗這種錯誤的。下面是一種可能:
def get_poetry(host, port, callback):
"""
Download a poem from the given host and port and invoke
callback(poem)
when the poem is complete. If there is a failure, invoke:
callback(None)
instead.
"""
通過檢查回撥函式的引數來判斷我們是否已經完成詩歌下載。這樣可能會避免客戶端無休止執行下去的情況發生,但這樣做仍會帶來一些問題。首先,使用None來表示失敗好像有點牽強。一些非同步的API可能會將None而不是錯誤狀態字作為預設返回值。其次,None值所攜帶的資訊量太少。它不能告訴我們出的什麼錯,更不說可以在除錯中為我呈現出一個跟蹤物件了。好的,也可以嘗試這樣:
def get_poetry(host, port, callback):
"""
Download a poem from the given host and port and invoke
callback(poem)
when the poem is complete. If there is a failure, invoke:
callback(err)
instead, where err is an Exception instance.
"""
使用Exception已經比較接近於我們的非同步程式了。現在我們可以通過得到Exception來獲得相比得到一個None多的多的出錯資訊了。正常情況下,在Python中遇到一個異常會得到一個跟蹤異常棧以讓我們來分析,或是為了日後的除錯而列印異常資訊日誌。跟蹤棧相當重要的,因此我們不能因為使用非同步程式設計就將其丟棄。
記住,我們並不想在回撥啟用時列印跟蹤棧,那並不是出問題的地方。我們想得到是Exception例項用其被丟擲的位置。
Twisted含有一個抽象類稱作Failure,如果有異常出現的話,其能捕獲Exception與跟蹤棧。
Failure的描述文件說明了如何建立它。將一個Failure物件付給回撥函式,我們就可以為以後的除錯儲存跟蹤棧的資訊了。
在twisted-failure/failure-examples.py中有一些使用Failure物件的示例程式碼。它演示了Failure是如何從一個丟擲的異常中儲存跟蹤棧資訊的,即使在except塊外部。我不用在建立一個Failure上花太多功夫。在第七部分中,我們將看到Twisted如何為我們完成這些工作。好了,看看下面這個嘗試:
def get_poetry(host, port, callback):
"""
Download a poem from the given host and port and invoke
callback(poem)
when the poem is complete. If there is a failure, invoke:
callback(err)
instead, where err is a twisted.python.failure.Failure instance.
"""
在這個版本中,我們得到了Exception和出現問題時的跟蹤棧。這已經很不錯了!
大多數情況下,到這個就OK了,但我們曾經遇到過另外一個問題。使用相同的回撥來處理正常的與不正常的結果是一件莫名奇妙的事。通常情況下,我們在處理失敗資訊進,相比成功資訊要進行不同的操作。在同步Python程式設計中,我們經常在處理失敗與成功兩種資訊上採用不同的處理路徑,即try/except處理方式:
try:
attempt_to_do_something_with_poetry()
except RhymeSchemeViolation:
# the code path when things go wrong
else:
# the code path when things go so, so right baby
如果我們想保留這種錯誤處理方式,那麼我們需要獨立的程式碼來處理錯誤資訊。那麼在非同步方式中,這就意味著一個獨立的回撥:
def get_poetry(host, port, callback, errback):
"""
Download a poem from the given host and port and invoke
callback(poem)
when the poem is complete. If there is a failure, invoke:
errback(err)
instead, where err is a twisted.python.failure.Failure instance.
"""
版本3.1
版本3.1實現位於twisted-client-3/get-poetry-1.py
。改變是很直觀的。PoetryClientFactory,獲得了callback和errback兩個回撥,並且其中我們實現了clientConnectFailed:
class PoetryClientFactory(ClientFactory):
protocol = PoetryProtocol
def __init__(self, callback, errback):
self.callback = callback
self.errback = errback
def poem_finished(self, poem):
self.callback(poem)
def clientConnectionFailed(self, connector, reason):
self.errback(reason)
由於clientConncetFailed已經收到一個Failure物件(其作為reason引數)來解釋為什麼會發生連線失敗,我們直接將其交給了errback回撥函式。直接執行3.1版本(無需開啟詩歌下載服務)的程式碼你會得到如下輸出:
Poem failed: [Failure instance: Traceback (failure with no frames): : Connection was refused by other side: 111: Connection refused. ]
這是由poem_failed回撥中的print函式打印出來的。在這個例子中,Twisted只是簡單將一個Exception傳遞給了我們而沒有丟擲它,因此這裡我們並沒有看到跟蹤棧。因為這並不一個Bug,所以跟蹤棧也不需要,Twisted只是想通知我們連接出錯。
總結:
-
我們在第六部分學到:
-
我們為Twisted程式寫的API必須是非同步的
-
不能將同步與非同步程式碼混合起來使用
-
我們可以在自己的程式碼中寫回調函式,正如Twisted做的那樣
-
並且,我們需要寫處理錯誤資訊的回撥函式
使用Twisted時,難道在寫我們自己的API時都要額外的加上兩個引數:正常的回撥與出現錯誤時的回撥。幸運的是,Twisted使用了一種機制來解決了這一問題,我們將在第七部分學習這部分內容。