(轉) Twisted 第四部分: 由Twisted支持的詩歌客戶端

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

儘管Twisted大多數狀況下用來寫服務器代碼,爲了一開始儘可能從簡單處着手,咱們首先從簡單的客戶端講起。python

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

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

而且運行客戶端:git

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

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

注意:正如我在第一部分說到,咱們開始學習使用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來形象地說明這一過程:

 

                                                                                                                                                                       第四部分:由Twisted支持的詩歌客戶端

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

相關文章
相關標籤/搜索