[Twisted] Part 11: Your Poetry is Served

本文由Dave的Part 11: Your Poetry is Served翻譯而成,你可以由Part 1開始閱讀這個系列的文章,也可以在這裡找到整個系列的目錄。

A Twisted Poetry Server

我們到目前為止已經學習了很多關於以Twisted來撰寫用戶端,讓我們換個方向也來用Twisted重新實現我們的伺服器。同時感謝Twisted的抽象概念,你會發現我們已經學會了幾乎我們需要知道的所有事情。看一下我們在twisted-server-1/fastpoetry.py中的詩歌伺服器。它被稱為fastpoetry因為這個伺服器會盡可能的快速發送詩歌,沒有任何延遲。注意它的程式碼比用戶端還少。

讓我們一次一個部分的來看看伺服器的各個部分,首先是poetryProtocol:
class PoetryProtocol(Protocol):

    def connectionMade(self):
        self.transport.write(self.factory.poem)
        self.transport.loseConnection()

與用戶端一樣,伺服器使用獨立的Protocol實例來管理每個不同的連接(在這邊的案例中,是由用戶端連接到伺服器)。Protocol類別實現了我們詩歌協議的伺服器端的部分。由於我們的wire protocol (線路協議)(譯註:wire protocol簡單來說就是一種點對點傳輸數據的機制,如果多個應用程式需要互動,就會需要wire protocol)僅有單向,所以伺服器的Protocol實例只需要關心發送數據。如果你記得,我們的wire protocol要求伺服器在連接建立後立刻開始發送詩歌,所以我們實現了connectionMade方法,它是一個在Protocol實例連接到Transport之後被調用的callback。

我們的方法告訴Transport去做兩件事情:發送整首詩歌(self.transport.write)與關閉這個連接(self.transport.loseConnection)。當然,這兩個作業都是非同步的。所以呼叫write()實際上表示「最終將所有資料送到用戶端」,而呼叫loseConnection()實際上表示「當我要求你發送的資料都發送完,就關閉這個連接」。

如你所見,Protocol是從Factory擷取詩歌的內容,讓我們繼續往下看:
class PoetryFactory(ServerFactory):

    protocol = PoetryProtocol

    def __init__(self, poem):
        self.poem = poem

除了根據需求建立PoetryProtocol實例,我們的工廠真正唯一的工作是儲存每首PoetryProtocol發送給用戶端的詩歌。

注意我們是繼承了ServerFactory而不是ClientFactory。由於我們的伺服器是被動的監聽連接而非主動建立連接,我們不需要ClientFactory提供額外的方法。我們如何確定這一點?因為我們使用reactor的listenTCP方法,而且該方法的文件說明了factory引數應該是ServerFactory的一個實例。

這個是我們呼叫listenTCP的main函式:
def main():
    options, poetry_file = parse_args()

    poem = open(poetry_file).read()

    factory = PoetryFactory(poem)

    from twisted.internet import reactor

    port = reactor.listenTCP(options.port or 0, factory,
                             interface=options.iface)

    print 'Serving %s on %s.' % (poetry_file, port.getHost())

    reactor.run()

它基本上做了三件事情:

  1. 讀取了我們要提供的詩歌。
  2. 建立了PoetryFactory並傳入這首詩歌。
  3. 使用listenTCP來告訴Twisted去監聽埠口上的連接,並且使用我們的工廠為每個新連接建立一個協定的實例。

之後,唯一要做的就是讓reactor開始執行迴圈。你可以使用我們之前任何一個用戶端(或者只用netcat)來測試我們的伺服器。

Discussion

回憶一下Part 5的圖8圖9,那些順序說明Twisted在建立一個新連接之後,一個新的Protocol實例是如何被建立以及初始化。這與Twisted在接受我們監聽埠上的連入連接時,也是使用了相同的機制,這也是為什麼connectTCP與listenTCP都需要一個factory引數。

