Python Twisted 介紹

擒賊先擒王發表於2019-02-18

 

Python Twisted介紹:http://blog.csdn.net/hanhuili/article/details/9389433
原文連結:http://www.aosabook.org/en/twisted.html

 

作者:Jessica McKellar

Twisted 是用 Python 實現的 基於事件驅動 的 網路引擎框架,即一個 網路庫。Twisted誕生於2000年初,在當時的網路遊戲開發者看來,無論他們使用哪種語言,手中都鮮有可兼顧擴充套件性及跨平臺的網路庫。Twisted 的作者試圖在當時現有的環境下開發遊戲,這一步走的非常艱難,他們迫切地需要一個可擴充套件性高、基於事件驅動、跨平臺的網路開發框架,為此他們決定自己實現一個,並從那些之前的遊戲和網路應用程式的開發者中學習,汲取他們的經驗教訓。

Twisted 支援許多常見的傳輸及應用層協議,包括 TCP、UDP、SSL/TLS、HTTP、IMAP、SSH、IRC以及FTP。就像 Python一樣,Twisted 也具有 “內建電池”(batteries-included)的特點。Twisted 對於其支援的所有協議都帶有客戶端和伺服器實現,同時附帶有基於命令列的工具,使得配置和部署產品級的 Twisted 應用變得非常方便。

 

 

21.1 為什麼需要Twisted

 

2000年時,Twisted的作者Glyph正在開發一個名為Twisted Reality的基於文字方式的多人線上遊戲。這個遊戲採用Java開發,裡面盡是一堆執行緒——每個連線就有3個執行緒處理。處理輸入的執行緒會在讀操作上阻塞,處理輸出的執行緒將在一些寫操作上阻塞,還有一個“邏輯”執行緒將在等待定時器超時或者事件入佇列時休眠。隨著玩家們在虛擬世界中移動並互動時,執行緒出現死鎖,快取被汙染,程式中的加鎖邏輯幾乎從來就沒對過——採用多執行緒使得整個軟體變得複雜、漏洞百出而且極難擴充套件。

為了尋求其他的解決方案,作者發現了Python,特別是Python中用於對流式物件比如socket和pipe進行多路I/O複用的select模組(UNIX規範第3版(SUSv3)描述了select)。那時,Java並沒有提供作業系統的select介面或者任何其他的非同步I/O API(針對非阻塞式I/O的包java.nio已經在J2SE 1.4中加入了,2002年釋出)。通過用Python中的select模組快速搭建起遊戲的原型,這迅速降低了程式的複雜度,並且比多執行緒版本要更加可靠。

Glyph迅速轉向了Python、select以及基於事件驅動的程式設計。他使用Python的select模組為遊戲編寫了客戶端和伺服器。但他想要的還不止於此。從根本上說,他希望能將網路行為轉變為對遊戲中的物件的方法呼叫。如果你能在遊戲中收取郵件會怎樣,就像Nethack mailer這種守護程式一樣?如果遊戲中的每位玩家都擁有一個主頁呢?Glyph發現他需要優秀的IMAP以及HTTP客戶端和伺服器的Python實現,而這些都要採用select。

他首先轉向了Medusa,這是一個在90年代中期開發的平臺,在這裡可以採用Python中的asyncore模組來編寫網路服務。asyncore是一個非同步化處理socket的模組,在作業系統的select API之上構建了一個排程器和回撥介面。

這對於Glyph來說是個激動人心的發現,但Medusa有兩個缺點:

  1. 這個專案到2001年就不再維護了,那正是glyph開發Twisted Reality的時候。
  2. asyncore只是對socket的一個薄封裝層,應用程式的編寫者仍然需要直接操作socket。這意味著程式可移植性的擔子仍然落在程式設計師自己身上。此外,那時asyncore對Windows的支援還有問題,Glyph希望能在Windows上執行一個帶有圖形使用者介面的客戶端。

Glyph需要自己實現一個網路引擎平臺,而且他意識到Twisted Reality已經開啟了問題的大門,這和他的遊戲一樣有趣。

