鍍金池/ 教程/ Python/ Twisted 進程守護
小插曲 Deferred
異步編程模式與Reactor初探
使用Deferred新功能實現(xiàn)新客戶端
由twisted支持的客戶端
增強defer功能的客戶端
改進詩歌下載服務器
測試詩歌
更加"抽象"的運用Twisted
Deferred用于同步環(huán)境
輪子內(nèi)的輪子: Twisted和Erlang
Twisted 進程守護
構造"回調(diào)"的另一種方法
Twisted 理論基礎
惰性不是遲緩: Twisted和Haskell
第二個小插曲,deferred
使用Deferred的詩歌下載客戶端
Deferreds 全貌
結束
取消之前的意圖
由Twisted扶持的客戶端
改進詩歌下載服務器
初識Twisted

Twisted 進程守護

簡介

目前我們所寫的服務器僅僅運行在終端窗口,結果通過 print 語句輸出到屏幕.這對于開發(fā)來說已經(jīng)足夠,但對于產(chǎn)品級的部署還遠遠不夠. 健壯的產(chǎn)品級服務器應該:

  1. 運行一個 daemon 進程,這個進程不與任何終端或用戶會話相關.因為沒有人愿意當某用戶登出時服務自動關閉.
  2. 將調(diào)試和錯誤信息發(fā)送到一系列滾轉日志文件, 或者 syslog 服務.
  3. 放棄過高的權限,比如,在運行前切換到較低權限.
  4. 保存它的 pid 文件以便管理員方便地向 daemon 發(fā)送信號.

我們可以利用Twisted提供的 twistd 腳本獲得所有以上功能. 但是首先需要稍稍修改我們的代碼.

IService

IService 接口定義了一個可以啟動或停止的命名服務. 這個服務究竟做了些什么? 答案是任何你喜歡的事情——這個接口只需要自提供的一些通用屬性和方法,無須用戶定義特定的函數(shù).

這邊有兩個需要的屬性: namerunning.其中 name 屬性是一個字符串,如 "fastpoetry",或者 None 如果你不想給這個服務起名字. running 屬性是 Boolean 變量,如果服務成功啟動,值為 True.

下面我們只涉及 IService 的某些方法, 跳過那些很顯而易見的或者在簡單的Twisted程序中用不到的高級方法. startServicestopServiceIService 的兩個關鍵方法:

def startService():
    """
    Start the service.
    """

def stopService():
    """
    Stop the service.

    @rtype: L{Deferred}
    @return: a L{Deferred} which is triggered when the service has
        finished shutting down. If shutting down is immediate, a
        value can be returned (usually, C{None}).
    """

同樣,這些方法做什么取決于服務的需求,比如 startService 可能會:

  • 加載配置數(shù)據(jù),或
  • 初始化數(shù)據(jù)庫,或
  • 開始監(jiān)聽某端口,或
  • 什么也不做.

stopService 可能會:

  • 儲存狀態(tài),或
  • 關閉打開的數(shù)據(jù)庫連接,或
  • 停止監(jiān)聽某端口,或
  • 什么也不做.

當我們寫自定義服務時, 要恰當?shù)貙崿F(xiàn)這些方法.對于一些通用的行為,比如監(jiān)聽某端口,Twisted提供了現(xiàn)成的服務可以使用.

注意 stopService 可以選擇地返回 deferred,要求當服務完全關閉時被激發(fā).這允許我們的服務在結束之后與整個程序終止之前完成清理工作.如果你需要服務立即關閉,可以僅僅返回 None 而不是 deferred.

服務可以被組織成集合以便一起啟動和停止.下面來看看這里最后一個 IService 方法: setServiceParent,它添加一個服務到集合:

def setServiceParent(parent):
    """
    Set the parent of the service.
    @type parent: L{IServiceCollection}
        @raise RuntimeError: Raised if the service already has a parent
        or if the service has a name and the parent already has a child 
    by that name.
    """

任何服務都可以有雙親,這意味著服務可以被組織為層級結構.這把我們引向了今天討論的另一個接口.

IServiceCollection

IServiceCollection 接口定義了一個對象,它可包含若干個 IService 對象.一個服務集合僅僅是一個普通的類容器,具有以下方法:

Application

一個Twisted Application 不是通過一個單獨的接口定義的.相反, Application 對象需要實現(xiàn) IServiceIServiceCollection 接口以及一些我們未曾涉及的接口.

