使用Python的Twisted框架構(gòu)建非阻塞下載程序的實(shí)例教程
第一個(gè)twisted支持的詩(shī)歌服務(wù)器
盡管Twisted大多數(shù)情況下用來(lái)寫(xiě)服務(wù)器代碼,但為了一開(kāi)始盡量從簡(jiǎn)單處著手,我們首先從簡(jiǎn)單的客戶端講起。
讓我們來(lái)試試使用Twisted的客戶端。源碼在twisted-client-1/get-poetry.py。首先像前面一樣要開(kāi)啟三個(gè)服務(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
并且運(yùn)行客戶端:
python twisted-client-1/get-poetry.py 10000 10001 10002
你會(huì)看到在客戶端的命令行打印出:
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
和我們的沒(méi)有使用Twisted的非阻塞模式客戶端打印的內(nèi)容接近。這并不奇怪,因?yàn)樗鼈兊墓ぷ鞣绞绞且粯拥摹?br />
下面,我們來(lái)仔細(xì)研究一下它的源代碼。
注意:我們開(kāi)始學(xué)習(xí)使用Twisted時(shí)會(huì)使用一些低層Twisted的APIs。這樣做是為揭去Twisted的抽象層,這樣我們就可以從內(nèi)向外的來(lái)學(xué)習(xí)Tiwsted。但是這就意味著,我們?cè)趯W(xué)習(xí)中所使用的APIs在實(shí)際應(yīng)用中可能都不會(huì)見(jiàn)到。記住這么一點(diǎn)就行:前面這些代碼只是用作練習(xí),而不是寫(xiě)真實(shí)軟件的例子。
可以看到,首先創(chuàng)建了一組PoetrySocket的實(shí)例。在PoetrySocket初始化時(shí),其創(chuàng)建了一個(gè)網(wǎng)絡(luò)socket作為自己的屬性字段來(lái)連接服務(wù)器,并且選擇了非阻塞模式:
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.connect(address) self.sock.setblocking(0)
最終我們雖然會(huì)提高到不使用socket的抽象層次上,但這里我們?nèi)匀恍枰褂盟?。在?chuàng)建完socket后,PoetrySocket通過(guò)方法addReader將自己傳遞給 reactor:
# tell the Twisted reactor to monitor this socket for reading from twisted.internet import reactor reactor.addReader(self)
這個(gè)方法給Twisted提供了一個(gè)文件描述符來(lái)監(jiān)視要發(fā)送來(lái)的數(shù)據(jù)。為什么我們不傳遞給Twisted一個(gè)文件描述符或回調(diào)函數(shù)而是一個(gè)對(duì)象實(shí)例?并且Twisted內(nèi)部沒(méi)有任何與這個(gè)詩(shī)歌服務(wù)相關(guān)的代碼,它怎么知道該如何與我們的對(duì)象實(shí)例交互?相信我,我已經(jīng)查看過(guò)了,打開(kāi)twisted.internet.interfaces模塊,和我一起來(lái)搞清楚是怎么回事。
Twisted接口
在twisted內(nèi)部有很多被稱(chēng)作接口的子模塊。每個(gè)都定義了一組接口類(lèi)。由于在8.0版本中,Twisted使用zope.interface作為這些類(lèi)的基類(lèi)。但我們這里并不來(lái)討論它其中的細(xì)節(jié)。我們只關(guān)心其在Twisted的子類(lèi),就是你看到的那些。
使用接口的核心目的之一就是文檔化。作為一個(gè)python程序員,你肯定知道Duck Typing。(python哲學(xué)思想:“如果看起來(lái)像鴨子,聽(tīng)起來(lái)像鴨子,就可以把它當(dāng)作鴨子”。因此python對(duì)象的接口力求簡(jiǎn)單而且統(tǒng)一,類(lèi)似其他語(yǔ)言中面向接口編程思想。) 翻閱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是一個(gè)Twisted的reactor實(shí)現(xiàn)的接口。因此任何一個(gè)Twisted的reactor都會(huì)一個(gè) addReader的方法,如同上面描述的一樣工作。這個(gè)方法聲明之所以沒(méi)有self參數(shù)是因?yàn)樗鼉H僅關(guān)心一個(gè)公共接口定義,self參數(shù)僅僅是接口實(shí)現(xiàn)時(shí)的一部分(在調(diào)用它時(shí),也沒(méi)有顯式地傳入一個(gè)self參數(shù))。接口類(lèi)永遠(yuǎn)不會(huì)被實(shí)例化或作為基類(lèi)來(lái)繼承實(shí)現(xiàn)。
技術(shù)上講,IReactorFDSet只會(huì)由reactor實(shí)現(xiàn)用來(lái)監(jiān)聽(tīng)文件描述符。具我所知,現(xiàn)在所有已實(shí)現(xiàn)reactor都會(huì)實(shí)現(xiàn)這個(gè)接口。
使用接口并不僅僅是為了文檔化。zope.interface允許你顯式地來(lái)聲明一個(gè)類(lèi)實(shí)現(xiàn)一個(gè)或多個(gè)接口,并提供運(yùn)行時(shí)檢查這些實(shí)現(xiàn)的機(jī)制。同樣也提供代理這一機(jī)制,它可以動(dòng)態(tài)地為一個(gè)沒(méi)有實(shí)現(xiàn)某接口的類(lèi)直接提供該接口。但我們這里就不做深入學(xué)習(xí)了。
你可能已經(jīng)注意到接口與最近添加到Python中虛基類(lèi)的相似性了。這里我們并不去分析它們之間的相似性與差異。若你有興趣,可以讀讀Python項(xiàng)目的創(chuàng)始人Glyph寫(xiě)的一篇關(guān)于這個(gè)話題的文章。
根據(jù)文檔的描述可以看出,addReader的reader參數(shù)是要實(shí)現(xiàn)IReadDescriptor接口的。這也就意味我們的PoetrySocket也必須這樣做。
閱讀接口模塊我們可以看到下面這段代碼:
class IReadDescriptor(IFileDescriptor): def doRead(): """ Some data is available for reading on your descriptor. """
同時(shí)你會(huì)看到在我們的PoetrySocket類(lèi)中有一個(gè)doRead方法。當(dāng)其被Twisted的reactor調(diào)用時(shí),就會(huì)采用異步的方式從socket中讀取數(shù)據(jù)。因此,doRead其實(shí)就是一個(gè)回調(diào)函數(shù),只是沒(méi)有直接將其傳遞給reactor,而是傳遞一個(gè)實(shí)現(xiàn)此方法的對(duì)象實(shí)例。這也是Twisted框架中的慣例—不是直接傳遞實(shí)現(xiàn)某個(gè)接口的函數(shù)而是傳遞實(shí)現(xiàn)它的對(duì)象。這樣我們通過(guò)一個(gè)參數(shù)就可以傳遞一組相關(guān)的回調(diào)函數(shù)。而且也可以讓回調(diào)函數(shù)之間通過(guò)存儲(chǔ)在對(duì)象中的數(shù)據(jù)進(jìn)行通信。
那在PoetrySocket中實(shí)現(xiàn)其它的回調(diào)函數(shù)呢?注意到IReadDescriptor是IFileDescriptor的一個(gè)子類(lèi)。這也就意味任何一個(gè)實(shí)現(xiàn)IReadDescriptor都必須實(shí)現(xiàn)IFileDescriptor。若是你仔細(xì)閱讀代碼會(huì)看到下面的內(nèi)容:
class IFileDescriptor(ILoggingContext): """ A file descriptor. """ def fileno(): ... def connectionLost(reason): …
我將文檔描述省略掉了,但這些函數(shù)的功能從字面上就可以理解:fileno返回我們想監(jiān)聽(tīng)的文件描述符,connectionLost是當(dāng)連接關(guān)閉時(shí)被調(diào)用。你也看到了,PoetrySocket實(shí)現(xiàn)了這些方法。
最后,IFileDescriptor繼承了ILoggingContext,這里我不想再展現(xiàn)其源碼。我想說(shuō)的是,這就是為什么我們要實(shí)現(xiàn)一個(gè)logPrefix回調(diào)函數(shù)。你可以在interface模塊中找到答案。
注意:你也許注意到了,當(dāng)連接關(guān)閉時(shí),在doRead中返回了一個(gè)特殊的值。我是如何知道的?說(shuō)實(shí)話,沒(méi)有它程序是無(wú)法正常工作的。我是在分析Twisted源碼中發(fā)現(xiàn)其它相應(yīng)的方法采取相同的方法。你也許想好好研究一下:但有時(shí)一些文檔或書(shū)的解釋是錯(cuò)誤的或不完整的。
更多關(guān)于回調(diào)的知識(shí)
我們使用Twisted的異步客戶端和前面的沒(méi)有使用Twisted的異步客戶非常的相似。兩者都要連接它們自己的socket,并以異步的方式從中讀取數(shù)據(jù)。最大的區(qū)別在于:使用Twisted的客戶端并沒(méi)有使用自己的select循環(huán)-而使用了Twisted的reactor。 doRead回調(diào)函數(shù)是非常重要的一個(gè)回調(diào)。Twisted調(diào)用它來(lái)告訴我們已經(jīng)有數(shù)據(jù)在socket接收完畢。我可以通過(guò)圖7來(lái)形象地說(shuō)明這一過(guò)程:
每當(dāng)回調(diào)被激活,就輪到我們的代碼將所有能夠讀的數(shù)據(jù)讀回來(lái)然后非阻塞式的停止。Twisted是不會(huì)因?yàn)槭裁串惓顩r(如沒(méi)有必要的阻塞)而終止我們的代碼。那么我們就故意寫(xiě)個(gè)會(huì)產(chǎn)生異常狀況的客戶端看看到底能發(fā)生什么事情。可以在twisted-client-1/get-poetry-broken.py中看到源代碼。這個(gè)客戶端與你前面看到的同樣有兩個(gè)異常狀況出現(xiàn):
這個(gè)客戶端并沒(méi)有選擇非阻塞式的socket
doRead回調(diào)方法在socket關(guān)閉連接前一直在不停地讀socket
現(xiàn)在讓我們運(yùn)行一下這個(gè)客戶端:
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ù)的完成順序不太一致外,和我前面阻塞式客戶端是一樣的。這是因?yàn)檫@個(gè)客戶端是一個(gè)阻塞式的。
由于使用了阻塞式的連接,就將我們的非阻塞式客戶端變成了阻塞式的客戶端。這樣一來(lái),我們盡管遭受了使用select的復(fù)雜但卻沒(méi)有享受到其帶來(lái)的異步優(yōu)勢(shì)。
像諸如Twisted這樣的事件循環(huán)所提供的多任務(wù)的能力是需要用戶的合作來(lái)實(shí)現(xiàn)的。Twisted會(huì)告訴我們什么時(shí)候讀或?qū)懸粋€(gè)文件描述符,但我們必須要盡可能高效而沒(méi)有阻塞地完成讀寫(xiě)工作。同樣我們應(yīng)該禁止使用其它各類(lèi)的阻塞函數(shù),如os.system中的函數(shù)。除此之外,當(dāng)我們遇到計(jì)算型的任務(wù)(長(zhǎng)時(shí)間占用CPU),最好是將任務(wù)切成若干個(gè)部分執(zhí)行以讓I/O操作盡可能地執(zhí)行。
你也許已經(jīng)注意到這個(gè)客戶端所花費(fèi)的時(shí)間少于先前那個(gè)阻塞的客戶端。這是由于這個(gè)在一開(kāi)始就與所有的服務(wù)建立連接,由于服務(wù)是一旦連接建立就立即發(fā)送數(shù)據(jù),而且我們的操作系統(tǒng)會(huì)緩存一部分發(fā)送過(guò)來(lái)但尚讀不到的數(shù)據(jù)到緩沖區(qū)中(緩沖區(qū)大小是有上限的)。因此就明白了為什么前面那個(gè)會(huì)慢了:它是在完成一個(gè)后再建立下一個(gè)連接并接收數(shù)據(jù)。
但這種小優(yōu)勢(shì)僅僅在小數(shù)據(jù)量的情況下才會(huì)得以體現(xiàn)。如果我們下載三首20M個(gè)單詞的詩(shī),那時(shí)OS的緩沖區(qū)會(huì)在瞬間填滿,這樣一來(lái)我們這個(gè)客戶端與前面那個(gè)阻塞式客戶端相比就沒(méi)有什么優(yōu)勢(shì)可言了。
抽象地構(gòu)建客戶端
首先是,這個(gè)客戶端竟然有創(chuàng)建網(wǎng)絡(luò)端口并接收端口處的數(shù)據(jù)這樣枯燥的代碼。Twisted理應(yīng)為我們實(shí)現(xiàn)這些例程性功能,省得我們每次寫(xiě)一個(gè)新的程序時(shí)都要自己去實(shí)現(xiàn)。這樣做特別有用,可以將我們從異步I/O涉及的一些棘手的異常處理中解放出來(lái)(參看前面的客戶端) , 如果要跨平臺(tái)就涉及到更多更加棘手的細(xì)節(jié)。如果你哪天下午有空,可以翻翻Twisted的WIN32實(shí)現(xiàn)源代碼,看看里面有多少小針線是來(lái)處理跨平臺(tái)的。
另一問(wèn)題是與錯(cuò)誤處理有關(guān)。當(dāng)運(yùn)行版本1的Twisted客戶端從并沒(méi)有提供服務(wù)的端口上下載詩(shī)歌時(shí),它就會(huì)崩潰。當(dāng)然我們是可以修正這個(gè)錯(cuò)誤,但通過(guò)下面我們要介紹Twisted的APIs來(lái)處理這些類(lèi)型的錯(cuò)誤會(huì)更簡(jiǎn)單。
最后,那個(gè)客戶端也不能復(fù)用。如果有另一個(gè)模塊需要通過(guò)我們的客戶端下載詩(shī)歌呢?人家怎么知道你的詩(shī)歌已經(jīng)下載完畢?我們不能用一個(gè)方法簡(jiǎn)單地將一首詩(shī)下載完成后再傳給人家,而在之前讓人家處于等待狀態(tài)。這確實(shí)是一個(gè)問(wèn)題,但我們不準(zhǔn)備在這個(gè)部分解決這個(gè)問(wèn)題—在未來(lái)的部分中一定會(huì)解決這個(gè)問(wèn)題。
我們將會(huì)使用一些高層次的APIs和接口來(lái)解決第一、二個(gè)問(wèn)題。Twisted框架是由眾多抽象層松散地組合起來(lái)的。因此,學(xué)習(xí)Twisted也就意味著需要學(xué)習(xí)這些層都提供什么功能,例如每層都有哪些APIs,接口和實(shí)例可供使用。接下來(lái)我們會(huì)通過(guò)剖析Twisted最最重要的部分來(lái)更好地感受一下Twisted都是怎么組織的。一旦你對(duì)Twisted的整個(gè)結(jié)構(gòu)熟悉了,學(xué)習(xí)新的部分會(huì)簡(jiǎn)單多了。
一般來(lái)說(shuō),每個(gè)Twisted的抽象都只與一個(gè)特定的概念相關(guān)。例如,第四部分中的客戶端使用的IReadDescriptor,它就是"一個(gè)可以讀取字節(jié)的文件描述符"的抽象。一個(gè)抽象往往會(huì)通過(guò)定義接口來(lái)指定那些想實(shí)現(xiàn)這個(gè)抽象(也就是實(shí)現(xiàn)這個(gè)接口)的對(duì)象的形為。在學(xué)習(xí)新的Twisted抽象概念時(shí),最需要謹(jǐn)記的就是:
多數(shù)高層次抽象都是在低層次抽象的基礎(chǔ)上建立的,很少有另立門(mén)戶的。
因此,你在學(xué)習(xí)新的Twisted抽象概念時(shí),始終要記住它做什么和不做什么。特別是,如果一個(gè)早期的抽象A實(shí)現(xiàn)了F特性,那么F特性不太可能再由其它任何抽象來(lái)實(shí)現(xiàn)。另外,如果另外一個(gè)抽象需要F特性,那么它會(huì)使用A而不是自己再去實(shí)現(xiàn)F。(通常的做法,B可能會(huì)通過(guò)繼承A或獲得一個(gè)指向A實(shí)例的引用)
網(wǎng)絡(luò)非常的復(fù)雜,因此Twisted包含很多抽象的概念。通過(guò)從低層的抽象講起,我們希望能更清楚起看到在一個(gè)Twisted程序中各個(gè)部分是怎么組織起來(lái)的。
核心的循環(huán)體
第一個(gè)我們要學(xué)習(xí)的抽象,也是Twisted中最重要的,就是reactor。在每個(gè)通過(guò)Twisted搭建起來(lái)的程序中心處,不管你這個(gè)程序有多少層,總會(huì)有一個(gè)reactor循環(huán)在不停止地驅(qū)動(dòng)程序的運(yùn)行。再也沒(méi)有比reactor提供更加基礎(chǔ)的支持了。實(shí)際上,Twisted的其它部分(即除了reactor循環(huán)體)可以這樣理解:它們都是來(lái)輔助X來(lái)更好地使用reactor,這里的X可以是提供Web網(wǎng)頁(yè)、處理一個(gè)數(shù)據(jù)庫(kù)查詢請(qǐng)求或其它更加具體的內(nèi)容。盡管堅(jiān)持像上一個(gè)客戶端一樣使用低層APIs是可能的,但如果我們執(zhí)意那樣做,那么我們必需自己來(lái)實(shí)現(xiàn)非常多的內(nèi)容。而在更高的層次上,意味著我們可以少寫(xiě)很多代碼。
但是當(dāng)在外層思考與處理問(wèn)題時(shí), 很容易就忘記了reactor的存在了。在任何一個(gè)常見(jiàn)大小的Twisted程序中 ,確實(shí)很少會(huì)有直接與reactor的APIs交互。低層的抽象也是一樣(即我們很少會(huì)直接與其交互)。我們?cè)谏弦粋€(gè)客戶端中用到的文件描述符抽象,就被更高層的抽象更好的歸納以至于我們很少會(huì)在真正的Twisted程序中遇到。(他們?cè)趦?nèi)部依然在被使用,只是我們看不到而已)
至于文件描述符抽象的消息,這并不是一個(gè)問(wèn)題。讓Twisted掌舵異步I/O處理,這樣我們就可以更加關(guān)注我們實(shí)際要解決的問(wèn)題。但對(duì)于reactor不一樣,它永遠(yuǎn)都不會(huì)消失。當(dāng)你選擇使用Twisted,也就意味著你選擇使用Reactor模式,并且意味著你需要使用回調(diào)與多任務(wù)合作的"交互式"編程方式。
Transports
Transports抽象是通過(guò)Twisted中interfaces模塊中ITransport接口定義的。一個(gè)Twisted的Transport代表一個(gè)可以收發(fā)字節(jié)的單條連接。對(duì)于我們的詩(shī)歌下載客戶端而言,就是對(duì)一條TCP連接的抽象。但是Twisted也支持諸如Unix中管道和UDP。Transport抽象可以代表任何這樣的連接并為其代表的連接處理具體的異步I/O操作細(xì)節(jié)。
如果你瀏覽一下ITransport中的方法,可能找不到任何接收數(shù)據(jù)的方法。這是因?yàn)門(mén)ransports總是在低層完成從連接中異步讀取數(shù)據(jù)的許多細(xì)節(jié)工作,然后通過(guò)回調(diào)將數(shù)據(jù)發(fā)給我們。相似的原理,Transport對(duì)象的寫(xiě)相關(guān)的方法為避免阻塞也不會(huì)選擇立即寫(xiě)我們要發(fā)送的數(shù)據(jù)。告訴一個(gè)Transport要發(fā)送數(shù)據(jù),只是意味著:盡快將這些數(shù)據(jù)發(fā)送出去,別產(chǎn)生阻塞就行。當(dāng)然,數(shù)據(jù)會(huì)按照我們提交的順序發(fā)送。
通常我們不會(huì)自己實(shí)現(xiàn)一個(gè)Transport。我們會(huì)去使用Twisted提供的實(shí)現(xiàn)類(lèi),即在傳遞給reactor時(shí)會(huì)為我們創(chuàng)建一個(gè)對(duì)象實(shí)例。
Protocols
Twisted的Protocols抽象由interfaces模塊中的IProtocol定義。也許你已經(jīng)想到,Protocol對(duì)象實(shí)現(xiàn)協(xié)議內(nèi)容。也就是說(shuō),一個(gè)具體的Twisted的Protocol的實(shí)現(xiàn)應(yīng)該對(duì)應(yīng)一個(gè)具體網(wǎng)絡(luò)協(xié)議的實(shí)現(xiàn),像FTP、IMAP或其它我們自己制定的協(xié)議。我們的詩(shī)歌下載協(xié)議,正如它表現(xiàn)的那樣,就是在連接建立后將所有的詩(shī)歌內(nèi)容全部發(fā)送出去并且在發(fā)送完畢后關(guān)閉連接。
嚴(yán)格意義上講,每一個(gè)Twisted的Protocols類(lèi)實(shí)例都為一個(gè)具體的連接提供協(xié)議解析。因此我們的程序每建立一條連接(對(duì)于服務(wù)方就是每接受一條連接),都需要一個(gè)協(xié)議實(shí)例。這就意味著,Protocol實(shí)例是存儲(chǔ)協(xié)議狀態(tài)與間斷性(由于我們是通過(guò)異步I/O方式以任意大小來(lái)接收數(shù)據(jù)的)接收并累積數(shù)據(jù)的地方。
因此,Protocol實(shí)例如何得知它為哪條連接服務(wù)呢?如果你閱讀IProtocol定義會(huì)發(fā)現(xiàn)一個(gè)makeConnection函數(shù)。這是一個(gè)回調(diào)函數(shù),Twisted會(huì)在調(diào)用它時(shí)傳遞給其一個(gè)也是僅有的一個(gè)參數(shù),即Transport實(shí)例。這個(gè)Transport實(shí)例就代表Protocol將要使用的連接。
Twisted內(nèi)置了很多實(shí)現(xiàn)了通用協(xié)議的Protocol。你可以在twisted.protocols.basic中找到一些稍微簡(jiǎn)單點(diǎn)的。在你嘗試寫(xiě)新Protocol時(shí),最好是看看Twisted源碼是不是已經(jīng)有現(xiàn)成的存在。如果沒(méi)有,那實(shí)現(xiàn)一個(gè)自己的協(xié)議是非常好的,正如我們?yōu)樵?shī)歌下載客戶端做的那樣。
Protocol Factories
因此每個(gè)連接需要一個(gè)自己的Protocol,而且這個(gè)Protocol是我們自己定義的類(lèi)的實(shí)例。由于我們會(huì)將創(chuàng)建連接的工作交給Twisted來(lái)完成,Twisted需要一種方式來(lái)為一個(gè)新的連接創(chuàng)建一個(gè)合適的協(xié)議。創(chuàng)建協(xié)議就是Protocol Factories的工作了。
也許你已經(jīng)猜到了,Protocol Factory的API由IProtocolFactory來(lái)定義,同樣在interfaces模塊中。Protocol Factory就是Factory模式的一個(gè)具體實(shí)現(xiàn)。buildProtocol方法在每次被調(diào)用時(shí)返回一個(gè)新Protocol實(shí)例,它就是Twisted用來(lái)為新連接創(chuàng)建新Protocol實(shí)例的方法。
詩(shī)歌下載客戶端2.0:第一滴心血
好吧,讓我們來(lái)看看由Twisted支持的詩(shī)歌下載客戶端2.0。源碼可以在這里twisted-client-2/get-poetry.py。你可以像前面一樣運(yùn)行它,并得到相同的輸出。這也是最后一個(gè)在接收到數(shù)據(jù)時(shí)打印其任務(wù)的客戶端版本了。到現(xiàn)在為止,對(duì)于所有Twisted程序都是交替執(zhí)行任務(wù)并處理相對(duì)較少數(shù)量數(shù)據(jù)的,應(yīng)該很清晰了。我們依然通過(guò)print函數(shù)來(lái)展示在關(guān)鍵時(shí)刻在進(jìn)行什么內(nèi)容,但將來(lái)客戶端不會(huì)在這樣繁鎖。
在第二個(gè)版本中,sockets不會(huì)再出現(xiàn)了。我們甚至不需要引入socket模塊也不用引用socket對(duì)象和文件描述符。取而代之的是,我們告訴reactor來(lái)創(chuàng)建到詩(shī)歌服務(wù)器的連接,代碼如下面所示:
factory = PoetryClientFactory(len(addresses)) from twisted.internet import reactor for address in addresses: host, port = address reactor.connectTCP(host, port, factory)
我們需要關(guān)注的是connectTCP這個(gè)函數(shù)。前兩個(gè)參數(shù)的含義很明顯,不解釋了。第三個(gè)參數(shù)是我們自定義的PoetryClientFactory類(lèi)的實(shí)例對(duì)象。這是一個(gè)專(zhuān)門(mén)針對(duì)詩(shī)歌下載客戶端的Protocol Factory,將它傳遞給reactor可以讓Twisted為我們創(chuàng)建一個(gè)PoetryProtocol實(shí)例。
值得注意的是,從一開(kāi)始我們既沒(méi)有實(shí)現(xiàn)Factory也沒(méi)有去實(shí)現(xiàn)Protocol,不像在前面那個(gè)客戶端中我們?nèi)?shí)例化我們PoetrySocket類(lèi)。我們只是繼承了Twisted在twisted.internet.protocol 中提供的基類(lèi)。Factory的基類(lèi)是twisted.internet.protocol.Factory,但我們使用客戶端專(zhuān)用(即不像服務(wù)器端那樣監(jiān)聽(tīng)一個(gè)連接,而是主動(dòng)創(chuàng)建一個(gè)連接)的ClientFactory子類(lèi)來(lái)繼承。
我們同樣利用了Twisted的Factory已經(jīng)實(shí)現(xiàn)了buildProtocol方法這一優(yōu)勢(shì)來(lái)為我們所用。我們要在子類(lèi)中調(diào)用基類(lèi)中的實(shí)現(xiàn):
def buildProtocol(self, address): proto = ClientFactory.buildProtocol(self, address) proto.task_num = self.task_num self.task_num += 1 return proto
基類(lèi)怎么會(huì)知道我們要?jiǎng)?chuàng)建什么樣的Protocol呢?注意,我們的PoetryClientFactory中有一個(gè)protocol類(lèi)變量:
class PoetryClientFactory(ClientFactory): task_num = 1 protocol = PoetryProtocol # tell base class what proto to build
基類(lèi)Factory實(shí)現(xiàn)buildProtocol的過(guò)程是:安裝(創(chuàng)建一個(gè)實(shí)例)我們?cè)O(shè)置在protocol變量上的Protocol類(lèi)與在這個(gè)實(shí)例(此處即PoetryProtocol的實(shí)例)的factory屬性上設(shè)置一個(gè)產(chǎn)生它的Factory的引用(此處即實(shí)例化PoetryProtocol的PoetryClientFactory)。這個(gè)過(guò)程如圖
正如我們提到的那樣,位于Protocol對(duì)象內(nèi)的factory屬性字段允許在都由同一個(gè)factory產(chǎn)生的Protocol之間共享數(shù)據(jù)。由于Factories都是由用戶代碼來(lái)創(chuàng)建的(即在用戶的控制中),因此這個(gè)屬性也可以實(shí)現(xiàn)Protocol對(duì)象將數(shù)據(jù)傳遞回一開(kāi)始初始化請(qǐng)求的代碼中來(lái),這將在第六部分看到。
值得注意的是,雖然在Protocol中有一個(gè)屬性指向生成其的Protocol Factory,在Factory中也有一個(gè)變量指向一個(gè)Protocol類(lèi),但通常來(lái)說(shuō),一個(gè)Factory可以生成多個(gè)Protocol。
在Protocol創(chuàng)立的第二步便是通過(guò)makeConnection與一個(gè)Transport聯(lián)系起來(lái)。我們無(wú)需自己來(lái)實(shí)現(xiàn)這個(gè)函數(shù)而使用Twisted提供的默認(rèn)實(shí)現(xiàn)。默認(rèn)情況是,makeConnection將Transport的一個(gè)引用賦給(Protocol的)transport屬性,同時(shí)置(同樣是Protocol的)connected屬性為T(mén)rue
一旦初始化到這一步后,Protocol開(kāi)始其真正的工作—將低層的數(shù)據(jù)流翻譯成高層的協(xié)議規(guī)定格式的消息。處理接收到數(shù)據(jù)的主要方法是dataReceived,我們的客戶端是這樣實(shí)現(xiàn)的:
def dataReceived(self, data): self.poem += data msg = 'Task %d: got %d bytes of poetry from %s' print msg % (self.task_num, len(data), self.transport.getHost())
每次dateReceved被調(diào)用就意味著我們得到一個(gè)新字符串。由于與異步I/O交互,我們不知道能接收到多少數(shù)據(jù),因此將接收到的數(shù)據(jù)緩存下來(lái)直到完成一個(gè)完整的協(xié)議規(guī)定格式的消息。在我們的例子中,詩(shī)歌只有在連接關(guān)閉時(shí)才下載完畢,因此我們只是不斷地將接收到的數(shù)據(jù)添加到我們的.poem屬性字段中。
注意我們使用了Transport的getHost方法來(lái)取得數(shù)據(jù)來(lái)自的服務(wù)器信息。我們這樣做只是與前面的客戶端保持一致。相反,我們的代碼沒(méi)有必要這樣做,因?yàn)槲覀儧](méi)有向服務(wù)器發(fā)送任何消息,也就沒(méi)有必要知道服務(wù)器的信息了。
我們來(lái)看一下dataReceved運(yùn)行時(shí)的快照。在2.0版本相同的目錄下有一個(gè)twisted-client-2/get-poetry-stack.py。它與2.0版本的不同之處只在于:
def dataReceived(self, data): traceback.print_stack() os._exit(0)
這樣一改,我們就能打印出跟蹤堆棧的信息,然后離開(kāi)程序,可以用下面的命令來(lái)運(yùn)行它:
python twisted-client-2/get-poetry-stack.py 10000
你會(huì)得到內(nèi)容如下的跟蹤堆棧:
File "twisted-client-2/get-poetry-stack.py", line 125, in poetry_main() ... # I removed a bunch of lines here File ".../twisted/internet/tcp.py", line 463, in doRead # Note the doRead callback return self.protocol.dataReceived(data) File "twisted-client-2/get-poetry-stack.py", line 58, in dataReceived traceback.print_stack()
看見(jiàn)沒(méi),有我們?cè)?.0版本客戶端的doRead回調(diào)函數(shù)。我們前面也提到過(guò),Twisted在建立新抽象層會(huì)使用已有的實(shí)現(xiàn)而不是另起爐灶。因此必然會(huì)有一個(gè)IReadDescriptor的實(shí)例在辛苦的工作,它是由Twisted代碼而非我們自己的代碼來(lái)實(shí)現(xiàn)。如果你表示懷疑,那么就看看twisted.internet.tcp中的實(shí)現(xiàn)吧。如果你瀏覽代碼會(huì)發(fā)現(xiàn),由同一個(gè)類(lèi)實(shí)現(xiàn)了IWriteDescriptor與ITransport。因此 IReadDescriptor實(shí)際上就是變相的Transport類(lèi)??梢杂脠D10來(lái)形象地說(shuō)明dateReceived的回調(diào)過(guò)程:
一旦詩(shī)歌下載完成,PoetryProtocol就會(huì)通知它的PooetryClientFactory:
def connectionLost(self, reason): self.poemReceived(self.poem) def poemReceived(self, poem): self.factory.poem_finished(self.task_num, poem)
當(dāng)transport的連接關(guān)閉時(shí),conncetionLost回調(diào)會(huì)被激活。reason參數(shù)是一個(gè)twisted.python.failure.Failure的實(shí)例對(duì)象,其攜帶的信息能夠說(shuō)明連接是被安全的關(guān)閉還是由于出錯(cuò)被關(guān)閉的。我們的客戶端因認(rèn)為總是能完整地下載完詩(shī)歌而忽略了這一參數(shù)。
工廠會(huì)在所有的詩(shī)歌都下載完畢后關(guān)閉reactor。再次重申:我們代碼的工作就是用來(lái)下載詩(shī)歌-這意味我們的PoetryClientFactory缺少?gòu)?fù)用性。我們將在下一部分修正這一缺陷。值得注意的是,poem_finish回調(diào)函數(shù)是如何通過(guò)跟蹤剩余詩(shī)歌數(shù)的:
... self.poetry_count -= 1 if self.poetry_count == 0: ...
如果我們采用多線程以讓每個(gè)線程分別下載詩(shī)歌,這樣我們就必須使用一把鎖來(lái)管理這段代碼以免多個(gè)線程在同一時(shí)間調(diào)用poem_finish。但是在交互式體系下就不必?fù)?dān)心了。由于reactor只能一次啟用一個(gè)回調(diào)。
新的客戶端實(shí)現(xiàn)在處理錯(cuò)誤上也比先前的優(yōu)雅的多,下面是PoetryClientFactory處理錯(cuò)誤連接的回調(diào)實(shí)現(xiàn)代碼:
def clientConnectionFailed(self, connector, reason): print 'Failed to connect to:', connector.getDestination() self.poem_finished()
注意,回調(diào)是在工廠內(nèi)部而不是協(xié)議內(nèi)部實(shí)現(xiàn)。由于協(xié)議是在連接建立后才創(chuàng)建的,而工廠能夠在連接未能成功建立時(shí)捕獲消息。
- python如何通過(guò)twisted搭建socket服務(wù)
- Python3.6中Twisted模塊安裝的問(wèn)題與解決
- python安裝twisted的問(wèn)題解析
- python如何通過(guò)twisted實(shí)現(xiàn)數(shù)據(jù)庫(kù)異步插入
- python基于twisted框架編寫(xiě)簡(jiǎn)單聊天室
- python 編程之twisted詳解及簡(jiǎn)單實(shí)例
- Python 基于Twisted框架的文件夾網(wǎng)絡(luò)傳輸源碼
- 剖析Python的Twisted框架的核心特性
- 實(shí)例解析Python的Twisted框架中Deferred對(duì)象的用法
- 詳解Python的Twisted框架中reactor事件管理器的用法
- 使用Python的Twisted框架編寫(xiě)非阻塞程序的代碼示例
- Python的Twisted框架中使用Deferred對(duì)象來(lái)管理回調(diào)函數(shù)
- Python的Twisted框架上手前所必須了解的異步編程思想
- 使用Python的Treq on Twisted來(lái)進(jìn)行HTTP壓力測(cè)試
- 利用Python的Twisted框架實(shí)現(xiàn)webshell密碼掃描器的教程
- 使用Python的Twisted框架實(shí)現(xiàn)一個(gè)簡(jiǎn)單的服務(wù)器
- 使用Python的Twisted框架編寫(xiě)簡(jiǎn)單的網(wǎng)絡(luò)客戶端
- python開(kāi)發(fā)實(shí)例之Python的Twisted框架中Deferred對(duì)象的詳細(xì)用法與實(shí)例
相關(guān)文章
python正則表達(dá)式re之compile函數(shù)解析
這篇文章主要介紹了python正則表達(dá)式re之compile函數(shù)解析,介紹了其定義,匹配模式等相關(guān)內(nèi)容,具有一定參考價(jià)值,需要的朋友可以了解下。2017-10-10Python編程實(shí)現(xiàn)使用線性回歸預(yù)測(cè)數(shù)據(jù)
這篇文章主要介紹了Python編程實(shí)現(xiàn)使用線性回歸預(yù)測(cè)數(shù)據(jù),具有一定借鑒價(jià)值,需要的朋友可以了解下。2017-12-12解決PySide+Python子線程更新UI線程的問(wèn)題
今天小編就為大家分享一篇解決PySide+Python子線程更新UI線程的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-01-01python用BeautifulSoup庫(kù)簡(jiǎn)單爬蟲(chóng)實(shí)例分析
文章給大家分享了關(guān)于python爬蟲(chóng)的相關(guān)實(shí)例以及相關(guān)代碼,有興趣的朋友們參考下。2018-07-07Python實(shí)現(xiàn)模擬登錄及表單提交的方法
這篇文章主要介紹了Python實(shí)現(xiàn)模擬登錄及表單提交的方法,涉及Python正則匹配、cookie及URL操作的相關(guān)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-07-07Pytorch-mlu?實(shí)現(xiàn)添加逐層算子方法詳解
本文主要分享了在寒武紀(jì)設(shè)備上?pytorch-mlu?中添加逐層算子的方法教程,代碼具有一定學(xué)習(xí)價(jià)值,有需要的朋友可以借鑒參考下,希望能夠有所幫助2021-11-11Python中讓MySQL查詢結(jié)果返回字典類(lèi)型的方法
這篇文章主要介紹了Python中讓MySQL查詢結(jié)果返回字典類(lèi)型的方法,默認(rèn)情況下Mysql返回的是元組類(lèi)型,本文實(shí)現(xiàn)了返回字典類(lèi)型,需要的朋友可以參考下2014-08-08