隨著時間的推移,Twisted Reality這個遊戲就演化成了Twisted網路引擎平臺。它可以做到當時Python中已有的網路平臺所無法做到的事情:

  • 使用基於事件驅動的程式設計模型,而不是多執行緒模型。
  • 跨平臺:為主流作業系統平臺暴露出的事件通知系統提供統一的介面。
  • “內建電池”的能力:提供流行的應用層協議實現,因此Twisted馬上就可為開發人員所用。
  • 符合RFC規範,已經通過健壯的測試套件證明了其一致性。
  • 能很容易的配合多個網路協議一起使用。
  • 可擴充套件。

 

 

21.2 Twisted架構概覽

 

Twisted是一個事件驅動型的網路引擎。由於事件驅動程式設計模型在Twisted的設計哲學中佔有重要的地位,因此這裡有必要花點時間來回顧一下究竟事件驅動意味著什麼。

事件驅動程式設計是一種程式設計正規化,這裡程式的執行流由外部事件來決定。它的特點是包含一個事件迴圈,當外部事件發生時使用回撥機制來觸發相應的處理。另外兩種常見的程式設計正規化是(單執行緒)同步以及多執行緒程式設計。

讓我們用例子來比較和對比一下單執行緒、多執行緒以及事件驅動程式設計模型。圖21.1展示了隨著時間的推移,這三種模式下程式所做的工作。這個程式有3個任務需要完成,每個任務都在等待I/O操作時阻塞自身。阻塞在I/O操作上所花費的時間已經用灰色框標示出來了。

圖21.1 執行緒模型

在單執行緒同步模型中,任務按照順序執行。如果某個任務因為I/O而阻塞,其他所有的任務都必須等待,直到它完成之後它們才能依次執行。這種明確的執行順序和序列化處理的行為是很容易推斷得出的。如果任務之間並沒有互相依賴的關係,但仍然需要互相等待的話這就使得程式不必要的降低了執行速度。

在多執行緒版本中,這3個任務分別在獨立的執行緒中執行。這些執行緒由作業系統來管理,在多處理器系統上可以並行處理,或者在單處理器系統上交錯執行。這使得當某個執行緒阻塞在某個資源的同時其他執行緒得以繼續執行。與完成類似功能的同步程式相比,這種方式更有效率,但程式設計師必須寫程式碼來保護共享資源,防止其被多個執行緒同時訪問。多執行緒程式更加難以推斷,因為這類程式不得不通過執行緒同步機制如鎖、可重入函式、執行緒區域性儲存或者其他機制來處理執行緒安全問題,如果實現不當就會導致出現微妙且令人痛不欲生的bug。

在事件驅動版本的程式中,3個任務交錯執行,但仍然在一個單獨的執行緒控制中。當處理I/O或者其他昂貴的操作時,註冊一個回撥到事件迴圈中,然後當I/O操作完成時繼續執行。回撥描述了該如何處理某個事件。事件迴圈輪詢所有的事件,當事件到來時將它們分配給等待處理事件的回撥函式。這種方式讓程式儘可能的得以執行而不需要用到額外的執行緒。事件驅動型程式比多執行緒程式更容易推斷出行為,因為程式設計師不需要關心執行緒安全問題。

當我們面對如下的環境時,事件驅動模型通常是一個好的選擇:

  1. 程式中有許多工,而且…
  2. 任務之間高度獨立(因此它們不需要互相通訊,或者等待彼此)而且…
  3. 在等待事件到來時,某些任務會阻塞。

當應用程式需要在任務間共享可變的資料時,這也是一個不錯的選擇,因為這裡不需要採用同步處理。

網路應用程式通常都有上述這些特點,這使得它們能夠很好的契合事件驅動程式設計模型。

 

重用已有的應用

在Twisted建立之前就已經有了許多針對多種流行的網路協議的客戶端和伺服器實現了。為什麼Glyph不直接用Apache、IRCd、BIND、OpenSSH或者任何其他已有的應用,而要為Twisted從頭開始重新實現各個協議的客戶端和伺服器呢?

問題在於所有這些已有的實現都存在有從頭寫起的網路層程式碼,通常都是C程式碼。而應用層程式碼直接同網路層耦合在一起,這使得它們非常難以以庫的形式來複用。當要一起使用這些元件時,如果希望在多個協議中暴露相同的資料,則它們必須以黑盒的形式來看待,這使得開發者根本沒機會重用程式碼。此外,伺服器和客戶端的實現通常是分離的,彼此之間不共享程式碼。要擴充套件這些應用,維護跨平臺的客戶端-伺服器相容性的難度本不至於這麼大。

