鍍金池/ 教程/ Python/ 由twisted支持的客戶端
小插曲 Deferred
異步編程模式與Reactor初探
使用Deferred新功能實現(xiàn)新客戶端
由twisted支持的客戶端
增強defer功能的客戶端
改進(jìn)詩歌下載服務(wù)器
測試詩歌
更加"抽象"的運用Twisted
Deferred用于同步環(huán)境
輪子內(nèi)的輪子: Twisted和Erlang
Twisted 進(jìn)程守護(hù)
構(gòu)造"回調(diào)"的另一種方法
Twisted 理論基礎(chǔ)
惰性不是遲緩: Twisted和Haskell
第二個小插曲,deferred
使用Deferred的詩歌下載客戶端
Deferreds 全貌
結(jié)束
取消之前的意圖
由Twisted扶持的客戶端
改進(jìn)詩歌下載服務(wù)器
初識Twisted

由twisted支持的客戶端

第一個twisted支持的詩歌服務(wù)器

盡管Twisted大多數(shù)情況下用來寫服務(wù)器代碼,但為了一開始盡量從簡單處著手,我們首先從簡單的客戶端講起。

讓我們來試試使用Twisted的客戶端。源碼在twisted-client-1/get-poetry.py。首先像前面一樣要開啟三個服務(wù)器:

python blocking-server/slowpoetry.py --port 10000 poetry/ecstasy.txt --num-bytes 30
python blocking-server/slowpoetry.py --port 10001 poetry/fascination.txt
python blocking-server/slowpoetry.py --port 10002 poetry/science.txt

并且運行客戶端:

python twisted-client-1/get-poetry.py 10000 10001 10002

你會看到在客戶端的命令行打印出:

Task 1: got 60 bytes of poetry from 127.0.0.1:10000
Task 2: got 10 bytes of poetry from 127.0.0.1:10001
Task 3: got 10 bytes of poetry from 127.0.0.1:10002
Task 1: got 30 bytes of poetry from 127.0.0.1:10000 
Task 3: got 10 bytes of poetry from 127.0.0.1:10002
Task 2: got 10 bytes of poetry from 127.0.0.1:10001 
... 
Task 1: 3003 bytes of poetry
Task 2: 623 bytes of poetry
Task 3: 653 bytes of poetry
Got 3 poems in 0:00:10.134220

和我們的沒有使用Twisted的非阻塞模式客戶端打印的內(nèi)容接近。這并不奇怪,因為它們的工作方式是一樣的。

下面,我們來仔細(xì)研究一下它的源代碼。

注意:正如我在第一部分說到,我們開始學(xué)習(xí)使用Twisted時會使用一些低層Twisted的APIs。這樣做是為揭去Twisted的抽象層,這樣我們就可以從內(nèi)向外的來學(xué)習(xí)Tiwsted。但是這就意味著,我們在學(xué)習(xí)中所使用的APIs在實際應(yīng)用中可能都不會見到。記住這么一點就行:前面這些代碼只是用作練習(xí),而不是寫真實軟件的例子。

可以看到,首先創(chuàng)建了一組PoetrySocket的實例。在PoetrySocket初始化時,其創(chuàng)建了一個網(wǎng)絡(luò)socket作為自己的屬性字段來連接服務(wù)器,并且選擇了非阻塞模式:

self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.connect(address)
self.sock.setblocking(0)

最終我們雖然會提高到不使用socket的抽象層次上,但這里我們?nèi)匀恍枰褂盟T趧?chuàng)建完socket后,PoetrySocket通過方法addReader將自己傳遞給 reactor:

# tell the Twisted reactor to monitor this socket for reading
from twisted.internet import reactor
reactor.addReader(self)

這個方法給Twisted提供了一個文件描述符來監(jiān)視要發(fā)送來的數(shù)據(jù)。為什么我們不傳遞給Twisted一個文件描述符或回調(diào)函數(shù)而是一個對象實例?并且Twisted內(nèi)部沒有任何與這個詩歌服務(wù)相關(guān)的代碼,它怎么知道該如何與我們的對象實例交互?相信我,我已經(jīng)查看過了,打開twisted.internet.interfaces模塊,和我一起來搞清楚是怎么回事。

Twisted接口

在twisted內(nèi)部有很多被稱作接口的子模塊。每個都定義了一組接口類。由于在8.0版本中,Twisted使用zope.interface作為這些類的基類。但我們這里并不來討論它其中的細(xì)節(jié)。我們只關(guān)心其在Twisted的子類,就是你看到的那些。

使用接口的核心目的之一就是文檔化。作為一個python程序員,你肯定知道Duck Typing。(python哲學(xué)思想:“如果看起來像鴨子,聽起來像鴨子,就可以把它當(dāng)作鴨子”。因此python對象的接口力求簡單而且統(tǒng)一,類似其他語言中面向接口編程思想。) 翻閱twisted.internet.interfaces找到方法的addReader定義,它的定義在IReactorFDSet中可以找到:

