Python Twisted系列教程4:由Twisted支持的詩歌客戶端


作者:dave@http://krondo.com/twisted-poetry/  譯者:楊曉偉(采用意譯)

你可以在這里從頭開始閱讀這個系列。

 

第一個twisted支持的詩歌服務器

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

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

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的非阻塞模式客戶端打印的內容接近。這並不奇怪,因為它們的工作方式是一樣的。

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

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

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

self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

self.sock.connect(address)

self.sock.setblocking(0)

最終我們雖然會提高到不使用socket的抽象層次上,但這里我們仍然需要使用它。在創建完socket后,PoetrySocket通過方法addReader將自己傳遞給 reactor

# tell the Twisted reactor to monitor this socket for reading

from twisted.internet import reactor

reactor.addReader(self)

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

 

Twisted接口

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

使用接口的核心目的之一就是文檔化。作為一個python程序員,你肯定知道Duck Typing。(說實話我還真不懂這種編程,但通過查看資料,其實就是動態編程的思想,根據你的動作來確定你的類型)

翻閱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是一個Twistedreactor實現的接口。因此任何一個Twistedreactor都會一個 addReader的方法,如同上面描述的一樣工作。這個方法聲明之所以沒有self參數是因為它僅僅關心一個公共接口定義,self參數僅僅是接口實現時的一部分(在調用它時,也沒有顯式地傳入一個self參數)。接口類永遠不會被實例化或作為基類來繼承實現。

注意1:技術上講,IReactorFDSet只會由reactor實現用來監聽文件描述符。具我所知,現在所有已實現reactor都會實現這個接口。

注意2:使用接口並不僅僅是為了文檔化。zope.interface允許你顯式地來聲明一個類實現一個或多個接口,並提供運行時檢查這些實現的機制。同樣也提供代理這一機制,它可以動態地為一個沒有實現某接口的類直接提供該接口。但我們這里就不做深入學習了。

注意3:你可能已經注意到接口與最近添加到Python中虛基類的相似性了。這里我們並不去分析它們之間的相似性與差異。若你有興趣,可以讀讀Ptyhon項目的創始人Glyph寫的一篇關於這個話題的文章

根據文檔的描述可以看出,addReaderreader參數是要實現IreadDescriptor接口的。這也就意味我們的PoetrySocket也必須這樣做。

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

class IReadDescriptor(IFileDescriptor):

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

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

那在PoetrySocket中實現其它的回調函數呢?注意到IReadDescriptorIFileDescriptor的一個子類。這也就意味任何一個實現IReadDescriptor都必須實現IFileDescriptor。若是你仔細閱讀代碼會看到下面的內容:

class IFileDescriptor(ILoggingContext):
    """
    A file descriptor.
    """

    def fileno():
        ...

    def connectionLost(reason):
        ...

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

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

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

更多關於回調的知識

我們使用Twisted的異步客戶端和前面的沒有使用Twisted的異步客戶非常的相似。兩者都要連接它們自己的socket,並以異步的方式從中讀取數據。最大的區別在於:使用Twisted的客戶端並沒有使用自己的select循環-而使用了Twistedreactor

doRead回調函數是非常重要的一個回調。Twisted調用它來告訴我們已經有數據在socket接收完畢。我可以通過圖7來形象地說明這一過程:

 

  7 doRead 回調過程

 

每當回調被激活,就輪到我們的代碼將所有能夠讀的數據讀回來然后非阻塞式的停止。正如我們第三部分說的那樣,Twisted是不會因為什么異常狀況(如沒有必要的阻塞)而終止我們的代碼。那么我們就故意寫個會產生異常狀況的客戶端看看到底能發生什么事情。可以在twisted-client-1/get-poetry-broken.py中看到源代碼。這個客戶端與你前面看到的同樣有兩個異常狀況出現:

1.這個客戶端並不沒有選擇非阻塞式的socket

2.doRead回調方法在socket關閉連接前一直在不停地讀socket

現在讓我們運行一下這個客戶端:

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

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

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

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

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

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

 

結束語

我沒有過多地解釋此部分第一個客戶端的內容。你可能注意到了,connectionLost函數會在沒有PoetrySocket等待詩歌后關閉reactor。由於我們的程序除了下載詩歌不提供其它服務,所以才會這樣做。但它揭示了兩個低層reactorAPIsremoveReadergetReaders

還有與我們客戶端使用的ReadersAPIs類同的WritersAPIs,它們采用相同的方式來監視我們要發送數據的文件描述符。可以通過閱讀interfaces文件來獲取更多的細節。讀和寫有各自的APIs是因為select函數需要分開這兩種事件(讀或寫可以進行的文件描述符)。當然了,可以等待即能讀也能寫的文件描述符。

 

第五部分,我們將使用Twisted的高層抽象方式實現另外一個客戶端,並且學習更多的Twisted的接口與APIs


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM