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

抽象地構建客戶端html

在第四部分中,咱們構建了第一個使用Twisted的客戶端。它確實能很好地工做,但仍有提升的空間。python


    首先是,這個客戶端居然有建立網絡端口並接收端口處的數據這樣枯燥的代碼。Twisted理應爲咱們實現這些例程性功能,免得咱們每次寫一個新的程序時都要去本身實現。Twisted這樣作也將咱們從像異步I/O操做中包括許多像異常處理這樣的細節處理解放出來。更多的細節處理存在於多平臺上運行咱們的代碼中。若是你那個下午有空,能夠翻翻TwistedWIN32實現源代碼,看看裏面有多少小針線是來處理跨平臺的。react

另外一問題是與錯誤處理有關。當運行版本1Twisted客戶端來從並無提供服務的端口上下載詩歌時,它就會崩潰。咱們是能夠修正這個錯誤,但經過下面咱們要介紹TwistedAPIs來處理這些類型的錯誤會更簡單。git

最後,那個客戶端也不能複用。若是有另外一個模塊須要經過咱們的客戶端下載詩歌呢?人家怎麼知道你的詩歌已經下載完畢?咱們不能用一個方法簡單地將一首詩下載完成後再傳給人家,而在以前讓人家處於等待狀態。這確實是一個問題,但咱們不許備在這個部分解決這個問題—在將來的部分中必定會解決這個問題。github


    咱們將會使用一些高層次的APIs和接口來解決第1、二個問題。Twisted框架是由衆多抽象層鬆散地組合起來的。所以,學習Twisted也就意味着須要學習這些層都提供什麼功能,例如每層都有哪些APIs,接口和實例可供使用。接下來咱們會經過剖析Twisted最最重要的部分來更好地感覺一下Twisted都是怎麼組織的。一旦你對Twisted的整個結構熟悉了,學習新的部分會簡單多了。數據庫

通常來講,每一個Twisted的抽象都只與一個特定的概念相關。例如,第四部分中的客戶端使用的IReadDescriptor,它就是「一個能夠讀取字節的文件描述符」的抽象。一個抽象每每會經過定義接口來指定那些想實現個抽象(也就是實現這個接口)對象的形爲。在學習新的Twisted抽象概念時,最須要謹記的就是:編程

多數高層次抽象都是在低層次抽象的基礎上創建的,不多有另立門戶的。緩存

    所以,你在學習新的Twisted抽象概念時,始終要記住它作什麼和不作什麼。特別是,若是一個早期的抽象A實現了F特性,那麼F特性不太可能再由其它任何抽象來實現。另外,若是另一個抽象須要F特性,那麼它會使用A而不是本身再去實現F。(一般的作法,B可能會經過繼承A或得到一個指向A實例的引用)安全

    網絡很是的複雜,所以Twisted包含不少抽象的概念。經過從低層的抽象講起,咱們但願能更清楚起看到在一個Twisted程序中各個部分是怎麼組織起來的。服務器


核心的循環體

    第一個咱們要學習的抽象,也是Twisted中最重要的,就是reactor。在每一個經過Twisted搭建起來的程序中心處,無論你這個程序有多少層,總會有一個reactor循環在不中止地驅動程序的運行。再也沒有比reactor提供更加基礎的支持了。實際上,Twisted的其它部分(即除了reactor循環體)能夠這樣理解:它們都是來輔助X來更好地使用reactor,這裏的X能夠是提供Web網頁、處理一個數據庫查詢請求或其它更加具體內容。儘管堅持像上一個客戶端同樣使用低層APIs是可能的,但若是咱們執意那樣作,那麼咱們必需本身來實現很是多的內容。而在更高的層次上,意味着咱們能夠少寫不少代碼。

    可是當在外層思考與處理問題葉。很容易就忘記了reactor的存在了。在任何一個常見大小的Twisted程序中 ,確實不多會有直接與reactorAPIs交互。低層的抽象也是同樣(即咱們不多會直接與其交互)。咱們在上一個客戶端中用到的文件描述符抽象,就被更高層的抽象更好的概括而至於咱們不多會在真正的Twisted程序中遇到。(他們在內部依然在被使用,只是咱們看不到而已)

    至於文件描述符抽象的消息,這並非一個問題。讓Twisted掌舵異步I/O處理,這樣咱們就能夠更加關注咱們實際要解決的問題。但對於reactor不同,它永遠都不會消失。當你選擇使用Twisted,也就意味着你選擇使用Reactor模式,而且意味着你須要使用回調與多任務合做的「交互式」編程方式。若是你想正確地使用Twisted,你必須牢記reactor的存在。咱們將在第六部分更加詳細的講解部份內容。可是如今要強調的是:


