Python的Twisted事件驅動的網路引擎框架
Python的Twisted事件驅動的網路引擎框架
概述
Twisted是用Python實現的基於事件驅動的網路引擎框架。Twisted支援許多常見的傳輸及應用層協議,包括TCP、UDP、SSL/TLS、HTTP、IMAP、SSH、IRC以及FTP。
優越性
- 使用基於事件驅動的程式設計模型,而不是多執行緒模型。
- 跨平臺:為主流作業系統平臺暴露出的事件通知系統提供統一的介面。
- “內建電池”的能力:提供流行的應用層協議實現,因此Twisted馬上就可為開發人員所用。
- 符合RFC規範,已經通過健壯的測試套件證明了其一致性。
- 能很容易的配合多個網路協議一起使用。
- 可擴充套件。
事件驅動程式設計
事件驅動程式設計是一種程式設計正規化,這裡程式的執行流由外部事件來決定。它的特點是包含一個事件迴圈,當外部事件發生時使用回撥機制來觸發相應的處理。另外兩種常見的程式設計正規化是(單執行緒)同步以及多執行緒程式設計。事件驅動模型也就是我們常說的觀察者,或者釋出-訂閱模型;理解它的幾個關鍵點:
-
一種物件間的一對多的關係;最簡單的如交通訊號燈,訊號燈是目標(一方),行人注視著訊號燈(多方);
-
當目標傳送改變(釋出),觀察者(訂閱者)就可以接收到改變;
-
觀察者如何處理(如行人如何走,是快走/慢走/不走,目標不會管的),目標無需干涉;所以就鬆散耦合了它們之間的關係。
下圖表現了單執行緒、多執行緒和事件驅動的關係:
- 在單執行緒同步模型中,任務按照順序執行。如果某個任務因為I/O而阻塞,其他所有的任務都必須等待,直到它完成之後它們才能依次執行。這種明確的執行順序和序列化處理的行為是很容易推斷得出的。如果任務之間並沒有互相依賴的關係,但仍然需要互相等待的話這就使得程式不必要的降低了執行速度。
- 在多執行緒版本中,這3個任務分別在獨立的執行緒中執行。這些執行緒由作業系統來管理,在多處理器系統上可以並行處理,或者在單處理器系統上交錯執行。這使得當某個執行緒阻塞在某個資源的同時其他執行緒得以繼續執行。與完成類似功能的同步程式相比,這種方式更有效率,但程式設計師必須寫程式碼來保護共享資源,防止其被多個執行緒同時訪問。多執行緒程式更加難以推斷,因為這類程式不得不通過執行緒同步機制如鎖、可重入函式、執行緒區域性儲存或者其他機制來處理執行緒安全問題,如果實現不當就會導致出現微妙且令人痛不欲生的bug。
- 在事件驅動版本的程式中,3個任務交錯執行,但仍然在一個單獨的執行緒控制中。當處理I/O或者其他昂貴的操作時,註冊一個回撥到事件迴圈中,然後當I/O操作完成時繼續執行。回撥描述了該如何處理某個事件。事件迴圈輪詢所有的事件,當事件到來時將它們分配給等待處理事件的回撥函式。這種方式讓程式儘可能的得以執行而不需要用到額外的執行緒。事件驅動型程式比多執行緒程式更容易推斷出行為,因為程式設計師不需要關心執行緒安全問題。
什麼情況可以使用事件驅動
- 程式中有許多工
- 任務之間高度獨立
- 在等待事件到來時,某些任務會阻塞。
一句話,不需要同步處理的多工處理就可以使用事件驅動了。
Twisted
Twisted中的客戶端和伺服器是用Python開發的,採用了一致性的介面。這使得開發新的客戶端和伺服器變得很容易實現,可以在客戶端和伺服器之間共享程式碼,在協議之間共享應用邏輯,以及對某個實現的程式碼做測試。Twisted採用了Reactor設計模式,其核心就是Reactor的事件迴圈。Reactor可以感知網路、檔案系統以及定時器事件。它等待然後處理這些事件,從特定於平臺的行為中抽象出來,並提供統一的介面,使得在網路協議棧的任何位置對事件做出響應都變得簡單。
下面以獲取一個URL頁面程式碼為例,同步呼叫方式如下:
import getPage
def processPage(page):
print page
def logError(error):
print error
def finishProcessing(value):
print "Shutting down..."
exit(0)
url = "http://google.com"
try:
page = getPage(url)
processPage(page)
except Error, e:
logError(error)
finally:
finishProcessing()
一非同步的方式獲取如下:
from twisted.internet import reactor
import getPage
def processPage(page):
print page
finishProcessing()
def logError(error):
print error
finishProcessing()
def finishProcessing(value):
print "Shutting down..."
reactor.stop()
url = "http://google.com"
# getPage takes: url,
# success callback, error callback
getPage(url, processPage, logError)
reactor.run()
從上面非同步程式碼段可以看出,如果編寫丟失了reactor.stop()就會進入死迴圈,Twisted應對這種複雜性的方式是新增一個稱為Deferred(延遲)的物件。
Deferreds
Deferred物件以抽象化的方式表達了一種思想,即結果還尚不存在。它同樣能夠幫助管理產生這個結果所需要的回撥鏈。Deferred物件包含一對回撥鏈,一個是針對操作成功的回撥,一個是針對操作失敗的回撥。初始狀態下Deferred物件的兩條鏈都為空。在事件處理的過程中,每個階段都為其新增處理成功的回撥和處理失敗的回撥。當一個非同步結果到來時,Deferred物件就被“啟用”,那麼處理成功的回撥和處理失敗的回撥就可以以合適的方式按照它們新增進來的順序依次得到呼叫。
非同步版URL獲取器採用Deferred程式碼片段如下:
from twisted.internet import reactor
import getPage
def processPage(page):
print page
def logError(error):
print error
def finishProcessing(value):
print "Shutting down..."
reactor.stop()
url = "http://google.com"
deferred = getPage(url) # getPage returns a Deferred
deferred.addCallbacks(success, failure)
deferred.addBoth(stop)
reactor.run()
Deferred物件建立時包含兩個添加回調的階段。第一階段,addCallbacks將 processPage和logError新增到它們各自歸屬的回撥鏈中。然後addBoth再將finishProcessing同時新增到這兩個回撥鏈上。Deferred物件只能被啟用一次,如果試圖重複啟用將引發一個異常.用圖解的方式來看,回撥鏈應該如圖所示:
Transports
Transports代表網路中兩個通訊結點之間的連線。Transports負責描述連線的細節,比如連線是面向流式的還是面向資料報的,流控以及可靠性。TCP、UDP和Unix套接字可作為transports的例子。Transports實現了ITransports介面
write 以非阻塞的方式按順序依次將資料寫到物理連線上
writeSequence 將一個字串列表寫到物理連線上
loseConnection 將所有掛起的資料寫入,然後關閉連線
getPeer 取得連線中對端的地址資訊
getHost 取得連線中本端的地址資訊
Protocols
Protocols描述瞭如何以非同步的方式處理網路中的事件。HTTP、DNS以及IMAP是應用層協議中的例子。Protocols實現了IProtocol介面,它包含如下的方法:
makeConnection 在transport物件和伺服器之間建立一條連線
connectionMade 連線建立起來後呼叫
dataReceived 接收資料時呼叫
connectionLost 關閉連線時呼叫
詳細的 reactor、transport、protocols例子
執行伺服器端指令碼將啟動一個TCP伺服器,監聽埠8000上的連線。伺服器採用的是Echo協議,資料經TCP transport物件寫出。執行客戶端指令碼將對伺服器發起一個TCP連線,回顯伺服器端的迴應然後終止連線並停止reactor事件迴圈。這裡的Factory用來對連線的雙方生成protocol物件例項。兩端的通訊是非同步的,connectTCP負責註冊回撥函式到reactor事件迴圈中,當socket上有資料可讀時通知回撥處理。
-
伺服器部分
from twisted.internet import protocol, reactor class Echo(protocol.Protocol): def dataReceived(self, data): # As soon as any data is received, write it back self.transport.write(data) class EchoFactory(protocol.Factory): def buildProtocol(self, addr): return Echo() reactor.listenTCP(8000, EchoFactory()) reactor.run()
-
客戶端部分
from twisted.internet import reactor, protocol class EchoClient(protocol.Protocol): def connectionMade(self): self.transport.write("hello, world!") def dataReceived(self, data): print "Server said:", data self.transport.loseConnection() def connectionLost(self, reason): print "connection lost" class EchoFactory(protocol.ClientFactory): def buildProtocol(self, addr): return EchoClient() def clientConnectionFailed(self, connector, reason): print "Connection failed - goodbye!" reactor.stop() def clientConnectionLost(self, connector, reason): print "Connection lost - goodbye!" reactor.stop() reactor.connectTCP("localhost", 8000, EchoFactory()) reactor.run()
Applications
Twisted是用來建立具有可擴充套件性、跨平臺的網路伺服器和客戶端的引擎。Applications基礎元件包含4個主要部分:服務(Service)、應用(Application)、配置管理(通過TAC檔案和外掛)以及twistd命令列程式。為了說明這個基礎元件,我們將上一節的Echo伺服器轉變成一個應用。
Service
IService介面下實現的可以啟動和停止的元件。Twisted自帶有TCP、FTP、HTTP、SSH、DNS等服務以及其他協議的實現。其中許多Service都可以註冊到單獨的應用中。IService介面的核心是:
startService 啟動服務。可能包含載入配置資料,設定資料庫連線或者監聽某個埠
stopService 關閉服務。可能包含將狀態儲存到磁碟,關閉資料庫連線或者停止監聽埠
Application
Application是處於最頂層的Service,代表了整個Twisted應用程式。Service需要將其自身同Application註冊,然後就可以用下面我們將介紹的部署工具twistd搜尋並執行應用程式。我們將建立一個可以同Echo Service註冊的Echo應用。
TAC檔案
當在一個普通的Python檔案中管理Twisted應用程式時,需要由開發者負責編寫啟動和停止reactor事件迴圈以及配置應用程式的程式碼。在Twisted的基礎元件中,協議的實現都是在一個模組中完成的,需要使用到這些協議的Service可以註冊到一個Twisted應用程式配置檔案中(TAC檔案)去,這樣reactor事件迴圈和程式配置就可以由外部元件來進行管理。
要將我們的Echo伺服器轉變成一個Echo應用,我們可以按照以下幾個簡單的步驟來完成:
-
將Echo伺服器的Protocol部分移到它們自己所歸屬的模組中去。
-
在TAC檔案中:
- 建立一個Echo應用。
- 建立一個TCPServer的Service例項,它將使用我們的EchoFactory,然後同前面建立的應用完成註冊。
管理reactor事件迴圈的程式碼將由twistd來負責,我們下面會對此進行討論。這樣,應用程式的程式碼就變成這樣了:
echo.py檔案:
from twisted.internet import protocol, reactor
class Echo(protocol.Protocol):
def dataReceived(self, data):
self.transport.write(data)
class EchoFactory(protocol.Factory):
def buildProtocol(self, addr):
return Echo()
twistd
twistd(讀作“twist-dee”)是一個跨平臺的用來部署Twisted應用程式的工具。它執行TAC檔案並負責處理啟動和停止應用程式。作為Twisted在網路程式設計中具有“內建電池”能力的一部分,twistd自帶有一些非常有用的配置標誌,包括將應用程式轉變為守護程序、定義日誌檔案的路徑、設定特權級別、在chroot下執行、使用非預設的reactor,甚至是在profiler下執行應用程式。
我們可以像這樣執行這個Echo服務應用:
twistd –y echo_server.tac
在這個簡單的例子裡,twistd將這個應用程式作為守護程序來啟動,日誌記錄在twistd.log檔案中。啟動和停止應用後,日誌檔案內容如下:
2011-11-19 22:23:07-0500 [-] Log opened.
2011-11-19 22:23:07-0500 [-] twistd 11.0.0 (/usr/bin/python 2.7.1) starting up.
2011-11-19 22:23:07-0500 [-] reactor class: twisted.internet.selectreactor.SelectReactor.
2011-11-19 22:23:07-0500 [-] echo.EchoFactory starting on 8000
2011-11-19 22:23:07-0500 [-] Starting factory <echo.EchoFactory instance at 0x12d8670>
2011-11-19 22:23:20-0500 [-] Received SIGTERM, shutting down.
2011-11-19 22:23:20-0500 [-] (TCP Port 8000 Closed)
2011-11-19 22:23:20-0500 [-] Stopping factory <echo.EchoFactory instance at 0x12d8670>
2011-11-19 22:23:20-0500 [-] Main loop terminated.
2011-11-19 22:23:20-0500 [-] Server Shut Down.
通過使用Twisted框架中的基礎元件來執行服務,這麼做使得開發人員能夠不用再編寫類似守護程序和記錄日誌這樣的冗餘程式碼了。這同樣也為部署應用程式建立了一個標準的命令列介面。
#Plugins
對於執行Twisted應用程式的方法,除了基於TAC檔案外還有一種可選的方法,這就是外掛系統。TAC系統可以很方便的將Twisted預定義的服務同應用程式配置檔案註冊,而外掛系統能夠方便的將使用者自定義的服務註冊為twistd工具的子命令,然後擴充套件應用程式的命令列介面。
在使用外掛系統時:
-
由於只有plugin API需要保持穩定,這使得第三方開發者能很容易地擴充套件軟體。
-
外掛發現能力已經整合到系統中了。外掛可以在程式首次執行時載入並儲存,每次程式啟動時會重新觸發外掛發現過程,或者也可以在程式執行期間反覆輪詢新外掛,這使得在程式已經啟動後我們還可以判斷是否有新的外掛安裝上了。
當使用Twisted外掛系統來擴充套件軟體時,我們要做的就是建立IPlugin介面下實現的物件並將它們放到一個特定的位置中,這裡外掛系統知道該如何去找到它們。
我們已經將Echo服務轉換為一個Twisted應用程式了,而將其轉換為一個Twisted外掛也是非常簡單直接的。在我們之前的Echo模組中,除了包含有Echo協議和EchoFactory的定義之外,現在我們還要新增一個名為twistd的目錄,其中還包含著一個名為plugins的子目錄,這裡正是我們需要定義echo外掛的地方。通過這個外掛,我們可以啟動一個echo服務,並將需要使用的埠號作為引數指定給twistd工具。
from zope.interface import implements
from twisted.python import usage
from twisted.plugin import IPlugin
from twisted.application.service import IServiceMaker
from twisted.application import internet
from echo import EchoFactory
class Options(usage.Options):
optParameters = [["port", "p", 8000, "The port number to listen on."]]
class EchoServiceMaker(object):
implements(IServiceMaker, IPlugin)
tapname = "echo"
description = "A TCP-based echo server."
options = Options
def makeService(self, options):
"""
Construct a TCPServer from a factory defined in myproject.
"""
return internet.TCPServer(int(options["port"]), EchoFactory())
serviceMaker = EchoServiceMaker()
現在,我們的Echo伺服器將作為一個服務選項出現在twistd –help的輸出中。執行twistd echo –port=1235將在埠1235上啟動一個Echo伺服器。
Twisted還帶有一個可拔插的針對伺服器端認證的模組twisted.cred,外掛系統常見的用途就是為應用程式新增一個認證模式。我們可以使用twisted.cred中現成的AuthOptionMixin類來新增針對各種認證的命令列支援,或者是新增新的認證型別。比如,我們可以使用外掛系統來新增基於本地Unix密碼資料庫或者是基於LDAP伺服器的認證方式。
twistd工具中附帶有許多Twisted所支援的協議外掛,只用一條單獨的命令就可以完成啟動伺服器的工作了。這裡有一些通過twistd啟動伺服器的例子:
twistd web –port 8080 –path .
這條命令將在8080埠啟動一個HTTP伺服器,在當前目錄中負責處理靜態和動態頁面請求。
twistd dns –p 5553 –hosts-file=hosts
這條命令在埠5553上啟動一個DNS伺服器,解析指定的檔案hosts中的域名,這個檔案的內容格式同/etc/hosts一樣。
sudo twistd conch –p tcp:2222
這條命令在埠2222上啟動一個SSH伺服器。ssh的金鑰必須獨立設定。
twistd mail –E –H localhost –d localhost=emails
這條命令啟動一個ESMTP POP3伺服器,為本地主機接收郵件並儲存到指定的emails目錄下。
我們可以方便的通過twistd來搭建一個用於測試客戶端功能的伺服器,但它同樣是可裝載的、產品級的伺服器實現。
在部署應用程式的方式上,Twisted通過TAC檔案、外掛以及命令列工具twistd的部署方式已經獲得了成功。但是有趣的是,對於大多數大型Twisted應用程式來說,部署它們仍然需要重寫一些這類管理和監控元件;Twisted的架構並沒有對系統管理員的需求呈現出太多的友好性。這也反映了一個事實,那就是對於系統管理員來說Twisted歷來就沒有太多架構可言,而這些系統管理員才是部署和維護應用程式的專家。在這方面,Twisted在未來架構設計的決策上需要更積極的徵求這類專家級使用者的反饋意見。