Twisted中的客戶端和伺服器是用Python開發的,採用了一致性的介面。這使得開發新的客戶端和伺服器變得很容易實現,可以在客戶端和伺服器之間共享程式碼,在協議之間共享應用邏輯,以及對某個實現的程式碼做測試。

 

Reactor模式

Twisted實現了設計模式中的反應堆(reactor)模式,這種模式在單執行緒環境中排程多個事件源產生的事件到它們各自的事件處理例程中去。

Twisted的核心就是reactor事件迴圈。Reactor可以感知網路、檔案系統以及定時器事件。它等待然後處理這些事件,從特定於平臺的行為中抽象出來,並提供統一的介面,使得在網路協議棧的任何位置對事件做出響應都變得簡單。

基本上reactor完成的任務就是:

while True:
    timeout = time_until_next_timed_event()
    events = wait_for_events(timeout)
    events += timed_events_until(now())
    for event in events:
        event.process()

Twisted目前在所有平臺上的預設reactor都是基於poll API的(UNIX規範第3版(SUSv3)中描述)。此外,Twisted還支援一些特定於平臺的高容量多路複用API。這些reactor包括基於FreeBSD中kqueue機制的KQueue reactor,支援epoll介面的系統(目前是Linux 2.6)中的epoll reactor,以及基於Windows下的輸入輸出完成埠的IOCP reactor。

在實現輪詢的相關細節中,Twisted需要考慮的包括:

  • 網路和檔案系統的限制
  • 緩衝行為
  • 如何檢測連線丟失
  • 出現錯誤時的返回值

Twisted的reactor實現同時也考慮了正確使用底層的非阻塞式API,並正確處理各種邊界情況。由於Python中沒有暴露出IOCP API,因此Twisted需要維護自己的實現。

 

管理回撥鏈

回撥是事件驅動程式設計模型中的基礎,也是reactor通知應用程式事件已經處理完成的方式。隨著程式規模不斷擴大,基於事件驅動的程式需要同時處理事件處理成功和出錯的情況,這使得程式變得越來越複雜。若沒有註冊一個合適的回撥,程式就會阻塞,因為這個事件處理的過程絕不會發生。出現錯誤時需要通過應用程式的不同層次從網路棧向上傳遞迴調鏈。

下面是兩段Python偽碼,分別是同步和非同步模式下獲取URL的玩具程式碼。讓我們相互比較一下這兩個版本,看看基於事件驅動的程式有什麼缺陷:

以同步的方式獲取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()

以非同步的方式獲取URL:

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()

在非同步版的URL獲取器中,reactor.run()啟動reactor事件迴圈。在同步和非同步版程式中,我們假定getPage函式處理獲取頁面的工作。如果獲取成功就呼叫processPage,如果嘗試獲取頁面時出現了Exception(異常),logError就得到呼叫。無論哪種情況,最後都要呼叫finishProcessing。

非同步版中的logError回撥正對應於同步版中的try/except塊。對processPage的回撥對應於else塊,無條件回撥的finishProcessing就對應於finally塊。

在同步版中,程式碼結構直接顯示出有一個try/except塊,logError和processPage這兩者間只會取其一呼叫一次,而finishProcessing總是會被呼叫一次。在非同步版中需要由程式設計師自己負責正確呼叫成功和失敗情況下的回撥鏈。如果由於程式設計錯誤,在processPage或者logError的回撥鏈之後沒有呼叫finishProcessing,reactor事件迴圈將永遠不會停止,程式就會卡住。

這個玩具式的例子告訴我們在開發Twisted的頭幾年裡這種複雜性令程式設計師感到非常沮喪。而Twisted應對這種複雜性的方式是新增一個稱為Deferred(延遲)的物件。

Deferreds

Deferred物件以抽象化的方式表達了一種思想,即結果還尚不存在。它同樣能夠幫助管理產生這個結果所需要的回撥鏈。當從函式中返回時,Deferred物件承諾在某個時刻函式將產生一個結果。返回的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物件上,而不是分散在程式碼各處再以引數形式傳遞給getPage。

