打造能夠複用的詩歌下載客戶端html
咱們在實現客戶端上已經花了大量的工做。最新版本的(2.0)客戶端使用了Transports,Protocols和Protocol Factories,即整個Twisted的網絡框架。但仍有大的改進空間。2.0版本的客戶端只能在命令行裏下載詩歌。這是由於PoetryClientFactory不只要下載詩歌還要負責在下載完畢後關閉程序。但這對於」PeotryClientFactory「的確是一項分外的工做,由於它除了作好生成一個PoetryProtocol的實例和收集下載完畢的詩歌的工做外最好什麼也別作。python
我須要一種方式來將詩歌傳給開始時請求它的函數。在同步程序中咱們會聲明這樣的API:react
def get_poetry(host, post): """Return a poem from the poetry server at the given host and port."""
固然了,咱們不能這樣作。詩歌在沒有所有下載完前上面的程序是須要被阻塞的,不然的話,就沒法按照上面的描述那樣去工做。可是這是一個交互式的程序,所以對於阻塞在socket是不會容許的。咱們須要一種方式來告訴調用者什麼時候詩歌下載完畢,無需在詩歌傳輸過程當中將其阻塞。這剛好又是Twisted要解決的問題。Twisted須要告訴咱們的代碼什麼時候socket上能夠讀寫、什麼時候超時等等。咱們前面已經看到Twisted使用回調機制來解決問題。所以,咱們也可使用回調:git
如今咱們有一個能夠與Twisted一塊兒使用的異步API,剩下的工做就是來實現它了。github
前面說過,咱們有時會採用非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用這個回調來將下載完畢的詩歌傳回去。app
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使用了一種機制來解決了這一問題,咱們將在第七部分學習這部份內容。