轉載:作者:dave@http://krondo.com/slow-poetry-and-the-apocalypse/ 譯者:楊曉偉(采用意譯)
打造可以復用的詩歌下載客戶端
我們在實現客戶端上已經花了大量的工作。最新版本的(2.0)客戶端使用了Transports,Protocols和Protocol Factories,即整個Twisted的網絡框架。但仍有大的改進空間。2.0版本的客戶端只能在命令行里下載詩歌。這是因為PoetryClientFactory不僅要下載詩歌還要負責在下載完畢后關閉程序。但這對于”PeotryClientFactory“的確是一項分外的工作,因為它除了做好生成一個PoetryProtocol的實例和收集下載完畢的詩歌的工作外最好什么也別做。
我需要一種方式來將詩歌傳給開始時請求它的函數。在同步程序中我們會聲明這樣的API:
|
1
2
|
def
get_poetry(host, post):
"""Return a poem from the poetry server at the given host and port."""
|
當然了,我們不能這樣做。詩歌在沒有全部下載完前上面的程序是需要被阻塞的,否則的話,就無法按照上面的描述那樣去工作。但是這是一個交互式的程序,因此對于阻塞在socket是不會允許的。我們需要一種方式來告訴調用者何時詩歌下載完畢,無需在詩歌傳輸過程中將其阻塞。這恰好又是Twisted要解決的問題。Twisted需要告訴我們的代碼何時socket上可以讀寫、何時超時等等。我們前面已經看到Twisted使用回調機制來解決問題。因此,我們也可以使用回調:
|
1
2
3
4
5
6
|
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
可以在<tt style="margin: 0px; padding: 0px;">twisted-client-3/get-poetry.py</tt>看到3.0版本。這個版本實現了get_poetry方法:
|
1
2
3
4
|
def
get_poetry(host, port, callback):
from
twisted.internet ``import
reactor
factory ``=
PoetryClientFactory(callback)
reactor.connectTCP(host, port, factory)
|
這個版本新的變動就是將一個回調函數傳遞給了PoetryClientFactory。這個Factory用這個回調來將下載完畢的詩歌傳回去。
|
1
2
3
4
5
6
7
|
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版本的:
|
1
2
3
4
5
6
7
8
9
10
|
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中實現:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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模塊都是同步的。如果我們正在寫一個同樣需要下載詩歌的同步方式的程序,我可能會通過在我們的代碼中添加下面幾句來實現我們的同步方式的下載詩歌客戶端版本:
|
1
2
3
4
|
...
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之前,我們是不會發現連接失敗這種錯誤的。下面是一種可能:
|
1
2
3
4
5
6
7
8
|
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值所攜帶的信息量太少。它不能告訴我們出的什么錯,更不說可以在調試中為我呈現出一個跟蹤對象了。好的,也可以嘗試這樣:
|
1
2
3
4
5
6
7
8
9
|
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對象付給回調函數,我們就可以為以后的調試保存跟蹤棧的信息了。
在<tt style="margin: 0px; padding: 0px;">twisted-failure/failure-examples.py</tt>中有一些使用Failure對象的示例代碼。它演示了Failure是如何從一個拋出的異常中保存跟蹤棧信息的,即使在except塊外部。我不用在創建一個Failure上花太多功夫。在第七部分中,我們將看到Twisted如何為我們完成這些工作。好了,看看下面這個嘗試:
|
1
2
3
4
5
6
7
8
|
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處理方式:
|
1
2
3
4
5
6
|
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
|
如果我們想保留這種錯誤處理方式,那么我們需要獨立的代碼來處理錯誤信息。那么在異步方式中,這就意味著一個獨立的回調:
|
1
2
3
4
5
6
7
8
|
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](http://github.com/jdavisp3/twisted-intro/blob/master/twisted-client-3/get-poetry-1.py)
。改變是很直觀的。PoetryClientFactory,獲得了callback和errback兩個回調,并且其中我們實現了clientConnectFailed:
|
1
2
3
4
5
6
7
8
9
|
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版本(無需開啟詩歌下載服務)的代碼你會得到如下輸出:
|
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使用了一種機制來解決了這一問題,我們將在第七部分學習這部分內容。