Deferred物件建立時包含兩個新增回撥的階段。第一階段,addCallbacks將 processPage和logError新增到它們各自歸屬的回撥鏈中。然後addBoth再將finishProcessing同時新增到這兩個回撥鏈上。用圖解的方式來看,回撥鏈應該如圖21.2所示:

圖21.2 回撥鏈

Deferred物件只能被啟用一次,如果試圖重複啟用將引發一個異常。這使得Deferred物件的語義相當接近於同步版中的try/except塊。從而讓非同步事件的處理能更容易推斷,避免由於針對單個事件的回撥呼叫多了一個或少了一個而產生微妙的bug。

理解Deferred物件對於理解Twisted程式的執行流是非常重要的。然而當使用Twisted為我們提供的針對網路協議的高層抽象時,通常情況下我們完全不需要直接使用Deferred物件。

Deferred物件所包含的抽象概念是非常強大的,這種思想已經被許多其他的事件驅動平臺所借用,包括jQuery、Dojo和Mochikit。

Transports

Transports代表網路中兩個通訊結點之間的連線。Transports負責描述連線的細節,比如連線是面向流式的還是面向資料包的,流控以及可靠性。TCP、UDP和Unix套接字可作為transports的例子。它們被設計為“滿足最小功能單元,同時具有最大程度的可複用性”,而且從協議實現中分離出來,這讓許多協議可以採用相同型別的傳輸。Transports實現了ITransports介面,它包含如下的方法:

write                   以非阻塞的方式按順序依次將資料寫到物理連線上
writeSequence           將一個字串列表寫到物理連線上
loseConnection          將所有掛起的資料寫入,然後關閉連線
getPeer                 取得連線中對端的地址資訊
getHost                 取得連線中本端的地址資訊

將transports從協議中分離出來也使得對這兩個層次的測試變得更加簡單。可以通過簡單地寫入一個字串來模擬傳輸,用這種方式來檢查。

Protocols

Protocols描述瞭如何以非同步的方式處理網路中的事件。HTTP、DNS以及IMAP是應用層協議中的例子。Protocols實現了IProtocol介面,它包含如下的方法:

makeConnection               在transport物件和伺服器之間建立一條連線
connectionMade               連線建立起來後呼叫
dataReceived                 接收資料時呼叫
connectionLost               關閉連線時呼叫

我們最好以一個例子來說明reactor、protocols以及transports這三者之間的關係。以下是完整的echo伺服器和客戶端的實現,首先來看看伺服器部分:

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()

執行伺服器端指令碼將啟動一個TCP伺服器,監聽埠8000上的連線。伺服器採用的是Echo協議,資料經TCP transport物件寫出。執行客戶端指令碼將對伺服器發起一個TCP連線,回顯伺服器端的迴應然後終止連線並停止reactor事件迴圈。這裡的Factory用來對連線的雙方生成protocol物件例項。兩端的通訊是非同步的,connectTCP負責註冊回撥函式到reactor事件迴圈中,當socket上有資料可讀時通知回撥處理。

Applications

Twisted是用來建立具有可擴充套件性、跨平臺的網路伺服器和客戶端的引擎。在生產環境中,以標準化的方式簡化部署這些應用的過程對於Twisted這種被廣泛採用的平臺來說是非常重要的一環。為此,Twisted開發了一套應用程式基礎元件,採用可重用、可配置的方式來部署Twisted應用。這種方式使程式設計師避免堆砌千篇一律的程式碼來將應用程式同已有的工具整合在一起,這包括精靈化程式(daemonization)、日誌處理、使用自定義的reactor迴圈、對程式碼做效能剖析等。

應用程式基礎元件包含4個主要部分:服務(Service)、應用(Application)、配置管理(通過TAC檔案和外掛)以及twistd命令列程式。為了說明這個基礎元件,我們將上一節的Echo伺服器轉變成一個應用。

Service

Service就是IService介面下實現的可以啟動和停止的元件。Twisted自帶有TCP、FTP、HTTP、SSH、DNS等服務以及其他協議的實現。其中許多Service都可以註冊到單獨的應用中。IService介面的核心是:

startService    啟動服務。可能包含載入配置資料,設定資料庫連線或者監聽某個埠
stopService     關閉服務。可能包含將狀態儲存到磁碟,關閉資料庫連線或者停止監聽埠

我們的Echo服務使用TCP協議,因此我們可以使用Twisted中IService介面下預設的TCPServer實現。