Application 是一個代表你整個Twisted應用的最頂層的服務. 在你 daemon 中的所有其他服務將是這個 Application 對象的兒子(甚至孫子,等等.).

其實需要你自己實現(xiàn) Application 的機會很小,Twisted已經(jīng)提供了一個當下常用的實現(xiàn).

Twisted Logging

Twisted在其模塊 twistd.python.log 中包含了其自身的日志架構.由于寫日志的基本 API 非常簡單, 我們僅僅介紹一個小例子: basic-twisted/log.py,如果你感興趣更多細節(jié)可以瀏覽Twisted模塊.

我們也不詳細介紹安裝日志處理程序的 API,因為 twistd 腳本會幫我們做.

FastPoetry 2.0

好吧,讓我們看看代碼.我們已經(jīng)將快詩服務器升級為使用 twistd. 源碼在 twisted-server-3/fastpoetry.py. 首先我們有了 詩歌協(xié)議:

class PoetryProtocol(Protocol):

    def connectionMade(self):
        poem = self.factory.service.poem
        log.msg('sending %d bytes of poetry to %s'
                 % (len(poem), self.transport.getPeer()))
        self.transport.write(poem)
        self.transport.loseConnection()

注意沒有使用 print 語句,而是使用 twisted.python.log.msg 函數(shù)去記錄每個新連接.

這里是 工廠類:

class PoetryFactory(ServerFactory):

    protocol = PoetryProtocol

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

正如你看到的,詩不再儲存在工廠中,而是儲存在一個被工廠引用的服務對象上。注意這邊協(xié)議是如何通過工廠從服務獲得詩歌.最后,看一下 服務類:

class PoetryService(service.Service):

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

    def startService(self):
        service.Service.startService(self)
        self.poem = open(self.poetry_file).read()
        log.msg('loaded a poem from: %s' % (self.poetry_file,))

就像許多其他接口類一樣,Twisted提供了一個基類供自定義實現(xiàn),同時具有方便的默認行為.

我們使用 twisted.application.service.Service 類實現(xiàn) PoetryService.

