我們?cè)趯?shí)現(xiàn)客戶端上已經(jīng)花了大量的工作。最新版本的(2.0)客戶端使用了Transports,Protocols和Protocol Factories,即整個(gè)Twisted的網(wǎng)絡(luò)框架。但仍有大的改進(jìn)空間。2.0版本的客戶端只能在命令行里下載詩歌。這是因?yàn)镻oetryClientFactory不僅要下載詩歌還要負(fù)責(zé)在下載完畢后關(guān)閉程序。但這對(duì)于"PoetryClientFactory"的確是一項(xiàng)分外的工作,因?yàn)樗俗龊蒙梢粋€(gè)PoetryProtocol的實(shí)例和收集下載完畢的詩歌的工作外最好什么也別做。
我需要一種方式來將詩歌傳給開始時(shí)請(qǐng)求它的函數(shù)。在同步程序中我們會(huì)聲明這樣的API:
def get_poetry(host, post):
"""Return a poem from the poetry server at the given host and port."""
當(dāng)然了,我們不能這樣做。詩歌在沒有全部下載完前上面的程序是需要被阻塞的,否則的話,就無法按照上面的描述那樣去工作。但是這是一個(gè)交互式的程序,因此對(duì)于阻塞在socket是不會(huì)允許的。我們需要一種方式來告訴調(diào)用者何時(shí)詩歌下載完畢,無需在詩歌傳輸過程中將其阻塞。這恰好又是Twisted要解決的問題。Twisted需要告訴我們的代碼何時(shí)socket上可以讀寫、何時(shí)超時(shí)等等。我們前面已經(jīng)看到Twisted使用回調(diào)機(jī)制來解決問題。因此,我們也可以使用回調(diào):
def get_poetry(host, port, callback):
"""
Download a poem from the given host and port and invoke
callback(poem)
when the poem is complete.
"""
現(xiàn)在我們有一個(gè)可以與Twisted一起使用的異步API,剩下的工作就是來實(shí)現(xiàn)它了。
前面說過,我們有時(shí)會(huì)采用非Twisted的方式來寫我們的程序。這是一次。你會(huì)在第七和八部分看到真正的Twisted方式(當(dāng)然,它使用了抽象)。先簡單點(diǎn)講更晚讓大家明白其機(jī)制。
可以在twisted-client-3/get-poetry.py看到3.0版本。這個(gè)版本實(shí)現(xiàn)了get_poetry方法:
def get_poetry(host, port, callback):
from twisted.internet import reactor
factory = PoetryClientFactory(callback)
reactor.connectTCP(host, port, factory)
這個(gè)版本新的變動(dòng)就是將一個(gè)回調(diào)函數(shù)傳遞給了PoetryClientFactory。這個(gè)Factory用這個(gè)回調(diào)來將下載完畢的詩歌傳回去。
class PoetryClientFactory(ClientFactory):
protocol = PoetryProtocol
def __init__(self, callback):
self.callback = callback
def poem_finished(self, poem):
self.callback(poem)
值得注意的是,這個(gè)版本中的工廠因其不用負(fù)責(zé)關(guān)閉reactor而比2.0版本的簡單多了。它也將處理連接失敗的工作除去了,后面我們會(huì)改正這一點(diǎn)。PoetryProtocol無需進(jìn)行任何變動(dòng),我們就直接復(fù)用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)
通過這一變動(dòng),get_poetry,PoetryClientFactory與PoetryProtocol類都完全可以復(fù)用了。它們都僅僅與詩歌下載有關(guān)。所有啟動(dòng)與關(guān)閉reactor的邏輯都在main中實(shí)現(xiàn):
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
因此,只要我們需要,就可以將這些可復(fù)用部分放在任何其它想實(shí)現(xiàn)下載詩歌功能的模塊中。
順便說一句,當(dāng)你測(cè)試3.0版本客戶端時(shí),可以重新配置詩歌下載服務(wù)器來使用詩歌下載的快點(diǎn)。現(xiàn)在客戶端下載的速度就不會(huì)像前面那樣讓人"應(yīng)接不暇"了。
我們可以用圖11來形象地展示回調(diào)的整個(gè)過程:
http://wiki.jikexueyuan.com/project/twisted-intro/images/p06_reactor-poem-callback.png" alt="" />
圖11是值得好好思考一下的。到現(xiàn)在為止,我們已經(jīng)完整描繪了一個(gè)一直到向我們的代碼發(fā)出信號(hào)的整個(gè)回調(diào)鏈條。但當(dāng)你用Twisted寫程序時(shí),或其它交互式的系統(tǒng)時(shí),這些回調(diào)中會(huì)包含一些我們的代碼來回調(diào)其它的代碼。換句話說,交互式的編程方式不會(huì)在我們的代碼處止步(Dave的意思是說,我們的回調(diào)函數(shù)中可能還會(huì)回調(diào)其它別人實(shí)現(xiàn)的代碼,即交互方式不會(huì)止步于我們的代碼,這個(gè)方式會(huì)繼續(xù)深入到框架的代碼或其它第三方的代碼)。
當(dāng)你在選擇Twisted實(shí)現(xiàn)你的工程時(shí),務(wù)必記住下面這幾條。當(dāng)你作出決定:
I'm going to use Twisted!
即代表你已經(jīng)作出這樣的決定:
我將程序構(gòu)建在reacotr驅(qū)動(dòng)的一系列異步回調(diào)鏈上
現(xiàn)在也許你還不會(huì)像我一樣大聲地喊出,但它確實(shí)是這樣的,這就是Twisted的工作方式。
貌似大部分Python程序與Python模塊都是同步的。如果我們正在寫一個(gè)同樣需要下載詩歌的同步方式的程序,我可能會(huì)通過在我們的代碼中添加下面幾句來實(shí)現(xiàn)同步方式下載詩歌的客戶端版本:
...
import poetrylib # I just made this module name up
poem = poetrylib.get_poetry(host, port)
...
然后繼續(xù)我們的程序。如果我們決定不需要這個(gè)這業(yè)務(wù),那我們可以將這幾行代碼去掉就OK了。如果我們要用Twisted版本的get_poetry來實(shí)現(xiàn)同步程序,那么我們需要對(duì)異步方式中的回調(diào)進(jìn)行大的改寫。這里,我并不想說改寫程序不好。而是想說,簡單地將同步與異步的程序混合在一起是不行的。
如果你是一個(gè)Twisted新手或初次接觸異步編程,建議你在試圖復(fù)用其它異步代碼時(shí)先寫點(diǎn)異步Twisted的程序。這樣你不用去處理因需要考慮各個(gè)模塊交互關(guān)系而帶來的復(fù)雜情況下,感受一下Twisted的運(yùn)行機(jī)制。
如果你的程序原來就是異步方式,那么使用Twisted就再好不過了。Twisted與pyGTK和pyQT這兩個(gè)基于reactor的GUI工具包實(shí)現(xiàn)了很好的可交互性。
在版本3.0中,我們沒有去檢測(cè)與服務(wù)器的連接失敗的情況,這比在1.0版本中出現(xiàn)時(shí)帶來的麻煩多得多。如果我們讓3.0版本的客戶端連到一個(gè)不存在的服務(wù)器上下載詩歌,那么不是像1.0版本那樣立刻程序崩潰掉而是永遠(yuǎn)處于等待狀態(tài)中。clientConncetionFailed回調(diào)仍然會(huì)被調(diào)用,但是因?yàn)槠湓贑lientFactory基類中什么也沒有實(shí)現(xiàn)(若子類沒有重寫基類函數(shù)則使用基類的函數(shù))。因此,got_poem回調(diào)將永遠(yuǎn)不會(huì)被激活,這樣一來,reactor也不會(huì)停止了。我們已經(jīng)在第2部分也遇到過這樣一個(gè)不做任何事情的函數(shù)了。
因此,我們需要解決這一問題,在哪兒解決呢?連接失敗的信息會(huì)通過clientConnectionFailed函數(shù)傳遞給工廠對(duì)象,因此我們就從這個(gè)函數(shù)入手。但這個(gè)工廠是需要設(shè)計(jì)成可復(fù)用的,因此如何合理處理這個(gè)錯(cuò)誤是依賴于工廠所使用的場景的。在一些應(yīng)用中,丟失詩歌是很糟糕的;但另外一些應(yīng)用場景下,我們只是盡量嘗試,不行就從其它地方下載 。換句話說,使用get_poetry的人需要知道會(huì)在何時(shí)出現(xiàn)這種問題,而不僅僅是什么情況下會(huì)正常運(yùn)行。在一個(gè)同步程序中,get_poetry可能會(huì)拋出一個(gè)異常并調(diào)用含有try/excep表達(dá)式的代碼來處理異常。但在一個(gè)異步交互的程序中,錯(cuò)誤信息也必須異步的傳遞出去??傊谌〉胓et_poetry之前,我們是不會(huì)發(fā)現(xiàn)連接失敗這種錯(cuò)誤的。下面是一種可能:
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.
"""
通過檢查回調(diào)函數(shù)的參數(shù)來判斷我們是否已經(jīng)完成詩歌下載。這樣可能會(huì)避免客戶端無休止運(yùn)行下去的情況發(fā)生,但這樣做仍會(huì)帶來一些問題。首先,使用None來表示失敗好像有點(diǎn)牽強(qiáng)。一些異步的API可能會(huì)將None而不是錯(cuò)誤狀態(tài)字作為默認(rèn)返回值。其次,None值所攜帶的信息量太少。它不能告訴我們出的什么錯(cuò),更不用說可以在調(diào)試中為我呈現(xiàn)出一個(gè)跟蹤對(duì)象了。好的,也可以嘗試這樣:
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已經(jīng)比較接近于我們的異步程序了。現(xiàn)在我們可以通過得到Exception來獲得相比得到一個(gè)None多的多的出錯(cuò)信息了。正常情況下,在Python中遇到一個(gè)異常會(huì)得到一個(gè)跟蹤異常棧以讓我們來分析,或是為了日后的調(diào)試而打印異常信息日志。跟蹤棧相當(dāng)重要的,因此我們不能因?yàn)槭褂卯惒骄幊叹蛯⑵鋪G棄。
記住,我們并不想在回調(diào)激活的地反打印跟蹤棧,那并不是出問題的地方。我們想得到是Exception實(shí)例和其被拋出的位置。
Twisted含有一個(gè)抽象類稱作Failure,如果有異常出現(xiàn)的話,其能捕獲Exception與跟蹤棧。
Failure的描述文檔說明了如何創(chuàng)建它。將一個(gè)Failure對(duì)象付給回調(diào)函數(shù),我們就可以為以后的調(diào)試保存跟蹤棧的信息了。
在twisted-failure/failure-examples.py中有一些使用Failure對(duì)象的示例代碼。它演示了Failure是如何從一個(gè)拋出的異常中保存跟蹤棧信息的,即使在except塊外部。我不用在創(chuàng)建一個(gè)Failure上花太多功夫。在第七部分中,我們將看到Twisted如何為我們完成這些工作。好了,看看下面這個(gè)嘗試:
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.
"""
在這個(gè)版本中,我們得到了Exception和出現(xiàn)問題時(shí)的跟蹤棧。這已經(jīng)很不錯(cuò)了!
大多數(shù)情況下,到這個(gè)就OK了,但我們?cè)?jīng)遇到過另外一個(gè)問題。使用相同的回調(diào)來處理正常的與不正常的結(jié)果是一件莫名奇妙的事。通常情況下,我們?cè)谔幚硎⌒畔⒑统晒π畔⒁M(jìn)行不同的操作。在同步Python編程中,我們經(jīng)常在處理失敗與成功兩種信息上采用不同的處理路徑,即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
如果我們想保留這種錯(cuò)誤處理方式,那么我們需要獨(dú)立的代碼來處理錯(cuò)誤信息。那么在異步方式中,這就意味著一個(gè)獨(dú)立的回調(diào):
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實(shí)現(xiàn)位于twisted-client-3/get-poetry-1.py。改變是很直觀的。PoetryClientFactory,獲得了callback和errback兩個(gè)回調(diào),并且其中我們實(shí)現(xiàn)了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)
由于clientConnectionFailed已經(jīng)收到一個(gè)Failure對(duì)象(其作為reason參數(shù))來解釋為什么會(huì)發(fā)生連接失敗,我們直接將其交給了errback回調(diào)函數(shù)。 直接運(yùn)行3.1版本(無需開啟詩歌下載服務(wù))的代碼:
python twisted-client-3/get-poetry-1.py 10004
你會(huì)得到如下輸出:
Poem failed: [Failure instance: Traceback (failure with no frames)::
Connection was refused by other side: 111: Connection refused. ]
這是由poem_failed回調(diào)中的print函數(shù)打印出來的。在這個(gè)例子中,Twisted只是簡單將一個(gè)Exception傳遞給了我們而沒有拋出它,因此這里我們并沒有看到跟蹤棧。因?yàn)檫@并不一個(gè)Bug,所以跟蹤棧也不需要,Twisted只是想通知我們連接出錯(cuò)。
我們?cè)诘诹糠謱W(xué)到:
使用Twisted時(shí),難道我們?cè)趯懽约旱腁PI時(shí)都要額外的加上兩個(gè)參數(shù):正常的回調(diào)與出現(xiàn)錯(cuò)誤時(shí)的回調(diào)? 幸運(yùn)的是,Twisted使用了一種機(jī)制來解決了這一問題,我們將在第七部分學(xué)習(xí)這部分內(nèi)容。
本部分原作參見: dave @ http://krondo.com/?p=1595
本部分翻譯內(nèi)容參見楊曉偉的博客 http://blog.sina.com.cn/s/blog_704b6af70100q4by.html