Application

Application是處於最頂層的Service,代表了整個Twisted應用程式。Service需要將其自身同Application註冊,然後就可以用下面我們將介紹的部署工具twistd搜尋並執行應用程式。我們將建立一個可以同Echo Service註冊的Echo應用。

TAC檔案

當在一個普通的Python檔案中管理Twisted應用程式時,需要由開發者負責編寫啟動和停止reactor事件迴圈以及配置應用程式的程式碼。在Twisted的基礎元件中,協議的實現都是在一個模組中完成的,需要使用到這些協議的Service可以註冊到一個Twisted應用程式配置檔案中(TAC檔案)去,這樣reactor事件迴圈和程式配置就可以由外部元件來進行管理。

要將我們的Echo伺服器轉變成一個Echo應用,我們可以按照以下幾個簡單的步驟來完成:

  1. 將Echo伺服器的Protocol部分移到它們自己所歸屬的模組中去。

  2. 在TAC檔案中:

    1. 建立一個Echo應用。
    2. 建立一個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工具的子命令,然後擴充套件應用程式的命令列介面。

在使用外掛系統時:

  1. 由於只有plugin API需要保持穩定,這使得第三方開發者能很容易地擴充套件軟體。

  2. 外掛發現能力已經整合到系統中了。外掛可以在程式首次執行時載入並儲存,每次程式啟動時會重新觸發外掛發現過程,或者也可以在程式執行期間反覆輪詢新外掛,這使得在程式已經啟動後我們還可以判斷是否有新的外掛安裝上了。

當使用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在未來架構設計的決策上需要更積極的徵求這類專家級使用者的反饋意見。

 

 

21.3 反思與教訓

Twisted最近剛剛渡過了其10週年的誕辰。自專案成立以來,由於受2000年早期的網路遊戲啟發,目前的Twisted已經在很大程度上實現了作為一個可擴充套件、跨平臺、事件驅動的網路引擎的目標。Twisted廣泛使用於生產環境中,從Google、盧卡斯電影到Justin.TV以及Launchpad軟體協作平臺都有在使用。Twisted中的伺服器端實現是多個開源軟體的核心,包括BuildBot、BitTorrent以及TahoeLAFS。

Twisted從最初開發到現在,其架構已經經歷了幾次大的變動。Deferred物件作為一個關鍵部分被增加了進來。如前文所述,這是用來管理延後的結果以及相應的回撥鏈。

還有一個重要的部分被移除掉了,在目前的實現中已經幾乎看不到任何影子了,這就是Twisted應用持久化(Twisted Application Persistence)。

 

 

Twisted應用持久化

Twisted應用持久化(TAP)是指將應用程式的配置和狀態儲存在一個pickle中。要執行採用了這種方案的應用需要兩個步驟:

  1. 使用mktap工具建立一個代表該應用的pickle(該工具現已廢棄不用)。

  2. 使用twistd命令列工具進行unpickle操作,然後執行該應用。

這個過程是受Smalltalk images的啟發,因為我們討厭那種臨時性的且難以使用的專用配置語言,不希望它們在專案中不斷擴散。我們更希望在Python中表示配置的細節。

很快,TAP檔案就引入了不必要的複雜性。修改Twisted中的類並不會使pickle中這些類的例項得到改變。在pickle物件上使用新版本的類方法或屬性時可能會使整個應用崩潰。因此“升級版”的概念得以引入,即將pickle物件升級到新的API版本。但這就會出現升級版本的矩陣化現象,出現各種不同版本的pickle物件,因此單元測試時需要維護涵蓋所有可能的升級路徑。想全面地跟蹤所有的介面變化依然很難,而且容易出錯。

TAP以及相關的元件全部被廢除了,最終從Twisted中完全剔除掉。取而代之的是TAC檔案和外掛系統。TAP這個縮寫被重新定義為Twisted Application Plugin(Twisted應用外掛),如今已經很難在Twisted中找到pickle系統的蹤跡了。

我們從TAP的慘敗中得到的教訓是:如果可維護性要達到合理化的程度,則永續性資料就需要有一個明確的模式。更一般的是,我們學到了如何為專案增加複雜度:為了解決某個問題而需要引入一個新系統時,我們要正確理解這個方案的複雜性,並經過測試。新系統所帶來的價值應該明顯大於其複雜性。確保了這一點之後我們才能將方案付諸於專案中。

 

 

