twisted系列教程五–改进twisted poetry client
在第四部分我們寫了我們的第一個twisted client.它工作的非常好,但仍舊有提升的空間
首先,這個twisted client 包含了一些比如創(chuàng)建sockets 和從這些sockets 中接收數(shù)據(jù)的細(xì)節(jié).twisted 已經(jīng)對這些操作提供了支持,所以我們沒有必要自己再去實現(xiàn).這個特別有用因為異步的I/O在處理異常方面需要一些技巧,如果需要支持多平臺的話需要更多的技巧.如果你有一個空閑的下午,你可以在twisted 源碼中搜索以下win32,你可以看到因為平臺問題到底引入了多少問題.
另外一個問題就是出錯處理,試著在沒有啟動server的情況下運行twisted client, 它會崩潰掉,我們會修復(fù)現(xiàn)在的twisted client,用twisted 的api 你會發(fā)現(xiàn)這很容易.
最后,這個client 不是特別可以復(fù)用.另外一個模塊怎樣來用我們的client? “calling” module 是怎樣知道我們什么時候執(zhí)行完下載的? 我們不能僅僅寫一個函數(shù)直接返回結(jié)果,那樣會引起阻塞.這些不是我們今天要全部解決的問題,我們會在后面的幾篇教程中解決最后一個問題.
我們今天要解決是是第一個和第二個問題,主要是通過一些高級的api 和一些接口.twisted 框架是抽象層松耦合的,學(xué)習(xí)twisted 就是要學(xué)習(xí)這些層能提供什么.比如,,每一層能提供什么api,接口,還有什么實現(xiàn)是可用的.我們不會詳細(xì)的講解每一層的抽象,我們僅僅去看一些最重要的碎片去理解twisted是怎樣被組合起來的.一但你對twisted 的結(jié)構(gòu)全貌了解了,你再去學(xué)習(xí)twisted 的其他的部分就會容易很多.
一般來說,每一個twisted 的抽象都跟一個特別的概念相關(guān).比如, 第四部分中的twisted client 使用的 IReadDescriptor 就是一個抽象–”可以讀取內(nèi)容的文件描述符”.一個twisted 的抽象被一個接口定義,這個接口指定了一個被這個抽象實例化了的對象應(yīng)該有什么樣的表現(xiàn).你要記住的是當(dāng)你學(xué)一個twisted 的抽象時:
大多數(shù)的高層的抽象是用底層的建立起來的,而不是替換它們
所以當(dāng)你學(xué)習(xí)一個新的twisted 抽象時,記住它做了什么和沒做什么.特別的,對于一些早期的抽象,可能抽象A 實現(xiàn)了特色F,然后F再也沒有被其他的抽象實現(xiàn).當(dāng)然,假如抽象B需要特色F,它用使用A而不是再去實現(xiàn)一遍F.
網(wǎng)絡(luò)是個復(fù)雜的主題,因此twisetd 中包含很多種抽象.從低級的抽象開始,我們可以清晰的得到一個結(jié)構(gòu)–它們在一個正常運行的系統(tǒng)中是怎樣結(jié)合在一起的.
Loopiness in the Brain
reactor是我們目前接觸的最重要的抽象,也是twisetd中最重要的抽象.在一個twisted程序的中間,不管你的程序有多少層,總有一個reactor 循環(huán)在來維持著這個程序不停的運行.twisted 中的其他的抽象都不會提供這樣的功能.twisted 的其他的部分都可以被看做可以讓我們更容易使用reactor 的物體,比如做一個web服務(wù)或者查詢一個sql語句.可能我們還會用一個些底層的api,就像twisetd client 1.0,但是這就需要我們自己去實現(xiàn)更多的內(nèi)容.如果用高級api則就意味著我們需要寫更少的代碼.
但是在我們寫twisted 外層的東西的時候我們經(jīng)常忘了reactor 的存在,在一個正常的twisted 的應(yīng)用中,只有很少的一部分會直接用到reactor api.對于一些底層的抽象來說也是這樣.我們在twisted client 1.0 中的使用的文件描述符抽象就被高級的抽象包裝,以至于在實際的twisted程序中并不會被用到.
就文件描述符來說,這并不是一個問題.讓twisted 來處理異步的I/O 可以讓我們專注于我們要解決的問題. 對于reactor 就不一樣了,reactor 不會不出現(xiàn),當(dāng)你選擇使用twisted時候你已經(jīng)選擇了reactor模式,這就意味著 reactor模式的代碼會用callback 和 多任務(wù)合作. 如果你想用好twisted,你必須時候想著reactor 的存在,我們會在第六章講關(guān)于這個的更多的內(nèi)容,現(xiàn)在你要記住的是:
圖片五和圖片六是最重要的兩張圖片
我們將會繼續(xù)使用圖片來說明新的概念,但這兩張圖片你需要印到你的心里.這兩張圖片確實在我心里在我用twisted寫代碼的時候.
在我們進入代碼之前,還有三個抽象需要我們?nèi)チ私?Transports,Protocols和Protocol Factories.
Transports
Transport 抽象在twisted interfaces 模塊中被ITransport 定義,一個twisted transport 代表了一個可以接收和發(fā)送數(shù)據(jù)的連接.在我們的client 中tranport 就想一個tcp 連接.但是twisted也支持UNIX PIPES 和 UDP socket 類型的I/O. transport 抽象代表了任何一個它們其中的一種連接,并處理每種連接帶來的各種異步I/O的細(xì)節(jié).
如果你查看了ITransport 中定義的方法,你不會發(fā)現(xiàn)有用來接收數(shù)據(jù)的方法.那是因為transport 被用來從連接中異步的讀數(shù)據(jù),然后把讀到的數(shù)據(jù)傳遞通過callback傳遞給我們.所以transport 的寫相關(guān)的方法為了防止阻塞并不是立即把數(shù)據(jù)交給我們.告訴transport 去寫數(shù)據(jù)意味著:在不產(chǎn)生阻塞的情況下盡快的把數(shù)據(jù)傳送出去.
我們并不會實現(xiàn)我們自己的transport 或者創(chuàng)建一個,而是我們用twsited 已經(jīng)幫我們實現(xiàn)的transport,reactor 在創(chuàng)建連接的時候會幫我們創(chuàng)建transport.
Protocols
twsited protocol 是在interfaces模塊中被IProtocol 定義.Protocol 對象實現(xiàn)了protocol,也就是說,一個twisted Protocol 的實現(xiàn)應(yīng)該是實現(xiàn)了一個特別的網(wǎng)絡(luò)協(xié)議,就像FTP或者IMAP或者其他的我們自己實現(xiàn)的協(xié)議.我們的poetry protocol,也是一種協(xié)議,在連接建立之后開始傳送數(shù)據(jù),連接斷開表示詩已經(jīng)傳送完畢.
嚴(yán)格的來說,每一個twisted Protocol 對象的實例都為一個特別的連接實現(xiàn)一個協(xié)議.所以我們程序的每一個連接都會需要一個Protocol 的實例.這就讓Protocol 實例自然而然的成為了存儲協(xié)議狀態(tài)和部分累積數(shù)據(jù)(因為在異步io中我們每一次接收的數(shù)據(jù)塊不是固定大小的)的地方.
所以Protocol 實例是怎樣知道它是在為哪一個連接負(fù)責(zé)? 假如我們看一下IProtocol 的定義,我們將會看到makeConnection.這個方法是一個callback,twisted代碼會調(diào)用它,一個transport 實例作為參數(shù).transport 會被連接和協(xié)議用到.
twisted 已經(jīng)包含了大量的協(xié)議的實現(xiàn).你可以看到一些簡單的協(xié)議在twisted.protocols.basic,在你想寫一個新的協(xié)議之前你最好去查找一下源代碼看有沒有已經(jīng)實現(xiàn)的協(xié)議符合你的要求.如果沒有的話,去實現(xiàn)你自己的協(xié)議也是很簡單的事情.
Protocol Factories
每一個連接需要它自己的Protocol,這個Protocol 應(yīng)該是我們實現(xiàn)的一個類的實例.既然我們讓twisted 來處理創(chuàng)建連接,twisted 需要一種當(dāng)一個連接被建立時就會有一個合適的protocol為它準(zhǔn)備好的方法. 創(chuàng)建Protocol實例的任務(wù)就交給Protocol Factories.
也許你已經(jīng)猜到,Protocol Factory API 被IProtocolFactory 定義,也在interfaces 模塊中.Protocol Factories 是工場設(shè)計模式的一個例子. buildProtocol 在每次調(diào)用的時候會返回一個Protocol 的實例,這個就是twisted 用來為每個新的連接新建protocol 的方法.
Get Poetry 2.0: First Blood.0
現(xiàn)在讓我們來看一下Twisted poery client 的2.0 版本.代碼在twisted-client-2/get-poetry.py.你可以運行它然后它的輸出和1.0 版本的很像.這個也會是最后一個會打印任務(wù)號的client.到現(xiàn)在為止你應(yīng)該對多任務(wù)交互運行和每一次只會讀取一小塊數(shù)據(jù)比較清楚了.我們?nèi)耘f會用打印語句來告訴你現(xiàn)在運行到什么地方了,但是在不久的將來這些代碼不會這么冗長.
在client 2.0 中,socket 已經(jīng)消失了.我甚至不用導(dǎo)入socket 模塊,并且我們永遠(yuǎn)不會再提到socket,以及文件描述符了.我們用以下的代碼讓reactor 去創(chuàng)造連接:
factory = PoetryClientFactory(len(addresses))
from twisted.internet import reactor
for address in addresses:
????host, port = address
????reactor.connectTCP(host, port, factory)
我們可以來看一下connectTCP,前面的兩個參數(shù)是自解釋的,第三個是PoetryClientFactory 的實例,這個是poetry client 的的Protocol Factory,傳遞給reactor,然后twisted 就可以創(chuàng)造PoetryProtocol.
注意一下我們實現(xiàn)Factory 或 Protocol 的時候并不是亂寫的,不像PoetrySocket對象,我們是繼承了twisted 提供的twisted.internet.protocol和twisted.internet.protocol.Factory.但我們這里是用的ClientFactory
clientfactory 是專門為client 準(zhǔn)備的(client 是建立連接,而server 是在等待連接).
我們也利用twsited factory 實現(xiàn)了buildProtocol,我們在子類中調(diào)用了父類中的buildProtocol方法:
def buildProtocol(self, address):
????proto = ClientFactory.buildProtocol(self, address)
????proto.task_num = self.task_num
????self.task_num += 1
????return proto
基類是怎樣知道去建立那些protocol呢?注意我們已經(jīng)在PoetryClientFactory 中設(shè)置protocol 屬性了:
class PoetryClientFactory(ClientFactory):
????task_num = 1
????protocol = PoetryProtocol # tell base class what proto to build
Factory 基類通過我們設(shè)置的protocol 屬性來實現(xiàn)buildProtocol,并在新建立的protocol實例上設(shè)置factory屬性,這時的factory是ProtocolFactory 的引用.這個過程可以用圖片八來表明:
圖片八
就像我們在上面提到的,factory 做為Protocol 的屬性,這樣被創(chuàng)造出來的Protocol 就可以通過factory來進行共享狀態(tài).由于factory 是被我們自己寫的代碼控制的(相對于被twisted 控制的部分),擁有相同的factory 屬性可以讓protocol 對象把運行時的狀態(tài)交給我們寫的代碼.(這兩段是根據(jù)自己的理解翻譯過來的), 在第六部分我們會看到具體的應(yīng)用.
注意,在factory 做Protocol 屬性的時候,這時的factory 是 Protocol Factory 的一個實例,factory 的protocol 屬性是Protocol 類的引用而不是一個實例的引用,因為一個factory會創(chuàng)建多個protocol實例.
Protocol 結(jié)構(gòu) 用transport 連接一個protocol 的第二個階段是用makeConnection方法.我們沒有必要自己實現(xiàn)這個方法因為twisted 的基類提供了一個默認(rèn)的實現(xiàn).默認(rèn)的,makeConnection有一個引用了Transport 的叫做transport的屬性,并設(shè)置了connected屬性為True,圖片九描述了這個過程:
?
?
圖片九
一但初始完這些,Protocol 就可以干點真正的工作了–把底層的數(shù)據(jù)流轉(zhuǎn)化為高級的協(xié)議信息數(shù)據(jù)流,處理數(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())
每一次dataReceived 被調(diào)用,我們就會以字符串的形式得到一個新的字節(jié)序列.因為是異步IO操作,我們不清楚我們會得到多少數(shù)據(jù),所以我們會進行緩沖直到有一個完整的protocol 消息.在我們的client 中,詩不會停止直到連接斷開,所以我們不停的像我們的.poem屬性中增加字節(jié).
我們在Transport中使用getHost 方法來區(qū)別這些數(shù)據(jù)來自哪一個server,我們以前的client中也是這么做的.否則我們的代碼沒有必要用Tansport.因為我們不用向server端送出任何數(shù)據(jù).
讓我們快速的看一下當(dāng)dataReceived 被調(diào)用的時候發(fā)生了什么?在我們的client 2.0相同目錄下,有一個client叫做 twisted-client-2/get-poetry-stack.py,與2.0 版本不同的是dataReceived 被改成了這樣:
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()
你會看到我們在twised client 1.0 中用到的doRead callback .就像我們之前說的,twisted 用底層的抽象來建立上一層的抽象,并不是替換掉它們.所以在這個過程中仍就會有一個IReadDescriptor的實現(xiàn)在起作用,它僅僅被twisted 實現(xiàn)了而不是我們自己的代碼.如果你好奇的話,twisted 的實現(xiàn)在 twisted.internet.tcp 中.如果你跟蹤源代碼,你會發(fā)現(xiàn)有一些對象實現(xiàn)了IWriteDescriptor 和 ITransport.所以Transport 最終是IReadDescriptor 的表現(xiàn)形式.我們可以用一張圖片來形象化dataReceived, 圖片十:
圖片十
一但一首小詩完成了下載,PoetryProtocol 對象會通知PoetryClientFactory:
def connectionLost(self, reason):
????self.poemReceived(self.poem)
def poemReceived(self, poem):
????self.factory.poem_finished(self.task_num, poem)
connectionLost callback 被觸發(fā)當(dāng)transport 的連接斷開之后,reason 參數(shù)是一個twisted.python.failure.Failure對象帶著額外的信息來告訴我們這個連接是正常的斷開還是由于錯誤.我們的client 忽略這個值假設(shè)我們已經(jīng)接收到整個的詩.
factory 會停掉reactor 在所有的詩被處理完之后.再一次的假設(shè)我們的的程序唯一做的事情就是下載詩,這樣就造成我們的PoetryClientFactory 對象不可重用,我們會解決這個問題在下一部分.但是要注意poem_finished callback 是怎樣記錄剩余沒有完成的詩的數(shù)量.
...
????self.poetry_count -= 1
????if self.poetry_count == 0:
????????...
假如我們正在寫一個多進程程序,每一首詩在一個不同的進程中進行,我們應(yīng)該保護這部分的代碼用一個鎖防止兩個進程或多個進程同時調(diào)用poem_finished.否則的話 我們可能會關(guān)閉reactor 兩次.但是在一個reactive 的系統(tǒng)中我們不需要這樣.reactor 一次只能運行一個callback,所以這種情況不會發(fā)生.
我們的新的client 在處理錯誤的方面比client 1.0 更優(yōu)雅.下面是PoetryClientFactory 類中的出錯處理:
def clientConnectionFailed(self, connector, reason):
????print 'Failed to connect to:', connector.getDestination()
????self.poem_finished()
注意這個callback 是在factory里,不是在protocol 里.因為protocol 是在連接建立之后才被創(chuàng)建的.factory 會獲取到信息當(dāng)一個連接不能被建立的時候.
A simpler client
盡管我們的client 已經(jīng)很簡單了,我們可以讓它更簡單如果不用任務(wù)號的話,這個是簡化了的2.1 版本:twisted-client-2/get-poetry-simple.py
Wrapping Up
client 2.0 版本使用了twisted 用戶比較熟悉的抽象.假如我們需要一個命令行的client,打印出詩然后退出,我們可以停在這里然后讓我們的程序停止.但是假如我們需要一些可復(fù)用的代碼,一些可以用在一個大型系統(tǒng)里面的代碼,不光用來下載詩還用來做其他的事情,我們?nèi)耘f有很多工作要去做.在第六部分我們將會近一步的嘗試.
?
?
?
總結(jié)
以上是生活随笔為你收集整理的twisted系列教程五–改进twisted poetry client的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: VC++CopyFile函数的用法
- 下一篇: Centos7 ubuntu 安装Tel