def addReader(reader):
    """
    I add reader to the set of file descriptors to get read events for.
    @param reader: An L{IReadDescriptor} provider that will be checked for
                   read events until it is removed from the reactor with
                   L{removeReader}.
    @return: C{None}.
    """

IReactorFDSet是一個Twisted的reactor實現(xiàn)的接口。因此任何一個Twisted的reactor都會一個 addReader的方法,如同上面描述的一樣工作。這個方法聲明之所以沒有self參數(shù)是因為它僅僅關(guān)心一個公共接口定義,self參數(shù)僅僅是接口實現(xiàn)時的一部分(在調(diào)用它時,也沒有顯式地傳入一個self參數(shù))。接口類永遠(yuǎn)不會被實例化或作為基類來繼承實現(xiàn)。

  1. 技術(shù)上講,IReactorFDSet只會由reactor實現(xiàn)用來監(jiān)聽文件描述符。具我所知,現(xiàn)在所有已實現(xiàn)reactor都會實現(xiàn)這個接口。
  2. 使用接口并不僅僅是為了文檔化。zope.interface允許你顯式地來聲明一個類實現(xiàn)一個或多個接口,并提供運行時檢查這些實現(xiàn)的機制。同樣也提供代理這一機制,它可以動態(tài)地為一個沒有實現(xiàn)某接口的類直接提供該接口。但我們這里就不做深入學(xué)習(xí)了。
  3. 你可能已經(jīng)注意到接口與最近添加到Python中虛基類的相似性了。這里我們并不去分析它們之間的相似性與差異。若你有興趣,可以讀讀Python項目的創(chuàng)始人Glyph寫的一篇關(guān)于這個話題的文章。

根據(jù)文檔的描述可以看出,addReader的reader參數(shù)是要實現(xiàn)IReadDescriptor接口的。這也就意味我們的PoetrySocket也必須這樣做。

閱讀接口模塊我們可以看到下面這段代碼:

class IReadDescriptor(IFileDescriptor):
    def doRead():
        """
        Some data is available for reading on your descriptor.
        """

同時你會看到在我們的PoetrySocket類中有一個doRead方法。當(dāng)其被Twisted的reactor調(diào)用時,就會采用異步的方式從socket中讀取數(shù)據(jù)。因此,doRead其實就是一個回調(diào)函數(shù),只是沒有直接將其傳遞給reactor,而是傳遞一個實現(xiàn)此方法的對象實例。這也是Twisted框架中的慣例—不是直接傳遞實現(xiàn)某個接口的函數(shù)而是傳遞實現(xiàn)它的對象。這樣我們通過一個參數(shù)就可以傳遞一組相關(guān)的回調(diào)函數(shù)。而且也可以讓回調(diào)函數(shù)之間通過存儲在對象中的數(shù)據(jù)進(jìn)行通信。

那在PoetrySocket中實現(xiàn)其它的回調(diào)函數(shù)呢?注意到IReadDescriptor是IFileDescriptor的一個子類。這也就意味任何一個實現(xiàn)IReadDescriptor都必須實現(xiàn)IFileDescriptor。若是你仔細(xì)閱讀代碼會看到下面的內(nèi)容:

class IFileDescriptor(ILoggingContext):
    """
    A file descriptor.
    """
    def fileno():
        ...
    def connectionLost(reason):
        …

我將文檔描述省略掉了,但這些函數(shù)的功能從字面上就可以理解:fileno返回我們想監(jiān)聽的文件描述符,connectionLost是當(dāng)連接關(guān)閉時被調(diào)用。你也看到了,PoetrySocket實現(xiàn)了這些方法。

最后,IFileDescriptor繼承了ILoggingContext,這里我不想再展現(xiàn)其源碼。我想說的是,這就是為什么我們要實現(xiàn)一個logPrefix回調(diào)函數(shù)。你可以在interface模塊中找到答案。

注意:你也許注意到了,當(dāng)連接關(guān)閉時,在doRead中返回了一個特殊的值。我是如何知道的?說實話,沒有它程序是無法正常工作的。我是在分析Twisted源碼中發(fā)現(xiàn)其它相應(yīng)的方法采取相同的方法。你也許想好好研究一下:但有時一些文檔或書的解釋是錯誤的或不完整的。因此可能當(dāng)你搞清楚怎么回事時,我們已經(jīng)完成第五部分了呵呵。

更多關(guān)于回調(diào)的知識

我們使用Twisted的異步客戶端和前面的沒有使用Twisted的異步客戶非常的相似。兩者都要連接它們自己的socket,并以異步的方式從中讀取數(shù)據(jù)。最大的區(qū)別在于:使用Twisted的客戶端并沒有使用自己的select循環(huán)-而使用了Twisted的reactor。 doRead回調(diào)函數(shù)是非常重要的一個回調(diào)。Twisted調(diào)用它來告訴我們已經(jīng)有數(shù)據(jù)在socket接收完畢。我可以通過圖7來形象地說明這一過程:

http://wiki.jikexueyuan.com/project/twisted-intro/images/p04_reactor-doread.png" alt="" />

圖7 doRead回調(diào)過程

每當(dāng)回調(diào)被激活,就輪到我們的代碼將所有能夠讀的數(shù)據(jù)讀回來然后非阻塞式的停止。正如我們第三部分說的那樣,Twisted是不會因為什么異常狀況(如沒有必要的阻塞)而終止我們的代碼。那么我們就故意寫個會產(chǎn)生異常狀況的客戶端看看到底能發(fā)生什么事情??梢栽?a rel="nofollow" >twisted-client-1/get-poetry-broken.py中看到源代碼。這個客戶端與你前面看到的同樣有兩個異常狀況出現(xiàn):

  1. 這個客戶端并沒有選擇非阻塞式的socket
  2. doRead回調(diào)方法在socket關(guān)閉連接前一直在不停地讀socket

現(xiàn)在讓我們運行一下這個客戶端:

python twisted-client-1/get-poetry-broken.py 10000 10001 10002

我們出得到如同下面一樣的輸出:

Task 1: got 3003 bytes of poetry from 127.0.0.1:10000
Task 3: got 653 bytes of poetry from 127.0.0.1:10002 
Task 2: got 623 bytes of poetry from 127.0.0.1:10001
Task 1: 3003 bytes of poetry 
Task 2: 623 bytes of poetry
Task 3: 653 bytes of poetry
Got 3 poems in 0:00:10.132753

可能除了任務(wù)的完成順序不太一致外,和我前面阻塞式客戶端是一樣的。這是因為這個客戶端是一個阻塞式的。

由于使用了阻塞式的連接,就將我們的非阻塞式客戶端變成了阻塞式的客戶端。這樣一來,我們盡管遭受了使用select的復(fù)雜但卻沒有享受到其帶來的異步優(yōu)勢。

像諸如Twisted這樣的事件循環(huán)所提供的多任務(wù)的能力是需要用戶的合作來實現(xiàn)的。Twisted會告訴我們什么時候讀或?qū)懸粋€文件描述符,但我們必須要盡可能高效而沒有阻塞地完成讀寫工作。同樣我們應(yīng)該禁止使用其它各類的阻塞函數(shù),如os.system中的函數(shù)。除此之外,當(dāng)我們遇到計算型的任務(wù)(長時間占用CPU),最好是將任務(wù)切成若干個部分執(zhí)行以讓I/O操作盡可能地執(zhí)行。

你也許已經(jīng)注意到這個客戶端所花費的時間少于先前那個阻塞的客戶端。這是由于這個在一開始就與所有的服務(wù)建立連接,由于服務(wù)是一旦連接建立就立即發(fā)送數(shù)據(jù),而且我們的操作系統(tǒng)會緩存一部分發(fā)送過來但尚讀不到的數(shù)據(jù)到緩沖區(qū)中(緩沖區(qū)大小是有上限的)。因此就明白了為什么前面那個會慢了:它是在完成一個后再建立下一個連接并接收數(shù)據(jù)。

但這種小優(yōu)勢僅僅在小數(shù)據(jù)量的情況下才會得以體現(xiàn)。如果我們下載三首20M個單詞的詩,那時OS的緩沖區(qū)會在瞬間填滿,這樣一來我們這個客戶端與前面那個阻塞式客戶端相比就沒有什么優(yōu)勢可言了。

結(jié)束語

我沒有過多地解釋此部分第一個客戶端的內(nèi)容。你可能注意到了,connectionLost函數(shù)會在沒有PoetrySocket等待詩歌后關(guān)閉reactor。由于我們的程序除了下載詩歌不提供其它服務(wù),所以才會這樣做。但它揭示了兩個低層reactor的APIs:removeReader和getReaders。

還有與我們客戶端使用的Readers的APIs類同的Writers的APIs,它們采用相同的方式來監(jiān)視我們要發(fā)送數(shù)據(jù)的文件描述符。可以通過閱讀interfaces文件來獲取更多的細(xì)節(jié)。讀和寫有各自的APIs是因為select函數(shù)需要分開這兩種事件(讀或?qū)懣梢赃M(jìn)行的文件描述符)。當(dāng)然了,可以等待即能讀也能寫的文件描述符。

第五部分,我們將使用Twisted的高層抽象方式實現(xiàn)另外一個客戶端,并且學(xué)習(xí)更多的Twisted的接口與APIs。

參考

本部分原作參見: dave http://krondo.com/?p=1445

本部分翻譯內(nèi)容參見楊曉偉的博客 http://blog.sina.com.cn/s/blog_704b6af70100q0hw.html