web2:重構的教訓

雖然這基本上不屬於架構設計上的決策,但從專案管理的角度來看,重寫Twisted的Web實現對於Twisted的外在形象以及維護者對程式碼庫中其他部分做架構改善的能力卻有著長遠的影響,因此這裡值得我們簡單討論一下。

在2000年中期,Twisted的開發者決定完全重寫twisted.web API,在Twisted程式碼庫中將其作為一個單獨的專案實現,這就是web2。web2將包含許多針對原有twisted.web的改善和提升,包括完全支援HTTP1.1,以及對流式資料的API支援。

web2最初只是試驗性的專案,但最終被大型專案所採用,甚至意外的得以在Debian系統上打包釋出。twisted.web和web2的開發一直並行持續了多年,新使用者常常被這兩個並行的專案搞混,關於究竟應該使用哪種實現缺乏明確的提示,這使得新使用者很沮喪。轉換到web2的情況從未出現,終於在2011年開發者將其從程式碼庫中移除,官方主頁上再也看不到它了。web2中做出的一些改進也被慢慢地移植回twisted.web中。

Twisted獲得了難以導航且結構混亂,容易使新開發者感到困惑的“惡名”,這個印象部分歸功於web2。以至於數年之後,Twisted社群仍然在同這種不和諧的名聲做鬥爭。

我們從web2中汲取的教訓是:從頭開始重構一個專案通常都是糟糕的主意。但如果必須這麼做,請確保開發者社群能夠懂得這麼做的長遠意義,而且在使用者社群中要有明確的選擇該使用哪種實現。

如果Twisted能夠倒退回web2的時代,開發者們應該會對twisted.web做一系列向後相容型的修改而不是去重構。

 

 

緊跟網際網路的浪潮

我們使用網際網路的方式還在持續演進中。把多種協議的實現作為軟體核心的一部分,這個技術決策使得Twisted揹負了維護這些協議的沉重負擔。隨著標準的改變以及對新協議的採納,原有的實現必須跟著演進,同時需要嚴格的保證向後相容性。

Twisted基本上是一個志願者驅動型的專案,專案發展的限制因素不是技術社群的熱情,而在於志願者的時間。比如說,1999年的RFC 2616中定義了HTTP 1.1規範,而在Twisted的HTTP協議實現中增加對HTTP 1.1的支援卻在2005年才開始,等到完成時已經是2009年了。1998年RFC 2460中定義了對IPv6的支援,而Twisted對其的支援還在進行中,但是直到2011年都未能合併進去。

隨著所支援的作業系統的介面改變,實現也要跟著演進。比如,epoll事件通知機制是在2002年加入到Linux 2.5.44版中的,Twisted隨之也發展出基於epoll的reactor事件迴圈來利用這個新的系統介面。2007年時,蘋果公司釋出的OS 10.5 Leopard系統中,系統呼叫poll的實現居然不支援外設,對於蘋果公司來說這個問題足以讓他們在系統自帶的Python中遮蔽掉select.poll介面。Twisted不得不自行解決這個問題,並從那時起就對使用者提供文件說明。

有時候,Twisted的開發並沒有緊跟網路世界的變化,有一些改進被移到核心層之外的程式庫中去了。比如Wokkel project,這是對Twisted的Jabber/XMPP支援的改進合集,已經作為“待合入”的獨立專案有幾年之久了,但還沒有看到合入的希望。在2009年也曾經嘗試過增加WebSocket到Twisted中,因為瀏覽器已經開始採納對新協議的支援了。但開發計劃最終卻轉到其他外部專案中去了,因為開發者們決定暫不包含新的協議,直到IETF把它從草案轉變成標準以後再說。

所有這一切都在說明,庫和附加元件的擴散有力的證明了Twisted的靈活性和可擴充套件性。通過採用嚴格的測試驅動開發策略以及文件化和編碼規範標準,這樣做能夠幫助專案避免出現需要“回爐”的情況。在維護大量所支援的協議和平臺的同時保持向後相容性。Twisted是一個成熟、穩定的專案,並繼續保持有非常活躍的開發狀態。

Twisted期待著在下一個十年裡成為你遨遊網際網路的引擎。

 

 

 

相關文章