5與圖6是這個系列中最最重要的圖


    咱們還將用圖來描述新的概念,但這兩個圖是須要你牢記在腦海中的。能夠這樣說,我在寫Twisted程序時一直想着這兩張圖。

在咱們付諸於代碼前,有三個新的概念須要闡述清楚:Transports,Protocols,Protocol Factoies


Transports

    Transports抽象是經過Twistedinterfaces模塊中ITransport接口定義的。一個TwistedTransport表明一個能夠收發字節的單條鏈接。對於咱們的詩歌下載客戶端而言,就是對一條TCP鏈接的抽象。可是Twisted也支持諸如Unix中管道和UDPTransport抽象能夠表明任何這樣的鏈接併爲其表明的鏈接處理具體的異步I/O操做細節。

    若是你瀏覽一下ITransport中的方法,可能找不到任何接收數據的方法。這是由於Transports老是在低層完成從鏈接中異步讀取數據的許多細節工做,而後經過回調將數據發給咱們。類似的原理,Transport對象的寫相關的方法爲避免阻塞也不會選擇當即寫咱們要發送的數據。告訴一個Transport要發送數據,只是意味着:儘快將這些數據發送出去,別產生阻塞就行。固然,數據會按照咱們提交的順序發送。

    一般咱們不會本身實現一個Transport。咱們會去實現Twisted提供的類,即在傳遞給reactor時會爲咱們建立一個對象實例。


Protocols

    TwistedProtocols抽象由interfaces模塊中的IProtocol定義。也許你已經想到,Protocol對象實現協議內容。也就是說,一個具體的TwistedProtocol的實現應該對應一個具體網絡協議的實現,像FTPIMAP或其它咱們本身規定的協議。咱們的詩歌下載協議,正如它表現的那樣,就是在鏈接創建後將全部的詩歌內容所有發送出去而且在發送完畢後關閉鏈接。

    嚴格意義上講,每個TwistedProtocols類實例都爲一個具體的鏈接提供協議解析。所以咱們的程序每創建一條鏈接(對於服務方就是每接受一條鏈接),都須要一個協議實例。這就意味着,Protocol實例是存儲協議狀態與間斷性(因爲咱們是經過異步I/O方式以任意大小來接收數據的)接收並累積數據的地方。

    所以,Protocol實例如何得知它爲哪條鏈接服務呢?若是你閱讀IProtocol定義會發現一個makeConnection函數。這是一個回調函數,Twisted會在調用它時傳遞給其一個也是僅有的一個參數,即就是Transport實例。這個Transport實例就表明Protocol將要使用的鏈接。

    Twisted包含不少內置能夠實現不少通用協議的Protocol。你能夠在twisted.protocols.basic找到一些稍微簡單點的。在你嘗試寫新Protocol時,最好是看看Twisted源碼是否是已經有現成的存在。若是沒有,那實現一個本身的協議是很是好的,正如咱們爲詩歌下載客戶端作的那樣。