我們在圖9沒有顯示的是connectionMade callback也被呼叫來作為Protocol初始化的一部分,不管什麼情況都是這樣,但我們在用戶端程式碼中不需要使用它。並且我們在用戶端的Protocol方法並沒有用在伺服器的實現中。因此,如果我們想要,我們可以建立一個共享函式庫,它有單一PoetryProtocol可用於用戶端與伺服器。事實上這是Twisted很典型的方法。例如NetstringReceiver Protocol可以從一個Transport中讀取與寫入netstrings

我們略過去撰寫我們底層版本的伺服器,但讓我們來思考一下底層中發生的事情。首先,呼叫listenTCP去告訴Twisted建立一個監聽socket,並且把socket加入事件迴圈。一個在監聽socket上的「事件」並不代表有資料要讀取,而是表示有一個用戶端等待著連接伺服器。

Twisted會自動接受連入的連接請求,進而建立一個新的用戶端socket,將伺服器直接連結到個別的用戶端。用戶端socket也會被加入到事件迴圈中,並且Twisted會建立一個新的Transport(透過PoetryFactory)與一個新的PoetryProcotol實例去服務這個用戶端。所以Protocol實例總是連接到用戶端socket,從來不是監聽socket。

我們可以在圖26中看見這一切:
26.the%2Bpoetry%2Bserver%2Bin%2Baction.png-Part 11: Your Poetry is Served

圖26.執行中的詩歌伺服器


在這張圖中有三個用戶端已經連接到詩歌伺服器。每個Transport代表一個用戶端socket,加上監聽socket總共四個file descriptors被select迴圈監控。當一個用戶端斷線時,相關的Transport與PoetryProcotol會被解除參考(dereferenced)並且被垃圾回收(假設我們沒有在某個地方藏著一個它們的參考,我們也應該避免這種情況來防止memory leaks(記憶體流失))。另一方面,只要我們的詩歌伺服器持續在監聽新的連接,PoetryFactory就會永遠的存在。就像美麗的詩歌,或者其他的東西。無論如何,圖26都是相當美麗的圖片,不是嗎?

如果我們所提供的詩歌相對來說比較短,那麼用戶端socket以及相關的Python物件不會存在太久。但對於一個較長的詩歌或一個非常繁忙的詩歌伺服器,我們最終同時可能有上百或上千個用戶端。這沒關係,Twisted對它可以處理的連接數輛沒有內建的限制。當然,當你在任何伺服器上增加了負載,在某些時候你會發現它無法即時回應或者達到了某些內部作業系統的限制。對於高負載的伺服器,仔細的測量與測試是每天必須的工作。

Twisted也不限制我們可以監聽的埠口數量。事實上,一個單獨的Twisted程序可以監聽數十個埠口並且對每一個提供不同的服務(藉由為每個listenTCP呼叫使用不同的工廠類別)。透過仔細的設計,無論你在一個Twisted程序或多個程序提供多個服務,你可以延後到佈署階段再來決定。

我們的伺服器還少了一些東西。首先,它不會產生任何可以幫助我們debug問題或分析網路流量的log。此外,伺服器也不是作為daemon在執行,使得它容易因意外的Ctrl+C而死亡(或著只是退出程序)。我們在將來的部分會修正這些問題,但首先,在Part 12,我們將撰寫另一個伺服器來進行詩歌轉換。

Suggested Exercises

  1. 不要使用Twisted寫一個非同步詩歌伺服器,就像我們在Part 2為用戶端做的那樣。注意監聽scoket為了讀取需要被監控,而一個「可讀的」監聽socket表示我們可以accept新的用戶端socket。
  2. 用Twisted寫一個底層的非同步詩歌伺服器,但是不使用listenTCP或protocol、 transport、與factory,就像我們在Part 4為用戶端做的那樣。所以你仍然會建立你自己的socket,但你能用Twisted reactor取代你自己的select迴圈。
  3. 使用callLater或LoopingCall去多次呼叫transport.write(),來Twisted詩歌伺服器的高層版本成為「慢速伺服器」。加入阻塞伺服器支援的--num-bytes與--delay命令列選項。不要忘記處理用戶端在收到完整詩歌前斷線的情況。
  4. 擴展高層詩歌伺服器讓它可以提供多首詩歌(在不同的埠口)。
  5. 從相同的Twisted程序提供多種服務有哪些原因?有哪些原因不要這樣做?

留言