這個基類提供了所有必要方法的默認實現(xiàn),所以我們只需要實現(xiàn)個性化的行為.在上面的例子中,我們只重載了 startService 方法來加載詩歌文件.注:我們?nèi)匀徽{(diào)用了相應的基類方法(它為我們設置 running 屬性).

另外值得一提的是: PoetryService 對象不知道關于 PoetryProtocol 的任何細節(jié).這里服務的任務僅僅是加載詩歌以及為其他需要詩歌的對象提供接口.也就是說, PoetryService 只關心提供詩歌的更高層的細節(jié),而不是關心諸如通過 TCP 連接發(fā)送詩歌這樣的更底層的細節(jié).所以同樣的服務可以被另外的協(xié)議使用,如 UDPXML-RPC.雖然對于簡單的服務好處不大,但你可以想象其在更實際服務實現(xiàn)中的優(yōu)勢.

如果這是一個典型的Twisted程序,到目前我們看到的代碼都不該出現(xiàn)在這個文件里.它們應該在一些模塊當中(也許是 fastpoetryfastpoetry.service).但是,遵循我們的慣例會使這些例子自包含,也就是在一個腳本中包含了所有東西.

Twisted tac files

這個腳本的其余部分包含通常作為完整內(nèi)容的 Twisted tac 文件. tac 文件是一個 Twisted Application Configuration 文件,它告訴 twistd 怎樣去構建一個應用.作為一個配置文件,它負責選擇設置(如端口,詩歌文件位置等)來以一種特定的方式運行這個應用.換句話說, tac 代表我們服務的一個特定部署(在這個端口服務這首詩),而不是啟動任何詩歌服務的一般腳本.

如果我們在同一個域運行多個詩歌服務,我們將為每一個服務準備一個 tac 文件(因此你可以明白為什么 tac 文件通常不包含任何一般目的的代碼).在我們的例子中, tac 文件被配置為使 poetry/ecstasy.txt 運行在回環(huán)接口的10000號端口:

# configuration parameters
port = 10000
iface = 'localhost'
poetry_file = 'poetry/ecstasy.txt'

注意 twistd 并不知道這些特定變量,我們僅僅將這些配置值統(tǒng)一的放在這里.事實上, twistd 只關心整個文件中的一個變量,我們即將看到.下面我們開始建立我們的應用:

# this will hold the services that combine to form the poetry server
top_service = service.MultiService()

我們的詩歌服務器將包含兩個服務, 上文定義的 PoetryService,和一個Twisted的內(nèi)置服務,它將建立服務我們詩歌的監(jiān)聽套接字.由于這兩個服務明顯的相關,我們用 MultiService(一個實現(xiàn) IServiceCollectionIService 的類) 將它們組織在一起.

作為一個服務集合, MultiService 把我們的詩歌服務組織在一起.同時作為一個服務, MultiService 啟動時將啟動它的子服務,關閉時將關閉它的子服務.讓我們向服務集合 添加 第一個詩歌服務:

# the poetry service holds the poem. it will load the poem when it is
# started
poetry_service = PoetryService(poetry_file)
poetry_service.setServiceParent(top_service)

這是非常簡單的內(nèi)容.我們僅創(chuàng)建了 PoetryService,然后用 setServiceParent 方法將其添加到服務集合.下面我們添加 TCP 監(jiān)聽器:

# the tcp service connects the factory to a listening socket. it will
# create the listening socket when it is started
factory = PoetryFactory(poetry_service)
tcp_service = internet.TCPServer(port, factory, interface=iface)
tcp_service.setServiceParent(top_service)

Twisted為創(chuàng)建連接到任意工廠的 TCP 監(jiān)聽套接字提供了 TCPServer 服務(這里是 PoetryFactory),我們沒有直接調(diào)用 reactor.listenTCP 因為 tac 文件的工作是使我們的應用準備好開始,而不是實際啟動它. 這里 TCPServer 將在被 twistd 啟動后創(chuàng)建套接字.

你可能注意到我們沒有為任何服務起名字.為服務起名不是必需的,而僅是一個可選項,如果你希望在運行時查找服務.因為我們不需要這個功能,所以這里沒有為服務命名.

既然我們已經(jīng)將兩個服務綁定到服務集合.現(xiàn)只需創(chuàng)建我們的應用,并且將它添加到集合:

# this variable has to be named 'application'
application = service.Application("fastpoetry")

# this hooks the collection we made to the application
top_service.setServiceParent(application)

在這個腳本中 twistd 所關心的唯一變量就是 application. twistd 正是通過它找到那個需要啟動的應用(所以這個變量必須被命名為 applicaton).當應用被啟動時,我們添加到它的所有服務都會被啟動.

圖34顯示了我們剛剛建立的應用的結構:

http://wiki.jikexueyuan.com/project/twisted-intro/images/p16_application.png" alt="" />

Running the Server

讓我們的新服務器運轉起來.作為 tac 文件,我們需要用 twistd 啟動它.當然,它僅僅是一個普通的Python文件.所以我們首先用 python 命令啟動,再看看會發(fā)生什么:

python twisted-server-3/fastpoetry.py

如果你這樣做,會發(fā)現(xiàn)什么也沒有發(fā)生!正如前文所述, tac 文件的工作是使我們的應用準備好運行,而不是實際運行它.作為 tac 文件這個特殊目的的提醒,人們將它的擴展名規(guī)定為 .tac 而不是 .py.但是 twistd 腳本實際并不區(qū)分擴展名.

讓我們用 twistd 腳本來實際運行這個服務器:

twistd --nodaemon --python twisted-server-3/fastpoetry.py

運行以上命令后會看到如下輸出:

2010-06-23 20:57:14-0700 [-] Log opened.
2010-06-23 20:57:14-0700 [-] twistd 10.0.0 (/usr/bin/python 2.6.5) starting up.
2010-06-23 20:57:14-0700 [-] reactor class: twisted.internet.selectreactor.SelectReactor.
2010-06-23 20:57:14-0700 [-] __builtin__.PoetryFactory starting on 10000
2010-06-23 20:57:14-0700 [-] Starting factory <__builtin__.PoetryFactory instance at 0x14ae8c0>
2010-06-23 20:57:14-0700 [-] loaded a poem from: poetry/ecstasy.txt

需要注意的幾點:

  1. 你可以看到Twisted日志系統(tǒng)的輸出, 包括 PoetryFactory 調(diào)用 log.msg.但是我們在 tac 文件中沒有安裝 logger, 所以 twistd 會幫我們安裝.
  2. 你可以看到我們的兩個主要服務 PoetryServiceTCPServer 啟動了.
  3. shell提示符不會返回. 這表明我們的服務器沒有以守護進程方式運行. 默認地, twistd 會以守護進程方式運行服務器(這正是 twistd 存在的原因), 但是如果你包含"--nodaemon" 選項,那么 twistd 將以一個常規(guī)shell進程的方式運行你的服務器,同時會將日志輸出導向到標準輸出. 這對于調(diào)試 tac 文件非常有用.

下面測試取詩服務器, 通過我們的詩歌代理或者 netcat 命令:

netcat localhost 10000

這將從服務器抓取詩歌,并且你可以看到一行如下的日志:

2010-06-27 22:17:39-0700 [__builtin__.PoetryFactory] sending 3003 bytes 
    of poetry to IPv4Address(TCP, '127.0.0.1', 58208)

這個日志來自 PoetryProtocol.connectionMade 方法調(diào)用 log.msg.當你向服務器發(fā)送更多請求時, 你將看到更多的日志條目.

現(xiàn)在可以用 Ctrl-C 來終止這個服務器. 你可以看到如下輸出:

2010-06-29 21:32:59-0700 [-] Received SIGINT, shutting down.
2010-06-29 21:32:59-0700 [-] (Port 10000 Closed)
2010-06-29 21:32:59-0700 [-] Stopping factory <__builtin__.PoetryFactory instance at 0x28d38c0>
2010-06-29 21:32:59-0700 [-] Main loop terminated.
2010-06-29 21:32:59-0700 [-] Server Shut Down.

正如你看到的, Twisted并沒有簡單地崩潰, 而是優(yōu)雅地關閉并將日志信息告訴你.

好啦, 現(xiàn)在再次啟動服務器:

twistd --nodaemon --python twisted-server-3/fastpoetry.py

現(xiàn)在打開另一個shell并切換到 twisted-intro 目錄. 其中有一個叫 twistd.pid 的文件. 它是被 twistd 創(chuàng)建的, 包含我們這個運行服務器進程號. 試一下下面的方法來關閉服務器:

kill `cat twistd.pid`

注意當服務器關閉后, twistd.pid 文件消失了, 它被 twistd 清理了.

A Real Daemon

現(xiàn)在讓我們以守護進程的方式啟動服務器, 這是 twistd 的默認方式:

twistd --python twisted-server-3/fastpoetry.py

這次我們立即看到shell提示符返回. 當你列出目錄中的文件時,會發(fā)現(xiàn)除了 twistd.pid 文件,又出現(xiàn)了 twistd.log 文件,它記錄了之前顯示在shell窗口的日志信息.

當啟動一個守護進程時, twistd 安裝一個日志管理器將條目寫入一個文件而不是標準輸出. 默認的日志文件是 twistd.log, 它出現(xiàn)在你運行 twistd 的目錄中,但是你可以通過"--logfile"來改變它的位置. twistd 安裝的的日志管理器將滾動輸出日志信息, 確保其不超過 1M.

你可以通過列出操作系統(tǒng)上的所有進程來查看正在運行的服務器. 你不妨通過取另一首詩來測試這個服務器. 你可以看到記錄每個詩歌請求的新條目出現(xiàn)在日志文件中.

由于這個服務器不再與shell相連(或者除了 init 的任何其他進程), 你不能通過 Ctrl-C 關閉它. 作為一個真的守護進程, 即使你登出它也繼續(xù)運行.但是你可以通過 twistd.pid 文件終止這個進程:

kill `cat twistd.pid`

隨后, 關閉消息出現(xiàn)在日志文件中, twistd.pid 文件被移除, 服務器停止.

檢查一下其他的 twistd 啟動選項是個不錯的主意. 例如,你可以告訴 twistd 在啟動進程守護前切換到另一個用戶或組賬戶(是一種當你的服務器不需要安全防范措施取消權限的典型方法). 我們就不進一步探討那些額外的選項了,你可以通過 twistd--help 自己研究它們.

Twisted 插件系統(tǒng)

現(xiàn)在我們已經(jīng)通過 twistd 啟動真正的守護進程服務器. 這非常完美,而且事實上我們的配置文件是純Python源碼文件,這一點為我們設置帶來巨大便利. 但是我們有時用不到這樣的便利性.對于詩歌服務器,我們通常只關心一小部分選項:

  1. 需要服務的詩歌
  2. 服務端口
  3. 監(jiān)聽接口

為了幾個簡單的變量建立一個 tac 文件顯得有點小題大做. 如果我們能夠通過 twistd 選項指定這些值將非常方便. Twisted的插件系統(tǒng)允許我們可以這樣做.

Twisted插件通過定義 Application 提供了一種方法, 可以實現(xiàn)個性化的命令行選項, 進而 twistd 動態(tài)的發(fā)現(xiàn)和運行. Twisted本身具有一套插件,你可以通過運行不帶參數(shù)的 twistd 命令來查看它們. 現(xiàn)在就試一試, 在 twisted-intro 目錄外. 在幫助部分后面,你可以看到如下輸出:

...
ftp                An FTP server.
telnet             A simple, telnet-based remote debugging service.
socks              A SOCKSv4 proxy service.
...

每一行顯示了一個Twisted內(nèi)置的插件, 你可以用 twistd 運行它們.

每個插件同樣有它們自己的選項,你可以通過 --help 來發(fā)現(xiàn)它們. 讓我們看看 ftp 插件有什么選項:

twistd ftp --help

注意我們需要將 --help 放在 ftp 后面而不是 twistd 后面, 因為我們想得到 ftp 的可選項.

我們可以像運行詩歌服務器一樣運行 ftp 服務器. 但由于它是一個插件,我們可以僅僅通過它的名字運行:

twistd --nodaemon ftp --port 10001

以上命令以非守護進程的方式在端口 10001 上運行 ftp 插件. 注意 twistdnodaemon 選項出現(xiàn)在插件名字的前面,插件特定選項 port 出現(xiàn)在插件名字的后面. 正如我們的詩歌服務器一樣,你可以用 Ctrl-C 停止它.

OK, 讓我們把詩歌服務器轉化為Twisted的插件. 首先我們需要介紹一些新概念.

IPlugin

任何Twisted插件都需要實現(xiàn) twisted.plugin.IPlugin 接口. 如果你瀏覽這個接口的聲明, 你會發(fā)現(xiàn)它沒有指定任何方法. 實現(xiàn) IPlugin 接口僅僅相當于一個插件在說:"你好,我是插件!"以便 twistd 找到它. 當然,出于實用考慮,它需要實現(xiàn)一些其他接口,我們很快會介紹.

但是你怎樣知道一個對象實現(xiàn)了一個空接口? zope.interface 包含了一個叫做 implements 的函數(shù),它可以用來聲明一個特定類實現(xiàn)了一個特定的接口. 我們將在插件版的詩歌服務器中看到這種使用.

IServiceMaker

除了 IPlugin,我們的插件還實現(xiàn) IServiceMaker 接口. 一個實現(xiàn)了 IServiceMaker 接口的對象知道如何創(chuàng)建 IService,它將成為運行程序的核心. IServiceMaker 指定了三個屬性和一個方法:

  1. tapname: 代表插件名字的字符串. "tap"代表"Twisted Application Plugin". 注:老版本的Twisted還使用"tapfiles"文件,不過這個功能現(xiàn)在已經(jīng)取消了.
  2. description: 插件的描述, twistd 將以它作為幫助信息輸出.
  3. options: 一個代表這個插件接受的命令行選項的對象.
  4. makeService: 一個創(chuàng)建 IService 對象的方法,需提供一些特定的命令行選項.

我們將在下一個版本的詩歌服務器中看到怎樣將上述內(nèi)容組織在一起.

Fast Poetry 3.0

現(xiàn)在我們已經(jīng)為插件版本的"Fast Poetry"做好準備,它位于 twisted/plugins/fastpoetry_plugin.py.

你可能注意到與其他例子不同, 我們命名了一個不同的目錄. 這是因為 twistd 需要插件文件位于 twisted/plugins 目錄中, 同時在你的Python搜索路徑上. 這個目錄不必是一個包(也就是, 不必包含任何 __init__.py 文件), 而且在路徑上可以有多個 twisted/plugins 目錄, twistd 都會找到它們. 這個插件的實際文件名是什么也沒有關系, 但是一個好的方案是根據(jù)應用所代表的含義來命名, 就像我們在這里做的.

我們的插件開頭部分同樣包括詩歌協(xié)議,工廠,以及像 tac 文件中所實現(xiàn)的服務.如前所述,這些代碼通常應該單獨的存在于一個模塊中,但出于我們例子自包含的目的,還是將它們放在插件文件中.

下面將 聲明 這個插件的命令行選項:

class Options(usage.Options):

      optParameters = [
          ['port', 'p', 10000, 'The port number to listen on.'],
          ['poem', None, None, 'The file containing the poem.'],
          ['iface', None, 'localhost', 'The interface to listen on.'],
      ]

以上代碼指定可以放在 twistd 命令后面使用的插件特定選項的名字.

這里就不必進一步解釋上述選項的含義了,其含義很顯然. 下面我們來看一下插件的主要部分 服務制造類:

class PoetryServiceMaker(object):

    implements(service.IServiceMaker, IPlugin)

    tapname = "fastpoetry"
    description = "A fast poetry service."
    options = Options

    def makeService(self, options):
        top_service = service.MultiService()

        poetry_service = PoetryService(options['poem'])
        poetry_service.setServiceParent(top_service)

    factory = PoetryFactory(poetry_service)
    tcp_service = internet.TCPServer(int(options['port']), factory,
                interface=options['iface'])

    tcp_service.setServiceParent(top_service)

    return top_service

這里你可以看到如何使用 zope.interface.implements 函數(shù)來聲明我們的類同時實現(xiàn) IServiceMakerIPlugin 接口.

你應該從之前的 tac 文件辨認出 makeService 中的代碼, 但是這次我們不需要自己建立一個 Application 對象, 我們僅僅創(chuàng)建并返回最頂層服務,這樣我們的程序就可以運行, twistd 來處理其余的事情. 注意我們是如何使用 options 參數(shù)來提取插件傳遞給 twistd 的特定命令行選項.

定義了上述類, 還有 一步 :

service_maker = PoetryServiceMaker()

twistd 腳本會發(fā)現(xiàn)我們插件的實例并使用它構建最頂層服務. 與 tac 文件不同的是, 選擇什么變量名沒有關系, 關鍵是我們的對象實現(xiàn)了 IPluginIServiceMaker 接口.

既然已經(jīng)創(chuàng)建了插件, 讓我們運行它. 確保你位于 twisted-intro 目錄中, 或者 twisted-intro 位于Python的搜索目錄中. 下面單獨運行 twistd,你會看到"fastpoetry"是列出的插件之一,后面顯示插件文件中定義的描述文字.

你同樣會注意到 twisted/plugins 目錄中出現(xiàn)了一個 dropin.cache 的新文件. 這個文件由 twistd 創(chuàng)建, 用來加速后續(xù)掃描插件的.

現(xiàn)在讓我們獲取一些關于插件的幫助信息:

twistd fastpoetry --help

你可以看到關于 fastpoetry 插件選項的幫助性文字. 最后,運行這個插件:

twistd fastpoetry --port 10000 --poem poetry/ecstasy.txt

這將以守護進程方式啟動 fastpoetry 服務器. 與前面例子一樣, 你會在當期文件夾看到 twistd.pidtwistd.log 文件. 測試完我們的服務器, 用一下命令關閉:

kill `cat twistd.pid`

這就是如何制作Twisted插件的方法.

總結

在這個部分, 我們學習了將Twisted服務器轉換到支持長時間運行的守護進程模式. 我們還涉及了Twisted日志系統(tǒng)以及如何使用 twistd 以守護進程模式啟動一個Twisted應用程序, 即或者通過 tac 配置文件或者Twisted插件. 在第十七部分 我們將轉向異步編程的更基本的主題和另外一種結構化Twisted回調(diào)函數(shù)的方法.

參考練習

  1. 修正 tac 文件以在另外一個端口服務另外一首詩. 使用另外一個 MultiService 對象以保持每首詩的服務是分離的.
  2. 創(chuàng)建一個新的 tac 文件來啟動一個詩歌代理服務器.
  3. 修正插件文件使其可接受第二個可選詩歌文件和服務端口.
  4. 為詩歌代理服務器創(chuàng)建一個新的插件.

參考

本部分原作參見: dave @ http://krondo.com/blog/?p=2345

本部分翻譯內(nèi)容參見luocheng @ https://github.com/luocheng/twisted-intro-cn/blob/master/p16.rst