Protocol Factories

    所以每一個鏈接須要一個本身的Portocol,並且這個Protocol是咱們本身定義類的實例。因爲咱們會將建立鏈接的工做交給Twisted來完成,Twisted須要一種方式來爲一個新的鏈接制定一個合適的協議。制定協議就是Protocol Factories的 工做了。

    也許你已經猜到了,Protocol FactoryAPIIProtocolFactory來定義,一樣在interfaces模塊中。Protocol Factory就是Factory模式的一個具體實現。buildProtocol方法在每次被調用時返回一個新Protocol實例。它就是Twisted用來爲新鏈接建立新Protocol實例的方法。


詩歌下載客戶端2.0:第一滴心血

    好吧,讓咱們來看看由Twisted支持的詩歌下載客戶端2.0。源碼能夠在這裏twisted-client-2/get-poetry.py。你能夠像前面同樣運行它,並獲得相同的輸出。這也是最後一個在接收到數據時打印其任務的客戶端版本了。到如今爲止,對於全部Twisted程序都是交替執行任務並處理相對較少數量數據的,應該很清晰了。咱們依然經過print函數來展現在關鍵時刻在進行什麼內容,但未來客戶端不會在這樣繁鎖。

    在第二個版本中,sockets不會再出現了。咱們甚至不須要引入socket模塊也不用引用socket對象和文件描述符。取而代之的是,咱們告訴reactor來建立到詩歌服務器的鏈接,代碼以下面所示:

factory = PoetryClientFactory(len(addresses))
 
from twisted.internet import reactor
 
for address in addresses:
    host, port = address
    reactor.connectTCP(host, port, factory)

咱們須要關注的是connectTCP這個函數。前兩個參數的含義很明顯,不解釋了。第三個參數是咱們自定義的PoetryClientFactory類的實例對象。這是一個專門針對詩歌下載客戶端的Protocol Factory,將它傳遞給reactor可讓Twisted爲咱們建立一個PeotryProtocol實例。

值得注意的是,從一開始咱們既沒有實現Factory也沒有去實現Protocol,不像在前面那個客戶端中咱們去實例化咱們PoetrySocket類。咱們只是繼承了Twistedtwisted.internet.protocol 中提供的基類Factory的基類是twisted.internet.protocol.Factory但咱們使用客戶端專用(即不像服務器端那樣監聽一個鏈接,而是主動建立一個鏈接)的ClientFactory子類來繼承。

咱們一樣利用了TwistedFactory已經實現了buildProtocol方法這一優點來爲咱們所用。咱們要在子類中調用基類中的實現:

def buildProtocol(self, address):
    proto = ClientFactory.buildProtocol(self, address)
    proto.task_num = self.task_num
    self.task_num += 1
    return proto

基類怎麼會知道咱們要建立什麼樣的Protocol呢?注意,咱們的PoetryClientFactory中有一個protocol類變量:

class PoetryClientFactory(ClientFactory):
 
    task_num = 1
 
    protocol = PoetryProtocol # tell base class what proto to build

基類Factory的實現buildProtocol過程是:安裝(建立一個實例)咱們設置在protocol變量上的Protocol類與在這個實例(此處即PoetryProtocol的實例)的factory屬性上設置一個產生它的Factory的引用(此處即實例化PoetryProtocolPoetryClientFactory)。這個過程如圖8所示:

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

8Protocol的生成過程

正如咱們提到的那樣,位於Protocol對象內的factory屬性字段容許在都由同一個factory產生的Protocol之間共享數據。因爲Factories都是由用戶代碼來建立的(即在用戶的控制中),所以這個屬性也能夠實現Protocol對象將數據傳遞迴一開始初始化請求的代碼中來,這將在第六部分看到。

值得注意的是,雖然在Protocol中有一個屬性指向生成其的Protocol Factory,在Factory中也有一個變量指向一個Protocol類,但一般來講,一個Factory能夠生成多個Protocol

Protocol創立的第二步即是經過makeConnection與一個Transport聯繫起來。咱們無需本身來實現這個函數而使用Twisted提供的默認實現。默認狀況是,makeConnectionTransport的一個引用賦給(Protocol的)transport屬性,同時置(一樣是Protocol的)connected屬性爲True,正如圖9描述的同樣:

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

