[Twisted] Part 13: Deferred All The Way Down
本文由Dave的Part 13: Deferred All The Way Down翻譯而成,你可以由Part 1開始閱讀這個系列的文章,也可以在這裡找到整個系列的目錄。
現在我們想要建立一個使用我們在Part 12寫的網路詩歌轉換服務的新的用戶端。但是有個小問題:由於轉換服務是在網路上存取的,所以我們需要使用非同步I/O。這表示我們用於請求轉換的API也必須是非同步的。換句話說,在我們新的用戶端中,try_to_cummingsify callback會回傳Deferred實例。
那麼當一個在deferred實例的鏈中的callback回傳了另一個deferred時會發生什麼事情?讓我們稱第一個deferred為「外部(outer)」deferred,第二個deferred為「內部(inner)」deferred。假設在外部deferred的callback N回傳了內部deferred。而這個callback說「我是非同步的,我要的結果還沒出現」。由於外部deferred需要以現在callback或errback的結果來呼叫鏈中的下一個callback或errback,所以外部deferred需要等待直到內部deferred被觸發為止。當然,外部deferred也不能阻塞,所以外部deferred會暫停callback鏈的執行,並返回控制權給reactor。
外部deferred如何知道什麼時候恢復執行呢?很簡單,在內部deferred中加入一對callback/errback。如此,當內部deferred被觸發,外部deferred會恢復執行它自己的鏈(譯註:過去我們講到deferred的觸發有點像「開始執行」的意思,實際上觸發的意思應該是「開始執行,並根據callback/errback鏈傳出結果」的這整個過程,只是過去比較不強調傳出結果這部分,而這邊內部deferred的觸發就必須記得這點才比較好理解文章中想表達的意思)。如果內部deferred執行成功(即它會呼叫外部deferred添加的callback),則外部deferred會以內部deferred的成功結果來呼叫callback N+1。如果內部deferred失敗(呼叫外部deferred添加的errback),外部deferred會以內部deferred的失敗結果來呼叫errback N+1。
有很多東西要消化,所以讓我們在圖28中說明這個想法:
在這張圖中,外部deferred有四對callback/errback。當外部deferred觸發時,鏈中的第一個callback回傳一個deferred實例(內部deferred)。此時,外部deferred會停止繼續觸發它的鏈,並返回控制權給reactor(在對內部deferred加入一對callback/errback後)。一段時間後,內部deferred觸發並解執行完畢callback/errback鏈,外部deferred接著恢復執行它的callback鏈。注意到外部deferred本身不會觸發內部deferred。這是不可能的事情,因為外部deferred不知道內部deferred何時會有結果可以用,或者是什麼結果(譯註:也就是說外部deferred並不知道內部deferred什麼時候會執行完畢,也不知道執行結果是正常或者失敗)。相反的,外部deferred只是單純的等待著(非同步的)內部deferred的啟動。
注意到圖28中外部deferred的callback連接到內部deferred的線是黑色的,而不是綠色或紅色。那是因為我們不知道外部deferred的callback是成功還是失敗,直到內部deferred被觸發。只有內部deferred被觸發後,外部deferred才能決定要呼叫我們鏈中的下一個callback還是下一個errback。
圖29從reactor的角度顯示圖28中外部與內部deferred觸發的順序:
這可能是Deferred類別中最複雜的功能,所以如果你需要一點時間來吸收這些知識,請不要擔心。我們會使用twisted-deferred/defer-10.py的範例碼再次說明它。這個範例建立了兩個外部deferreds,一個使用普通的callback,另一個的其中一個callback回傳了內部deferred。透過研究程式碼與輸出,你可以看到當回傳內部deferred時,第二個外部deferred如何停止執行它的鏈,然後在內部deferred觸發時再次啟動。
使用NetstringReceiver作為父類別使得這個協定可以很簡單的實現。一旦建立連接,我們會向伺服器送出轉換請求,以及從我們的工廠中取回轉換名稱與要轉換的詩歌。當我們的用戶端收到詩歌時,我們會傳給工廠進行處理。這是工廠類別的程式碼:
這個工廠是為用戶端設計的並且處理單一轉換請求,同時儲存供協定實現使用的轉換名稱與詩歌。這個工廠類別建立了一個Deferred,用來代表轉換請求的結果。注意工廠類別如何處理兩種錯誤情況:連接失敗與連接在接收詩歌完成前被關閉。同時注意即使我們收到詩歌,clientConnectionLost也會被呼叫,但在這種狀況下,因為有handlePoem方法,self.deferred會被設定為None。
這個工廠類別建立了Deferred實例也觸發了它,在Twisted程式設計中這是個值得遵守的好規則,所以讓我們強調一下這點:
這個「你創造它,你觸發它」的規則有助於確保給定的deferred實例只被觸發一次,並且使它更容易遵循Twisted程式中的控制流程。
除了轉換用的工廠類別之外,還有一個Proxy類別隱藏了向特定轉換伺服器建立TCP連接的細節:
這個類別提供了一個xform()介面,讓其他程式碼可以用它來請求轉換。這樣其他的程式碼就可以請求轉換並解得到deferred實例,不用在主機名稱與埠號上浪費時間。
除了try_to_cummingsify callback之外,剩下的程式都沒改變:
這個外層deferred的callback現在回傳了一個deferred,但我們完全不必改變main函式的其他部分,除了建立Proxy實例。由於try_to_cummingsify已經是deferred callback/errback鏈(get_poetry回傳的deferred實例)的一部分,所以它已經以非同步的方式使用了,並且其他任何東西都不需要更改。
你會注意到我們回傳了d.addErrback(fail)的結果,這只是一個小小的syntactic sugar(糖衣語法)。addCallback與addErrback方法回傳了原始的deferred,我們也可以寫成:
第一個版本也是一樣的意思,只是比較短。
要下載兩首詩歌並轉換它們,你可以像這樣啟動轉換伺服器:
以及像這樣啟動詩歌伺服器:
然後你就可以向之前說的那樣執行詩歌用戶端。之後,試試中止轉換伺服器並以相同的指令重新執行用戶端。
我們知道所有關於deferred的事情了嗎?還沒有,還有一個重要的功能要討論,但我們會把它留給Part 14。
Introduction
回想一下Part 10中的5.1版本用戶端。用戶端使用Deferred實例來管理包括呼叫詩歌轉換引擎的callback鏈。在5.1版本用戶端中,引擎的實現是在用戶端本身由一個同步函式來呼叫。現在我們想要建立一個使用我們在Part 12寫的網路詩歌轉換服務的新的用戶端。但是有個小問題:由於轉換服務是在網路上存取的,所以我們需要使用非同步I/O。這表示我們用於請求轉換的API也必須是非同步的。換句話說,在我們新的用戶端中,try_to_cummingsify callback會回傳Deferred實例。
那麼當一個在deferred實例的鏈中的callback回傳了另一個deferred時會發生什麼事情?讓我們稱第一個deferred為「外部(outer)」deferred,第二個deferred為「內部(inner)」deferred。假設在外部deferred的callback N回傳了內部deferred。而這個callback說「我是非同步的,我要的結果還沒出現」。由於外部deferred需要以現在callback或errback的結果來呼叫鏈中的下一個callback或errback,所以外部deferred需要等待直到內部deferred被觸發為止。當然,外部deferred也不能阻塞,所以外部deferred會暫停callback鏈的執行,並返回控制權給reactor。
外部deferred如何知道什麼時候恢復執行呢?很簡單,在內部deferred中加入一對callback/errback。如此,當內部deferred被觸發,外部deferred會恢復執行它自己的鏈(譯註:過去我們講到deferred的觸發有點像「開始執行」的意思,實際上觸發的意思應該是「開始執行,並根據callback/errback鏈傳出結果」的這整個過程,只是過去比較不強調傳出結果這部分,而這邊內部deferred的觸發就必須記得這點才比較好理解文章中想表達的意思)。如果內部deferred執行成功(即它會呼叫外部deferred添加的callback),則外部deferred會以內部deferred的成功結果來呼叫callback N+1。如果內部deferred失敗(呼叫外部deferred添加的errback),外部deferred會以內部deferred的失敗結果來呼叫errback N+1。
有很多東西要消化,所以讓我們在圖28中說明這個想法:
在這張圖中,外部deferred有四對callback/errback。當外部deferred觸發時,鏈中的第一個callback回傳一個deferred實例(內部deferred)。此時,外部deferred會停止繼續觸發它的鏈,並返回控制權給reactor(在對內部deferred加入一對callback/errback後)。一段時間後,內部deferred觸發並解執行完畢callback/errback鏈,外部deferred接著恢復執行它的callback鏈。注意到外部deferred本身不會觸發內部deferred。這是不可能的事情,因為外部deferred不知道內部deferred何時會有結果可以用,或者是什麼結果(譯註:也就是說外部deferred並不知道內部deferred什麼時候會執行完畢,也不知道執行結果是正常或者失敗)。相反的,外部deferred只是單純的等待著(非同步的)內部deferred的啟動。
注意到圖28中外部deferred的callback連接到內部deferred的線是黑色的,而不是綠色或紅色。那是因為我們不知道外部deferred的callback是成功還是失敗,直到內部deferred被觸發。只有內部deferred被觸發後,外部deferred才能決定要呼叫我們鏈中的下一個callback還是下一個errback。
圖29從reactor的角度顯示圖28中外部與內部deferred觸發的順序:
這可能是Deferred類別中最複雜的功能,所以如果你需要一點時間來吸收這些知識,請不要擔心。我們會使用twisted-deferred/defer-10.py的範例碼再次說明它。這個範例建立了兩個外部deferreds,一個使用普通的callback,另一個的其中一個callback回傳了內部deferred。透過研究程式碼與輸出,你可以看到當回傳內部deferred時,第二個外部deferred如何停止執行它的鏈,然後在內部deferred觸發時再次啟動。
Client 6.0
讓我們使用巢狀deferreds的新知識,並且重新實現我們的詩歌用戶端,以使用Part 12的網路轉換服務。你可以在twisted-client-6/get-poetry.py找到程式碼。詩歌協定與工廠類別(譯註:PoetryProtocol與PoetryClientFactory)與之前的版本相比並沒有改變。但是現在我們有了處理轉換請求的協定與工廠類別(譯註:TransformClientProtocol與TransformClientFactory)。這裡是轉換用戶端的協定類別:class TransformClientProtocol(NetstringReceiver):
def connectionMade(self):
self.sendRequest(self.factory.xform_name, self.factory.poem)
def sendRequest(self, xform_name, poem):
self.sendString(xform_name + '.' + poem)
def stringReceived(self, s):
self.transport.loseConnection()
self.poemReceived(s)
def poemReceived(self, poem):
self.factory.handlePoem(poem)
使用NetstringReceiver作為父類別使得這個協定可以很簡單的實現。一旦建立連接,我們會向伺服器送出轉換請求,以及從我們的工廠中取回轉換名稱與要轉換的詩歌。當我們的用戶端收到詩歌時,我們會傳給工廠進行處理。這是工廠類別的程式碼:
class TransformClientFactory(ClientFactory):
protocol = TransformClientProtocol
def __init__(self, xform_name, poem):
self.xform_name = xform_name
self.poem = poem
self.deferred = defer.Deferred()
def handlePoem(self, poem):
d, self.deferred = self.deferred, None
d.callback(poem)
def clientConnectionLost(self, _, reason):
if self.deferred is not None:
d, self.deferred = self.deferred, None
d.errback(reason)
clientConnectionFailed = clientConnectionLost
這個工廠是為用戶端設計的並且處理單一轉換請求,同時儲存供協定實現使用的轉換名稱與詩歌。這個工廠類別建立了一個Deferred,用來代表轉換請求的結果。注意工廠類別如何處理兩種錯誤情況:連接失敗與連接在接收詩歌完成前被關閉。同時注意即使我們收到詩歌,clientConnectionLost也會被呼叫,但在這種狀況下,因為有handlePoem方法,self.deferred會被設定為None。
這個工廠類別建立了Deferred實例也觸發了它,在Twisted程式設計中這是個值得遵守的好規則,所以讓我們強調一下這點:
一般來說,一個建立Deferred實例的物件也應該負責觸發這個Deferred。
這個「你創造它,你觸發它」的規則有助於確保給定的deferred實例只被觸發一次,並且使它更容易遵循Twisted程式中的控制流程。
除了轉換用的工廠類別之外,還有一個Proxy類別隱藏了向特定轉換伺服器建立TCP連接的細節:
class TransformProxy(object):
"""
I proxy requests to a transformation service.
"""
def __init__(self, host, port):
self.host = host
self.port = port
def xform(self, xform_name, poem):
factory = TransformClientFactory(xform_name, poem)
from twisted.internet import reactor
reactor.connectTCP(self.host, self.port, factory)
return factory.deferred
這個類別提供了一個xform()介面,讓其他程式碼可以用它來請求轉換。這樣其他的程式碼就可以請求轉換並解得到deferred實例,不用在主機名稱與埠號上浪費時間。
除了try_to_cummingsify callback之外,剩下的程式都沒改變:
def try_to_cummingsify(poem):
d = proxy.xform('cummingsify', poem)
def fail(err):
print >>sys.stderr, 'Cummingsify failed!'
return poem
return d.addErrback(fail)
這個外層deferred的callback現在回傳了一個deferred,但我們完全不必改變main函式的其他部分,除了建立Proxy實例。由於try_to_cummingsify已經是deferred callback/errback鏈(get_poetry回傳的deferred實例)的一部分,所以它已經以非同步的方式使用了,並且其他任何東西都不需要更改。
你會注意到我們回傳了d.addErrback(fail)的結果,這只是一個小小的syntactic sugar(糖衣語法)。addCallback與addErrback方法回傳了原始的deferred,我們也可以寫成:
d.addErrback(fail)
return d
第一個版本也是一樣的意思,只是比較短。
TESTING OUT THE CLIENT
新用戶端的語法有些不同。如果你在10001埠執行轉換服務,並在10002與10003埠執行兩個詩歌伺服器,你可以這樣執行:python twisted-client-6/get-poetry.py 10001 10002 10003
要下載兩首詩歌並轉換它們,你可以像這樣啟動轉換伺服器:
python twisted-server-1/transformedpoetry.py --port 10001
以及像這樣啟動詩歌伺服器:
python twisted-server-1/fastpoetry.py --port 10002 poetry/fascination.txt
python twisted-server-1/fastpoetry.py --port 10003 poetry/science.txt
然後你就可以向之前說的那樣執行詩歌用戶端。之後,試試中止轉換伺服器並以相同的指令重新執行用戶端。
Wrapping Up
在這個章節中,我們學習了deferreds如何透明的處理callback鏈中的其他deferreds,因此我們可以安全的在「外部」deferred中加入非同步callbacks,而不用擔心細節的部分。這很方便,因為許多我們的函式最後都會成為非同步的。我們知道所有關於deferred的事情了嗎?還沒有,還有一個重要的功能要討論,但我們會把它留給Part 14。
Suggested Exercises
- 修改用戶端讓我們可以透過名稱要求特定種類的轉換。
- 修改用戶端讓轉換伺服器的位址是一個可選的引數。如果沒有提供就跳過轉換步驟。
- PoetryClientFactory目前違反了deferreds的「你創造它,你觸發它」的規則。重構get_poetry和PoetryClientFactory來解決這個問題。
- 雖然我們沒有演示,但errback回傳deferred實例的狀況是對稱的。修改twisted-deferred/defer-10.py範例來驗證它。
- 在Deferred的實現中找到處理callback/errback回傳另一個Deferred這種狀況的地方。
留言
張貼留言