作者: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時會使用一些底層Twisted的APIs。這樣做是為揭去Twisted的抽象層,這樣我們就可以從內向外的來學習Tiwsted。但是這就意味着,我們在學習中所使用的APIs在實際應用中可能都不會見到。記住這么一點就行:前面這些代碼只是用作練習,而不是寫真實軟件的例子。
可以看到,首先創建了一組PoetrySocke
t
的實例。在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
是一個
Twisted
的
reactor
實現的接口。因此任何一個
Twisted
的
reactor
都會一個
addReader
的方法,如同上面描述的一樣工作。這個方法聲明之所以沒有
self
參數是因為它僅僅關心一個公共接口定義,
self
參數僅僅是接口實現時的一部分(在調用它時,也沒有顯
式地
傳入一個
self
參數)。接口類永遠不會被實例化或作為基類來繼承實現。
注意
1
:技術上講,
IReactorFDSet
只會由
reactor
實現用來監聽文件描述符。具我所知,現在所有已實現
reactor
都會實現這個接口。
注意
2
:使用接口並不僅僅是為了文檔化。
zope.interface
允許你顯式地來聲明一個類實現一個或多個接口,並提供運行時檢查這些實現的機制。同樣也提供代理這一機制,它可以動態地為一個沒有實現某接口的類直接提供該接口。但我們這里就不做深入學習了。
注意
3
:你可能已經注意到接口與最近添加到
Python
中虛基類的相似性了。這里我們並不去分析它們之間的相似性與差異。若你有興趣,可以讀讀
Ptyhon
項目的創始人
Glyph
寫的一篇關於這個話題的文章。
根據文檔的描述可以看出,
addReader
的
reader
參數是要實現
IreadDescriptor
接口的。這也就意味我們的
PoetrySocket
也必須這樣做。
閱讀接口模塊我們可以看到下面這段代碼:
class IReadDescriptor(IFileDescriptor): def doRead(): """ Some data is available for reading on your descriptor. """
同時
你會看到在我們的
PoetrySocket
類中有一個
doRead
方法。
當其被
Twisted
的
reactor
調用時,就會采用異步的方式從
socket
中讀取數據。因此,
doRead
其實就是一個回調函數,只是沒有直接將其傳遞給
reactor
,而是傳遞一個實現此方法的對象實例。這也是
Twisted
框架中的慣例—不是直接傳遞實現某個接口的函數而是傳遞實現它的對象。這樣我們通過一個參數就可以傳遞一組相關的回調函數。而且也可以讓回調函數之間通過存儲在對象中的數據進行通信。
那在
PoetrySocket
中實現其它的回調函數呢?注意到
IReadDescriptor
是
IFileDescriptor
的一個子類。這也就意味任何一個實現
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
循環
-
而使用了
Twisted
的
reactor
。
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
。由於我們的程序除了下載詩歌不提供其它服務,所以才會這樣做。但它揭示了兩個低層
reactor
的
APIs
:
removeReader
和
getReaders
。
還有與我們客戶端使用的
Readers
的
APIs
類同的
Writers
的
APIs
,它們采用相同的方式來監視我們要發送數據的文件描述符。可以通過閱讀
interfaces
文件來獲取更多的細節。讀和寫有各自的
APIs
是因為
select
函數需要分開這兩種事件(讀或寫可以進行的文件描述符)。當然了,可以等待即能讀也能寫的文件描述符。
第五部分,我們將使用
Twisted
的高層抽象方式實現另外一個客戶端,並且學習更多的
Twisted
的接口與
APIs
。