9Protocol遇到其Transport

一旦初始化到這一步後,Protocol開始其真正的工做—將低層的數據流翻譯成高層的協議規定格式的消息。處理接收到數據的主要方法是dataReceived,咱們的客戶端是這樣實現的:

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被調用就意味着咱們獲得一個新字符串。因爲與異步I/O交互,咱們不知道能接收到多少數據,所以將接收到的數據緩存下來直到完成一個完整的協議規定格式的消息。在咱們的例子中,詩歌只有在鏈接關閉時才下載完畢,所以咱們只是不斷地將接收到的數據添加到咱們的.poem屬性字段中。

注意咱們使用了TransportgetHost方法來取得數據來自的服務器信息。咱們這樣作只是與前面的客戶端保持一致。相反,咱們的代碼沒有必要這樣作,由於咱們沒有向服務器發送任何消息,也就沒有必要知道服務器的信息了。

咱們來看一下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

你會獲得內容以下的跟蹤堆棧:

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回調函數。咱們前面也提到過,Twisted在創建新抽象層進會使用已有的實現而不是另起爐竈。所以必然會有一個IReadDescriptor的實例在辛苦的工做,它是由Twisted代碼而非咱們本身的代碼來實現。若是你表示懷疑,那麼就看看twisted.internet.tcp中的實現吧。若是你瀏覽代碼會發現,由同一個類實現了IWriteDescriptor與ITransport。所以 IreadDescriptor實際上就是變相的Transport類。能夠用圖10來形象地說明dateReceived的回調過程:
第五部分:由Twisted支持的詩歌客戶端

 圖10:dateReceived回調過程
一旦詩歌下載完成,PoetryProtocol就會通知它的PooetryClientFactory:
def connectionLost(self, reason):     
  self.poemReceived(self.poem) 
def poemReceived(self, poem):    
  self.factory.poem_finished(self.task_num, poem)

transport的鏈接關閉時,conncetionLost回調會被激活。reason參數是一個twisted.python.failure.Failure的實例對象,其攜帶的信息可以說明鏈接是被安全的關閉仍是因爲出錯被關閉的。咱們的客戶端因認爲老是能完整地下載完詩歌而忽略了這一參數。

工廠會在全部的詩歌都下載完畢後關閉reactor。再次重申:咱們代碼的工做就是用來下載詩歌-這意味咱們的PoetryClientFactory缺乏複用性。咱們將在下一部分修正這一缺陷。值得注意的是,poem_finish回調函數是如何經過跟蹤剩餘詩歌數的:

...
    self.poetry_count -= 1
 
    if self.poetry_count == 0:
 
 …

若是咱們採用多線程以讓每一個線程分別下載詩歌,這樣咱們就必須使用一把鎖來管理這段代碼以避免多個線程在同一時間調用poem_finish。可是在交互式體系下就沒必要擔憂了。因爲reactor只能一次啓用一個回調。

新的客戶端實如今處理錯誤上也比先前的優雅的多,下面是PoetryClientFactory處理錯誤鏈接的回調實現代碼:

def clientConnectionFailed(self, connector, reason):
    print 'Failed to connect to:', connector.getDestination()
    self.poem_finished()

注意,回調是在工廠內部而不是協議內部實現。因爲協議是在鏈接創建後才建立的,而工廠可以在鏈接未能成功創建時捕獲消息。



結束語:

版本2的客戶端使用的抽象對於那些Twisted高手應該很是熟悉。若是僅僅是爲在命令行上打印出下載的詩歌這個功能,那麼咱們已經完成了。但若是想使咱們的代碼可以複用,可以被內嵌在一些包含詩歌下載功能並能夠作其它事情的大軟件中,咱們還有許多工做要作,咱們將在第六部分講解相關內容。

相關文章
相關標籤/搜索