在第四部分中,我們構(gòu)建了第一個使用Twisted的客戶端。它確實能很好地工作,但仍有提高的空間。
首先是,這個客戶端竟然有創(chuàng)建網(wǎng)絡(luò)端口并接收端口處的數(shù)據(jù)這樣枯燥的代碼。Twisted理應(yīng)為我們實現(xiàn)這些例程性功能,省得我們每次寫一個新的程序時都要自己去實現(xiàn)。這樣做特別有用,可以將我們從異步I/O涉及的一些棘手的異常處理中解放出來(參看前面的客戶端) , 如果要跨平臺就涉及到更多更加棘手的細節(jié)。如果你哪天下午有空,可以翻翻Twisted的WIN32實現(xiàn)源代碼,看看里面有多少小針線是來處理跨平臺的。
另一問題是與錯誤處理有關(guān)。當運行版本1的Twisted客戶端從并沒有提供服務(wù)的端口上下載詩歌時,它就會崩潰。當然我們是可以修正這個錯誤,但通過下面我們要介紹Twisted的APIs來處理這些類型的錯誤會更簡單。
最后,那個客戶端也不能復(fù)用。如果有另一個模塊需要通過我們的客戶端下載詩歌呢?人家怎么知道你的詩歌已經(jīng)下載完畢?我們不能用一個方法簡單地將一首詩下載完成后再傳給人家,而在之前讓人家處于等待狀態(tài)。這確實是一個問題,但我們不準備在這個部分解決這個問題—在未來的部分中一定會解決這個問題。
我們將會使用一些高層次的APIs和接口來解決第一、二個問題。Twisted框架是由眾多抽象層松散地組合起來的。因此,學(xué)習(xí)Twisted也就意味著需要學(xué)習(xí)這些層都提供什么功能,例如每層都有哪些APIs,接口和實例可供使用。接下來我們會通過剖析Twisted最最重要的部分來更好地感受一下Twisted都是怎么組織的。一旦你對Twisted的整個結(jié)構(gòu)熟悉了,學(xué)習(xí)新的部分會簡單多了。
一般來說,每個Twisted的抽象都只與一個特定的概念相關(guān)。例如,第四部分中的客戶端使用的IReadDescriptor,它就是"一個可以讀取字節(jié)的文件描述符"的抽象。一個抽象往往會通過定義接口來指定那些想實現(xiàn)這個抽象(也就是實現(xiàn)這個接口)的對象的形為。在學(xué)習(xí)新的Twisted抽象概念時,最需要謹記的就是:
多數(shù)高層次抽象都是在低層次抽象的基礎(chǔ)上建立的,很少有另立門戶的。
因此,你在學(xué)習(xí)新的Twisted抽象概念時,始終要記住它做什么和不做什么。特別是,如果一個早期的抽象A實現(xiàn)了F特性,那么F特性不太可能再由其它任何抽象來實現(xiàn)。另外,如果另外一個抽象需要F特性,那么它會使用A而不是自己再去實現(xiàn)F。(通常的做法,B可能會通過繼承A或獲得一個指向A實例的引用)
網(wǎng)絡(luò)非常的復(fù)雜,因此Twisted包含很多抽象的概念。通過從低層的抽象講起,我們希望能更清楚起看到在一個Twisted程序中各個部分是怎么組織起來的。
第一個我們要學(xué)習(xí)的抽象,也是Twisted中最重要的,就是reactor。在每個通過Twisted搭建起來的程序中心處,不管你這個程序有多少層,總會有一個reactor循環(huán)在不停止地驅(qū)動程序的運行。再也沒有比reactor提供更加基礎(chǔ)的支持了。實際上,Twisted的其它部分(即除了reactor循環(huán)體)可以這樣理解:它們都是來輔助X來更好地使用reactor,這里的X可以是提供Web網(wǎng)頁、處理一個數(shù)據(jù)庫查詢請求或其它更加具體的內(nèi)容。盡管堅持像上一個客戶端一樣使用低層APIs是可能的,但如果我們執(zhí)意那樣做,那么我們必需自己來實現(xiàn)非常多的內(nèi)容。而在更高的層次上,意味著我們可以少寫很多代碼。
但是當在外層思考與處理問題時, 很容易就忘記了reactor的存在了。在任何一個常見大小的Twisted程序中 ,確實很少會有直接與reactor的APIs交互。低層的抽象也是一樣(即我們很少會直接與其交互)。我們在上一個客戶端中用到的文件描述符抽象,就被更高層的抽象更好的歸納以至于我們很少會在真正的Twisted程序中遇到。(他們在內(nèi)部依然在被使用,只是我們看不到而已)
至于文件描述符抽象的消息,這并不是一個問題。讓Twisted掌舵異步I/O處理,這樣我們就可以更加關(guān)注我們實際要解決的問題。但對于reactor不一樣,它永遠都不會消失。當你選擇使用Twisted,也就意味著你選擇使用Reactor模式,并且意味著你需要使用回調(diào)與多任務(wù)合作的"交互式"編程方式。如果你想正確地使用Twisted,你必須牢記reactor的存在。我們將在第六部分更加詳細的講解部分內(nèi)容。但是現(xiàn)在要強調(diào)的是:
圖5與圖6是這個系列中最最重要的圖
我們還將用圖來描述新的概念,但這兩個圖是需要你牢記在腦海中的??梢赃@樣說,我在寫Twisted程序時一直想著這兩張圖。
在我們付諸于代碼前,有三個新的概念需要闡述清楚:Transports, Protocols, Protocol Factories
Transports抽象是通過Twisted中interfaces模塊中ITransport接口定義的。一個Twisted的Transport代表一個可以收發(fā)字節(jié)的單條連接。對于我們的詩歌下載客戶端而言,就是對一條TCP連接的抽象。但是Twisted也支持諸如Unix中管道和UDP。Transport抽象可以代表任何這樣的連接并為其代表的連接處理具體的異步I/O操作細節(jié)。
如果你瀏覽一下ITransport中的方法,可能找不到任何接收數(shù)據(jù)的方法。這是因為Transports總是在低層完成從連接中異步讀取數(shù)據(jù)的許多細節(jié)工作,然后通過回調(diào)將數(shù)據(jù)發(fā)給我們。相似的原理,Transport對象的寫相關(guān)的方法為避免阻塞也不會選擇立即寫我們要發(fā)送的數(shù)據(jù)。告訴一個Transport要發(fā)送數(shù)據(jù),只是意味著:盡快將這些數(shù)據(jù)發(fā)送出去,別產(chǎn)生阻塞就行。當然,數(shù)據(jù)會按照我們提交的順序發(fā)送。
通常我們不會自己實現(xiàn)一個Transport。我們會去使用Twisted提供的實現(xiàn)類,即在傳遞給reactor時會為我們創(chuàng)建一個對象實例。
Twisted的Protocols抽象由interfaces模塊中的IProtocol定義。也許你已經(jīng)想到,Protocol對象實現(xiàn)協(xié)議內(nèi)容。也就是說,一個具體的Twisted的Protocol的實現(xiàn)應(yīng)該對應(yīng)一個具體網(wǎng)絡(luò)協(xié)議的實現(xiàn),像FTP、IMAP或其它我們自己制定的協(xié)議。我們的詩歌下載協(xié)議,正如它表現(xiàn)的那樣,就是在連接建立后將所有的詩歌內(nèi)容全部發(fā)送出去并且在發(fā)送完畢后關(guān)閉連接。
嚴格意義上講,每一個Twisted的Protocols類實例都為一個具體的連接提供協(xié)議解析。因此我們的程序每建立一條連接(對于服務(wù)方就是每接受一條連接),都需要一個協(xié)議實例。這就意味著,Protocol實例是存儲協(xié)議狀態(tài)與間斷性(由于我們是通過異步I/O方式以任意大小來接收數(shù)據(jù)的)接收并累積數(shù)據(jù)的地方。
因此,Protocol實例如何得知它為哪條連接服務(wù)呢?如果你閱讀IProtocol定義會發(fā)現(xiàn)一個makeConnection函數(shù)。這是一個回調(diào)函數(shù),Twisted會在調(diào)用它時傳遞給其一個也是僅有的一個參數(shù),即Transport實例。這個Transport實例就代表Protocol將要使用的連接。
Twisted內(nèi)置了很多實現(xiàn)了通用協(xié)議的Protocol。你可以在twisted.protocols.basic中找到一些稍微簡單點的。在你嘗試寫新Protocol時,最好是看看Twisted源碼是不是已經(jīng)有現(xiàn)成的存在。如果沒有,那實現(xiàn)一個自己的協(xié)議是非常好的,正如我們?yōu)樵姼柘螺d客戶端做的那樣。
因此每個連接需要一個自己的Protocol,而且這個Protocol是我們自己定義的類的實例。由于我們會將創(chuàng)建連接的工作交給Twisted來完成,Twisted需要一種方式來為一個新的連接創(chuàng)建一個合適的協(xié)議。創(chuàng)建協(xié)議就是Protocol Factories的工作了。
也許你已經(jīng)猜到了,Protocol Factory的API由IProtocolFactory來定義,同樣在interfaces模塊中。Protocol Factory就是Factory模式的一個具體實現(xiàn)。buildProtocol方法在每次被調(diào)用時返回一個新Protocol實例,它就是Twisted用來為新連接創(chuàng)建新Protocol實例的方法。
好吧,讓我們來看看由Twisted支持的詩歌下載客戶端2.0。源碼可以在這里twisted-client-2/get-poetry.py。你可以像前面一樣運行它,并得到相同的輸出。這也是最后一個在接收到數(shù)據(jù)時打印其任務(wù)的客戶端版本了。到現(xiàn)在為止,對于所有Twisted程序都是交替執(zhí)行任務(wù)并處理相對較少數(shù)量數(shù)據(jù)的,應(yīng)該很清晰了。我們依然通過print函數(shù)來展示在關(guān)鍵時刻在進行什么內(nèi)容,但將來客戶端不會在這樣繁鎖。
在第二個版本中,sockets不會再出現(xiàn)了。我們甚至不需要引入socket模塊也不用引用socket對象和文件描述符。取而代之的是,我們告訴reactor來創(chuàng)建到詩歌服務(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這個函數(shù)。前兩個參數(shù)的含義很明顯,不解釋了。第三個參數(shù)是我們自定義的PoetryClientFactory類的實例對象。這是一個專門針對詩歌下載客戶端的Protocol Factory,將它傳遞給reactor可以讓Twisted為我們創(chuàng)建一個PoetryProtocol實例。
值得注意的是,從一開始我們既沒有實現(xiàn)Factory也沒有去實現(xiàn)Protocol,不像在前面那個客戶端中我們?nèi)嵗覀働oetrySocket類。我們只是繼承了Twisted在twisted.internet.protocol 中提供的基類。Factory的基類是twisted.internet.protocol.Factory,但我們使用客戶端專用(即不像服務(wù)器端那樣監(jiān)聽一個連接,而是主動創(chuàng)建一個連接)的ClientFactory子類來繼承。
我們同樣利用了Twisted的Factory已經(jīng)實現(xiàn)了buildProtocol方法這一優(yōu)勢來為我們所用。我們要在子類中調(diào)用基類中的實現(xiàn):
def buildProtocol(self, address):
proto = ClientFactory.buildProtocol(self, address)
proto.task_num = self.task_num
self.task_num += 1
return proto
基類怎么會知道我們要創(chuàng)建什么樣的Protocol呢?注意,我們的PoetryClientFactory中有一個protocol類變量:
class PoetryClientFactory(ClientFactory):
task_num = 1
protocol = PoetryProtocol # tell base class what proto to build
基類Factory實現(xiàn)buildProtocol的過程是:安裝(創(chuàng)建一個實例)我們設(shè)置在protocol變量上的Protocol類與在這個實例(此處即PoetryProtocol的實例)的factory屬性上設(shè)置一個產(chǎn)生它的Factory的引用(此處即實例化PoetryProtocol的PoetryClientFactory)。這個過程如圖8所示:
http://wiki.jikexueyuan.com/project/twisted-intro/images/p05_protocols-1.png" alt="" />
正如我們提到的那樣,位于Protocol對象內(nèi)的factory屬性字段允許在都由同一個factory產(chǎn)生的Protocol之間共享數(shù)據(jù)。由于Factories都是由用戶代碼來創(chuàng)建的(即在用戶的控制中),因此這個屬性也可以實現(xiàn)Protocol對象將數(shù)據(jù)傳遞回一開始初始化請求的代碼中來,這將在第六部分看到。
值得注意的是,雖然在Protocol中有一個屬性指向生成其的Protocol Factory,在Factory中也有一個變量指向一個Protocol類,但通常來說,一個Factory可以生成多個Protocol。
在Protocol創(chuàng)立的第二步便是通過makeConnection與一個Transport聯(lián)系起來。我們無需自己來實現(xiàn)這個函數(shù)而使用Twisted提供的默認實現(xiàn)。默認情況是,makeConnection將Transport的一個引用賦給(Protocol的)transport屬性,同時置(同樣是Protocol的)connected屬性為True,正如圖9描述的一樣:
http://wiki.jikexueyuan.com/project/twisted-intro/images/p05_protocols-2.png" alt="" />
一旦初始化到這一步后,Protocol開始其真正的工作—將低層的數(shù)據(jù)流翻譯成高層的協(xié)議規(guī)定格式的消息。處理接收到數(shù)據(jù)的主要方法是dataReceived,我們的客戶端是這樣實現(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)用就意味著我們得到一個新字符串。由于與異步I/O交互,我們不知道能接收到多少數(shù)據(jù),因此將接收到的數(shù)據(jù)緩存下來直到完成一個完整的協(xié)議規(guī)定格式的消息。在我們的例子中,詩歌只有在連接關(guān)閉時才下載完畢,因此我們只是不斷地將接收到的數(shù)據(jù)添加到我們的.poem屬性字段中。
注意我們使用了Transport的getHost方法來取得數(shù)據(jù)來自的服務(wù)器信息。我們這樣做只是與前面的客戶端保持一致。相反,我們的代碼沒有必要這樣做,因為我們沒有向服務(wù)器發(fā)送任何消息,也就沒有必要知道服務(wù)器的信息了。
我們來看一下dataReceved運行時的快照。在2.0版本相同的目錄下有一個twisted-client-2/get-poetry-stack.py。它與2.0版本的不同之處只在于:
def dataReceived(self, data):
traceback.print_stack()
os._exit(0)
這樣一改,我們就能打印出跟蹤堆棧的信息,然后離開程序,可以用下面的命令來運行它:
python twisted-client-2/get-poetry-stack.py 10000
你會得到內(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()
看見沒,有我們在1.0版本客戶端的doRead回調(diào)函數(shù)。我們前面也提到過,Twisted在建立新抽象層會使用已有的實現(xiàn)而不是另起爐灶。因此必然會有一個IReadDescriptor的實例在辛苦的工作,它是由Twisted代碼而非我們自己的代碼來實現(xiàn)。如果你表示懷疑,那么就看看twisted.internet.tcp中的實現(xiàn)吧。如果你瀏覽代碼會發(fā)現(xiàn),由同一個類實現(xiàn)了IWriteDescriptor與ITransport。因此 IReadDescriptor實際上就是變相的Transport類??梢杂脠D10來形象地說明dateReceived的回調(diào)過程:
http://wiki.jikexueyuan.com/project/twisted-intro/images/p05_reactor-data-received.png" alt="" />
一旦詩歌下載完成,PoetryProtocol就會通知它的PooetryClientFactory:
def connectionLost(self, reason):
self.poemReceived(self.poem)
def poemReceived(self, poem):
self.factory.poem_finished(self.task_num, poem)
當transport的連接關(guān)閉時,conncetionLost回調(diào)會被激活。reason參數(shù)是一個twisted.python.failure.Failure的實例對象,其攜帶的信息能夠說明連接是被安全的關(guān)閉還是由于出錯被關(guān)閉的。我們的客戶端因認為總是能完整地下載完詩歌而忽略了這一參數(shù)。
工廠會在所有的詩歌都下載完畢后關(guān)閉reactor。再次重申:我們代碼的工作就是用來下載詩歌-這意味我們的PoetryClientFactory缺少復(fù)用性。我們將在下一部分修正這一缺陷。值得注意的是,poem_finish回調(diào)函數(shù)是如何通過跟蹤剩余詩歌數(shù)的:
...
self.poetry_count -= 1
if self.poetry_count == 0:
...
如果我們采用多線程以讓每個線程分別下載詩歌,這樣我們就必須使用一把鎖來管理這段代碼以免多個線程在同一時間調(diào)用poem_finish。但是在交互式體系下就不必擔(dān)心了。由于reactor只能一次啟用一個回調(diào)。
新的客戶端實現(xiàn)在處理錯誤上也比先前的優(yōu)雅的多,下面是PoetryClientFactory處理錯誤連接的回調(diào)實現(xiàn)代碼:
def clientConnectionFailed(self, connector, reason):
print 'Failed to connect to:', connector.getDestination()
self.poem_finished()
注意,回調(diào)是在工廠內(nèi)部而不是協(xié)議內(nèi)部實現(xiàn)。由于協(xié)議是在連接建立后才創(chuàng)建的,而工廠能夠在連接未能成功建立時捕獲消息。
版本2的客戶端使用的抽象對于那些Twisted高手應(yīng)該非常熟悉。如果僅僅是為在命令行上打印出下載的詩歌這個功能,那么我們已經(jīng)完成了。但如果想使我們的代碼能夠復(fù)用,能夠被內(nèi)嵌在一些包含詩歌下載功能并可以做其它事情的大軟件中,我們還有許多工作要做,我們將在第六部分講解相關(guān)內(nèi)容。
本部分原作參見: dave @ http://krondo.com/?p=1522
本部分翻譯內(nèi)容參見楊曉偉的博客 http://blog.sina.com.cn/s/blog_704b6af70100q2ac.html