天天看點

[轉]使用 Twisted Matrix 架構來進行網絡程式設計

作者:David Mertz ([email protected]), 程式員,博士, Gnosis Software, Inc.

第 1 部分 了解異步聯網

對 Twisted Matrix 進行分類有點像盲人摸象。Twisted Matrix 擁有許多能力,徹底了解這些能力的作用需要思維模式進行轉變。實際上,在我寫這第一部分時,對于 Twisted Matrix 我可能也隻是一知半解。我們可以一起來掌握它。

對于 Python 最近的一些版本,其優點之一在于,它們“功能齊全(batteries included)” — 即,标準分發版包含的子產品可以讓您完成大多數程式設計任務中要完成的幾乎任何工作。一般而言,當您想要一個第三方 Python 子產品或軟體包時,您所要做的是完成某個專門且不尋常的任務。Twisted Matrix 是所描述模式的少數幾個例外之一;它是一個精心設計的通用子產品集合,用于執行各種形式的網絡程式設計任務,它改變了用 Python 标準庫不易于輕松地進行網絡程式設計的局面。

Python 的标準庫缺少對異步、非阻塞網絡應用程式的支援的說法并不完全正确。子產品

asyncore

對單個線程内的 I/O 通道之間進行切換提供了基本支援。但是,Twisted Matrix 将這種風格提高到了一個更高的層次,它提供大量預先建構且可重用的協定、接口群組件。

第一個伺服器

Twisted Matrix 附帶的文檔十分詳盡,但卻很難掌握。讓我們從一個簡單的伺服器開始,并以之為基礎進行建構。在最近一篇 developerWorks技巧文章(請參閱 參考資料以擷取連結)中,我示範了一個基于 XML 的“Weblog 伺服器”,它向客戶機提供了 Web 伺服器最新點選數的記錄流。XML 方面的問題在這裡不很重要,但可以将

SocketServer

及其

ThreadingTCPServer

類作為基線。這個未使用 Twisted Matrix 的伺服器包括:

清單 1. SocketServer-weblog.py  

from SocketServer import BaseRequestHandler, ThreadingTCPServer
from time import sleep
import sys, socket
from webloglib import log_fields, hit_tag
class WebLogHandler(BaseRequestHandler):
    def handle(self):
        print"Connected from", self.client_address
        self.request.sendall('<hits>')
        try:
            while True:
                for hit in LOG.readlines():
                    self.request.sendall(hit_tag % log_fields(hit))
                sleep(5)
        except socket.error:
            self.request.close()
        print"Disconnected from", self.client_address
if __name__=='__main__':
    global LOG
    LOG = open('access-log')
    LOG.seek(0, 2)     # Start at end of current access log
    srv = ThreadingTCPServer(('',8888), WebLogHandler)
    srv.serve_forever()
     

           

除了建立每個客戶機線程的開銷之外,這個基于

SocketServer

的伺服器一個引人注目的特性在于它對其處理程式内的

time.sleep()

使用阻塞調用。對于 Twisted Matrix 的非阻塞

select()

循環,這樣的阻塞是不允許的。

第一個非阻塞方法将任何人為的延遲推給客戶機,讓客戶機明确地請求每批新的 Weblog 記錄(它也發送一條消息以表明缺少記錄,而不是什麼都不發送)。這個使用 Twisted Matrix 的伺服器看起來類似:

清單 2. twisted-weblog-1.py

from twisted.internet import reactor
from twisted.internet.protocol import Protocol, Factory
from webloglib import hit_tag, log_fields
class WebLog(Protocol):
    def connectionMade(self):
        print"Connected from", self.transport.client
        self.transport.write('<hits>')
    def dataReceived(self, data):
        newhits = LOG.readlines()
        ifnot newhits:
            self.transport.write('<none/>')
        for hit in newhits:
            self.transport.write(hit_tag % log_fields(hit))
    def connectionLost(self, reason):
        print"Disconnected from", self.transport.client
factory = Factory()
factory.protocol = WebLog
if __name__=='__main__':
    global LOG
    LOG = open('access-log')
    LOG.seek(0, 2)     # Start at end of current access log
    reactor.listenTCP(8888, factory)
    reactor.run()
    
           

讀者應該參考我先前的一篇技巧文章,以了解客戶機應用程式的詳細資訊。但是必須注意下面的更改。主客戶機循環增加了兩行:

清單 3. 增強的(阻塞)客戶機循環

while 1:
    xml_data = sock.recv(8192)
    parser.feed(xml_data)
    sleep(5)          # Delay before requesting new records
    sock.send('NEW?') # Send signal to indicate readiness
    
           

回頁首

Twisted 伺服器的部件

一個 Twisted Matrix 伺服器由幾個子產品化元素組成。在位元組流級别,伺服器實作了一項協定,這通常是通過繼承

twisted.internet.protocol.Protocol

或繼承該類先前專門化的某個子類實作的。例如,假設(

twisted.protocols

中的)子類包括

dns

ftp

gnutella

http

nntp

shoutcast

以及其他許多協定。協定基本上應該知道如何處理連接配接的建立和斷開,以及如何在連接配接中接收和發送資料。這些職責與基于

SocketServer

的伺服器中的職責沒有多大差別,差異在于,前者在為每個元素定義方法的子產品化方面略勝一籌。

Twisted Matrix 伺服器的下一個級别是工廠。在我們的

twisted-weblog-1.py

示例中,工廠除了存儲協定以外其實沒做别的事情。不過,在較複雜的伺服器中,工廠是執行與協定伺服器有關的初始化和終止化操作的好地方。最重要的一點可能是,工廠可以在 應用程式中持久存儲(我們很快将看到這一點)。

協定和工廠對伺服器運作時所處的網絡都一無所知。相反, 反應器(reactor)是實際偵聽網絡的類(它利用其協定的工廠執行個體來進行偵聽)。反應器基本上隻是一個偵聽給定端口和網絡接口的循環(選擇哪個端口和網絡接口是通過調用諸如

.listenTCP()

.listenSSL()

.listenUDP()

之類的方法實作的)。Twisted Matrix 中的基本反應器

SelectReactor

運作在單個線程内,這一點是需要明白的;該伺服器會針對新資料檢查每一個連接配接,并将資料傳遞給相關的協定對象。所産生的結果就是, 确實不允許協定對象阻塞,甚至花費的時間太長以至無法完成(必須适當地進行協定程式設計)。

回頁首

增強的伺服器

讓我們設法增強 Twisted Weblog 伺服器,以便它遵循

SocketServer-weblog.py

的模式;無須客戶機重複請求即可向客戶機提供新記錄。這裡的問題是,向

WebLog(Protocol)

方法中插入

time.sleep()

調用會導緻它阻塞,是以是不允許的。在我們這樣做的時候,請注意以前的伺服器可能會犯錯誤,因為它們隻向一個客戶機提供每批新記錄。我們猜測,如果您想允許多個客戶機監控一個 Weblog,那麼您也會希望它們都接收正在進行的更新。

Twisted Matrix 在不阻塞的情況下延遲操作的方法是使用

.callLater()

方法向反應器添加回調。以此方法添加的回調被添加到提供服務的事件隊列中,但隻有在指定的延遲之後才會真正地對其進行處理。将這兩項更改放在一起,增強的 Weblog 伺服器看起來類似:

清單 4. twisted-weblog-1.py

from twisted.internet import reactor
from twisted.internet.protocol import Protocol, Factory
from webloglib import hit_tag, log_fields
import time
class WebLog(Protocol):
    def connectionMade(self):
        print"Connected from", self.transport.client
        self.transport.write('<hits>')
        self.ts = time.time()
        self.newHits()
    def newHits(self):
        for hit in self.factory.records:
            if self.ts <= hit[0]:
                self.transport.write(hit_tag % log_fields(hit[1]))
        self.ts = time.time()
        reactor.callLater(5, self.newHits)
    def connectionLost(self, reason):
        print"Disconnected from", self.transport.client
class WebLogFactory(Factory):
    protocol = WebLog
    def __init__(self, fname):
        self.fname = fname
        self.records = []
    def startFactory(self):
        self.fp = open(self.fname)
        self.fp.seek(0, 2) # Start at end of current access log
        self.updateRecords()
    def updateRecords(self):
        ts = time.time()
        for rec in self.fp.readlines():
            self.records.append((ts, rec))
        self.records = self.records[-100:]  # Only keep last 100 hits
        reactor.callLater(1, self.updateRecords)
    def stopFactory(self):
        self.fp.close()
if __name__=='__main__':
    reactor.listenTCP(8888, WebLogFactory('access-log'))
    reactor.run()
    
           

在這個示例中,我們定義了一個定制工廠,并将一些初始化代碼從

_main_

塊移到了該工廠。還要注意的是,該伺服器的客戶機不需要(也不應該)休眠或發送新的請求 — 實際上,我使用的客戶機應用程式就是我在 XML 技巧文章中讨論過的客戶機應用程式(請參閱 參考資料)。

工廠和協定在各自的定制方法

.updatedRecords()

.newHits()

中使用了相同的技術。即,如果方法想要定期運作,那麼其最後一行可以排程該方法在指定的延遲以後重複運作。表面上看來,該模式很像遞歸 — 但它不是遞歸(而且重複排程不需要一定在最後一行進行,可以在您期望的地方進行排程)。例如,方法

.newHits()

簡單地讓控制反應器循環知道它希望再過 5 秒鐘後被調用,但該方法本身卻終止了。我們并不要求方法隻能排程自己 — 它可以排程所期望的任何事情,如果願意的話,也可以将工廠和協定以外的函數添加到反應器循環。

回頁首

持久性和排程

除了

reactor.callLater()

排程以外,Twisted Matrix 還包含一個通用類

twisted.internet.defer.Deferred

。實際上, 延遲是對象被排程回調的泛化,但它們也允許使用諸如連結依賴回調和在這些連結中進行錯誤條件處理之類的技術。

Deferred

對象背後的思想是:當您調用一個方法時,我們不等待其結果(結果可能要過一會兒才出來),該方法可以立即傳回一個

Deferred

對象,而反應器/排程程式稍後可以重新調用此對象,那時可望可以得到結果。

我還沒有真正地使用

Deferred

對象,但要使它們正常工作好像有些困難。如果您需要等待一個阻塞操作 — 比如,對于來自遠端資料庫查詢的結果 — 您不會确切地知道在可以使用結果之前究竟要等待多長時間。

Deferred

對象 确實有一個逾時機制,但我要在今後的文章才讨論這一機制。感興趣的讀者至少應該知道,Twisted Matrix 開發人員已經試圖提供一個标準 API 來包裝阻塞操作。當然,最壞的情形是回退到使用線程來進行阻塞操作,因為這些操作确實無法轉換成異步回調。

Twisted Matrix 伺服器另外一個重要元素是它們對持久性提供了友善的支援。反應器是一個監控 I/O 事件并對這些事件做出響應的循環。應用程式類似于增強的反應器,能夠将其狀态進行 pickle 處理(即序列化),以便用于随後的重新啟動。而且,可以将應用程式“有狀态地”儲存到“.tap”檔案,并且可以使用工具

twistd

對其進行管理和監控。這裡有一個簡單的示例,示範了其用法(它是根據 Twisted 文檔的

OneTimeKey

示例進行模組化的)。該伺服器将不同的 Fibonacci 數傳遞給所有感興趣的客戶機,而不會在它們之間重複這些數字 — 即使伺服器被停止然後被啟動:

清單 5. fib_server.py

from twisted.internet.app import Application
from twisted.internet.protocol import Protocol, Factory
class Fibonacci(Protocol):
    "Serve a sequence of Fibonacci numbers to all requesters"def dataReceived(self, data):
        self.factory.new = self.factory.a + self.factory.b
        self.transport.write('%d' % self.factory.new)
        self.factory.a = self.factory.b
        self.factory.b = self.factory.new
def main():
    import fib_server    # Use script as namespace
    f = Factory()
    f.protocol = fib_server.Fibonacci
    f.a, f.b = 1, 1
    application = Application("Fibonacci")
    application.listenTCP(8888, f)
    application.save()
if'__main__' == __name__:
    main()
    
           

您可以看到,我們所做的所有工作主要是用

application

替換

reactor

。雖然類

Application

也有一個

.run()

方法,但我們仍然使用其

.save()

方法來建立一個

Fibonacci.tap

檔案。運作該伺服器的操作如下所示:

清單 6. 運作 fib_server.py

% python fib_server.py
% twistd -f Fibonacci.tap
...let server run, then shut it down...
% kill `cat twistd.pid`
...re-start server where it left off...
% twistd -f Fibonacci-shutdown.tap
...serve numbers where we left off...
           

連接配接到該伺服器的客戶機如果隻是間歇地需要新數字,而不需要盡快地得到新數字的話,那麼它應該在其循環中使用

time.sleep()

。顯然,更有用的伺服器可以提供更有趣的有狀态資料流。

回頁首

接下來是什麼?

本文讨論了 Twisted Matrix 比較低級别的細節 — 定義定制協定以及其他内容。但 Twisted Matrix 存在于許多級别中 — 包括用于 Web 服務及其他公共協定的進階别模闆制作。在這一系列文章的下一篇中,我們将開始具體地研究 Web 服務,并将挑選一些尚未讨論的雜項主題來進行研究。

第 2 部分  實作 Web 伺服器

在 本系列文章的第 1 部分中,我們研究了 Twisted 的低級方面,比如定義定制協定。從很大程度上講,Twisted 的這些低級方面是最容易掌握的。盡管異步的非阻塞樣式對于習慣了線程技術的開發人員而言多少有點新奇,但是新協定能夠符合 Twisted Matrix 文檔中的示例。較進階的 Web 開發工具發展得越來越快,因而要了解更多的 API 細節。事實上,雖然 Twisted 的 Web 模闆制作架構 woven已經很成熟,但它還不夠穩定,是以我在此隻簡略提及。

Twisted 庫的名稱要說明一下。“Twisted Matrix Laboratories”是位于地球上各個角落的一組不同的開發人員對其自身的稱呼,會有一定的變化。用于事件驅動的網絡程式設計的 Python 庫就稱為“Twisted”- 我的最後一篇專欄文章并沒有仔細區分這個組和産品。

增強 Weblog 伺服器的功能

我們之前研究過價值甚微的伺服器,它使用定制協定以及定制伺服器和客戶機來遠端監控網站的通路率。對于本文,讓我們用基于 Web 的接口來增強該功能。在我們的方案中可以使用某個 URL 來監控網站所接收的通路量。

對于基于 Web 的 Weblog 伺服器,有一種非常簡單的方法,它與 Twisted 在本質上毫不相幹。假定您隻讓像 Weblog.html 這樣的 Web 頁面列出有關對網站的最近幾次通路的資訊。與前面的示例保持一緻的同時,我們将顯示通路的送出者和資源,但是隻有在請求的狀态碼為

200

(并且送出者可用)時才如此。在我的網站(請參閱 參考資料以擷取連結)上可以找到此類頁面(其内容沒有更新)的示例。

我們需要做兩件事:(1) 将

<meta http-equiv=refresh ...>

标記放在 HTML 頭中,使顯示保持最新;(2) 一旦發生新的通路就間歇地重寫 Weblog.html 檔案本身。第二個任務隻需要一個一直運作的背景程序,例如:

清單 1. logmaker.py Weblog 重新整理器腳本

from webloglib import log_fields, TOP, ROW, END, COLOR
import webloglib as wll
from urllib import unquote_plus as uqp
import os, time
LOG = open('../access-log')
RECS = []
PAGE = 'www/weblog.html'
while 1:
    page = open(PAGE+'.tmp','w')
    RECS.extend(LOG.readlines())
    RECS = RECS[-35:]
    print >> page, TOP
    odd = 0
    for rec in RECS:
        hit = [field.strip('"') for field in log_fields(rec)]
        if hit[wll.status]=='200' and hit[wll.referrer]!='-':
            resource = hit[wll.request].split()[1]
            referrer = uqp(hit[wll.referrer]).replace('&',' &')
            print >> page, ROW % (COLOR[odd], referrer, resource)
            odd = not odd
    print >> page, END
    page.close()
    os.rename(PAGE+'.tmp',PAGE)
    time.sleep(5)
    
           

子產品

Webloglib

中包含了所使用的精确 HTML,以及用于日志字段位置的一些常量。您可以從 參考資料所列出的 URL 中下載下傳該子產品。

這裡要注意的是:不必将 Twisted 用作伺服器 - Apache 或任何其他 Web 伺服器都可以很好地擔當此任。

回頁首

建立 Twisted Web 伺服器

運作 Twisted Web 伺服器非常簡單 - 或許比啟動其他伺服器還要簡單。運作 Twisted Web 伺服器的第一步是建立一個 .tap 檔案,就像我們在第一篇文章中所看到的那樣。您 可以通過在腳本中定義應用程式、包括對

application.save()

的調用然後運作該腳本來建立 .tap 檔案。但是您也可以使用工具 mktap 來建立 .tap 檔案。事實上,對于許多公共協定,您可以建立伺服器 .tap 檔案,而完全不需要任何特殊的腳本。例如:

mktap Web --path ~/twisted/www --port 8080

這建立了一個非常通用的伺服器,它在端口 8080 上處理來自基本目錄 ~/twisted/www 的檔案。要運作該伺服器,請使用工具 twistd來啟動所建立的 Web.tap 檔案。

twistd -f Web.tap

對于 HTTP 之外的其他類型的伺服器,您也可以使用其他名稱來代替

Web

dns

conch

news

telnet

im

manhole

等。這些名稱中有些是常見的伺服器,其他則特定于 Twisted。而且一直都可以添加更多名稱。

正好位于基本目錄的任何靜态 HTML 檔案都可以由該伺服器進行傳遞,這和其他伺服器非常相似。但是另外有一點,您還可以處理擴充名為 .rpy 的動态頁面 - 從概念上講,這些動态頁面類似于 CGI 腳本,但是它們避免了減慢 CGI 速度的派生(fork)開銷和解釋器啟動時間。Twisted 動态腳本的結構與 CGI 腳本略有不同;最簡單的情況下它可以類似于:

清單 2. www/dynamic.rpy Twisted 頁面

from twisted.web import resource
page = '''<html><head><title>Dynamic Page</title></head>
<body>
  <p>Dynamic Page served by Twisted Matrix</p>
</body>
</html>'''
class Resource(resource.Resource):
    def render(self, request):
        return page
resource = Resource()
           

檔案級變量

resource

很特殊 - 它需要指向

twisted.web.resource.Resource

子類的執行個體,該類定義了

.render()

方法。您在所處理的目錄中想包括多少動态頁面就可以包括多少,并且可以自動處理每個頁面。

回頁首

使用 Twisted 來更新靜态頁面

在我的第一篇 Twisted 文章中所提出的定時回調技術可以用來定期更新上面所讨論的 Weblog.html 檔案。也就是說,您可以用非阻塞

twisted.internet.reactor.callLater()

調用來替換

logmaker.py

中的

time.sleep()

調用:

清單 3. tlogmaker.py Weblog 重新整理器腳本

from webloglib import log_fields, TOP, ROW, END, COLOR
import webloglib as wll
from urllib import unquote_plus as uqp
import os, twisted.internet
LOG = open('../access-log')
RECS = []
PAGE = 'www/weblog.html'
def update():
    global RECS
    page = open(PAGE+'.tmp','w')
    RECS.extend(LOG.readlines())
    RECS = RECS[-35:]
    print >> page, TOP
    odd = 0
    for rec in RECS:
        hit = [field.strip('"') for field in log_fields(rec)]
        if hit[wll.status]=='200' and hit[wll.referrer]!='-':
            resource = hit[wll.request].split()[1]
            referrer = uqp(hit[wll.referrer]).replace('&',' &')
            print >> page, ROW % (COLOR[odd], referrer, resource)
            odd = not odd
    print >> page, END
    page.close()
    os.rename(PAGE+'.tmp',PAGE)
    twisted.internet.reactor.callLater(5, update)
update()
twisted.internet.reactor.run()
           

logmaker.py

tlogmaker.py

的差别不大 - 兩者都可以在背景啟動并且都可以讓它們一直運作以更新頁面

referesher.html

。更有趣的是可以将

tlogmaker.py

目錄建構到 Twisted 伺服器中,而不是僅讓它在背景程序中運作。這非常簡單,我們隻需要在該腳本結尾處再添加兩行:

from twisted.web import static resource = static.File("~/twisted/www")

還可以除去對

twisted.internet.reactor.run()

的調用。通過這些更改,使用下面兩行腳本建立伺服器:

mktap --resource-script=tlogmaker.py --port 8080
        

--path ~/twisted/www
      
           

然後像前面那樣使用

twistd

來運作已建立的

web.tap

伺服器。現在 Web 伺服器自己可以使用其标準核心分派循環每五秒鐘重新整理一下頁面 Weblog.html。

回頁首

使 Weblog 變成動态頁面

處理 Web 日志的另一種方法是每次收到請求時使用動态頁面來生成最新通路量。但是,每次接收到這樣的一個請求就讀取整個

access-log

檔案并不是個好主意 - 忙碌的網站在日志檔案中可能有幾千條記錄,反複讀取這些記錄非常耗時間。更好的辦法是讓 Twisted 伺服器自己擁有一個針對日志檔案的檔案句柄,隻在需要時才讀取 新記錄。

在某種程度上,讓伺服器維護檔案句柄正是

tlogmaker.py

所做的工作,但是它将最新的記錄存儲在檔案而不是存儲在記憶體中。但是,這種方法強迫我們圍繞該持久性功能編寫整個伺服器。讓各個動态頁面分别向伺服器發出自己的持久性請求會更加好。例如,通過這種方法您可以添加新的有狀态動态頁面,而不必停止或改變長期運作的(和通用的)伺服器。頁面配置設定的持久性的關鍵是 Twisted 的 系統資料庫。例如,下面是一個處理 Weblog 的動态頁面:

清單 4. www/Weblog.rpy 動态 Weblog 頁面

from twisted.web import resource, server
from persist import Records
from webloglib import log_fields, TOP, ROW, END, COLOR
import webloglib as wll
records = registry.getComponent(Records)
if not records:
   records = Records()
   registry.setComponent(Records, records)
class Resource(resource.Resource):
    def render(self, request):
        request.write(TOP)
        odd = 0
        for rec in records.getNew():
            print rec
            hit = [field.strip('"') for field in log_fields(rec)]
            if hit[wll.status]=='200' and hit[wll.referrer]!='-':
                resource = hit[wll.request].split()[1]
                referrer = hit[wll.referrer].replace('&',' &')
                request.write(ROW % (COLOR[odd],referrer,resource))
                odd = not odd
        request.write(END)
        request.finish()
        return server.NOT_DONE_YET
resource = Resource()
           

一開始會對系統資料庫産生的疑惑是 Weblog.rpy 從未導入它。.rpy 腳本和純 .py 腳本不完全一樣 - 前者在 Twisted 環境 中運作,該環境提供了對其中的

register

的自動通路。

request

對象是另一個來自架構而非 .rpy 自身的東西。

還請注意傳回頁面内容的方式,這種方式有些新鮮。上面不隻傳回 HTML 字元串,我們将幾次針對

request

對象的寫操作高速緩存起來,然後通過調用

request.finish()

來完成這些工作。模樣奇特的傳回值

server.NOT_DONE_YET

是一個标記,要求 Twisted 伺服器将頁面内容清出

request

對象。另一個選項是将

Deferred

對象添加到請求中,并在執行對

Deferred

的回調時處理頁面(例如,如果直到資料庫查詢完成後才能生成頁面)。

回頁首

建立持久性對象

請注意 Weblog.rpy 頂部少量的條件邏輯。第一次處理動态頁面時,

Records

對象還未被添加到系統資料庫中。但是第一次之後,我們希望每次調用

records.getNew()

都使用相同的對象。如果調用

registry.getComponent()

成功,則這次調用會傳回對應類的已注冊對象,否則就傳回一個錯誤值以允許進行測試。當然,調用過程之間,對象儲存在 Twisted 伺服器的位址空間中。

持久性類最好放在 .rpy 檔案所導入的子產品中。這樣一來,每個動态頁面都可以利用您編寫的持久性類。執行個體屬性中可以包含您喜歡的任何類型的持久性。但是,有些東西(比如開放檔案)不能在伺服器關閉時儲存(但是,簡單的值可以在伺服器運作之間儲存,并且可以儲存在諸如 web-shutdown.tap 之類的檔案中)。我使用的子產品

persist

包含了一個非常簡單的類

Counter

,該類借用自 Twisted Matrix 文檔,還包含另一個類

Records

,我将它用于 Weblog 動态頁面:

清單 5. 持久性支援子產品 persist.py

class Counter:
    def __init__(self):
        self.value = 0
    def increment(self):
        self.value += 1
    def getValue(self):
        return self.value
class Records:
    def __init__(self, log_name='../access-log'):
        self.log = open(log_name)
        self.recs = self.log.readlines()
    def getNew(self):
        self.recs.extend(self.log.readlines())
        self.recs = self.recs[-35:]
        return self.recs
        
           

您可以很自由地在持久性類中放置您喜歡的任何方法 - 系統資料庫隻是在各次對動态頁面的調用之間将執行個體儲存在記憶體中。

回頁首

下一次

在本文中,我們研究了 Twisted Web 伺服器的基礎。安裝基本伺服器(或者甚至是有少許定制代碼的伺服器)是非常簡單的。但是

twisted.web.woven

子產品中有更強大的功能,該子產品為 Twisted Web 伺服器提供了模闆制作系統。總而言之,woven 提供了類似于 PHP、ColdFusion 或 JSP 這樣的程式設計風格,但是可以證明,它提供的代碼和模闆之間的部分比其他那些系統所提供的要有用得多(當然,

twisted.web.woven

允許用 Python 編寫您的程式)。在本系列的第 3 部分和第 4 部分中,我們還将解決動态頁面和 Web 安全性問題。

第 3 部分 有狀态 Web 伺服器和模闆化

與 Web 浏覽器互動

在本系列的 第 2 部分 中,我介紹了 Twisted 使用 .rpy 擴充名提供的動态 Web 頁面。但是 weblog 伺服器的這些初始版本隻能提供最低限度的動态。我使用了 HTML 标記來強迫頁面周期性地重新整理,并且每執行一次重新整理,都要進行一些計算,以确定相應的最近更新。但是沒有提到伺服器的使用者配置方面。

是以,本文将要介紹的第一件事就是,如何在我們上次看到過的同一基本動态頁面架構中配置使用者互動。但是在開始之前,我将為那些沒有閱讀本系列前兩個部分的讀者快速回顧一下如何啟動 Twisted Web 伺服器。

建立一個 “精簡的應用程式” 通常是最好的方法,并且這完全可以利用指令行選項來完成。但不是說 必須這樣做。隻要您願意,也可以在基本的 Web 伺服器中包含一些額外的功能(比如跨使用者和會話維護持久性資料),卻不必編寫任何自定義代碼。建立精簡的應用程式的方法類似于:

mktap web --path ~/twisted/www --port 8080

利用下面的指令啟動該應用程式:

twistd -f web.tap

就是這樣的。碰巧在 ~/twisted/www 基本目錄(或子目錄)中的任何 HTML 或 .rpy 檔案将為端口 8080 上的用戶端服務。實際上,您可以提供任何您喜歡的檔案類型,隻是 .rpy 檔案将被看作是特殊的動态腳本。

動态頁面 config_refresher.rpy 比本系列前一部分給出的任何頁面要稍微長一些,因為它在主體中包含了 HTML 模闆而不是導入模闆。我們首先來看設定代碼:

清單 1. 動态腳本 config _refresher.py (設定)

from twisted.web import resource, server
from persist import Records
from webloglib import log_fields, COLOR
from urllib import unquote_plus as uqp
fieldnames = """ip timestamp request status
                bytes referrer agent""".split()
field_dict = dict(zip(fieldnames, range(len(fieldnames))))
           

與我們在前面兩個部分所看到的一些導入不同,我将字段名稱映射到它們在

log_fields()

傳回的元組中的位置。還請注意自定義

persist

子產品的使用,該子產品将在 Twisted Web 伺服器的記憶體中儲存 weblog,是以不必在每次用戶端請求記錄時都讀取整個日記檔案。接下來介紹 HTML 模闆:

清單 2. config_refresher.py 腳本 (模闆)

TOP = '''<html><head><title>Weblog Refresher</title>
  <META HTTP-EQUIV="Refresh" CONTENT="30"/></head>
  <body>
  <table  width="100%%">
  <tr bgcolor="yellow">
  <form action="http://gnosis.cx:8080/config_refresher.rpy"
        method="GET">
    <td> IP  <input type="checkbox" name="ip" %s/> </td>
    <td> Timestamp <input type="checkbox" name="timestamp" %s/></td>
    <td> Request <input type="checkbox" name="request" %s/></td>
    <td> Status <input type="checkbox" name="status" %s/></td>
    <td> Bytes  <input type="checkbox" name="bytes" %s/></td>
    <td> Referrer <input type="checkbox" name="referrer" %s/></td>
    <td> Agent <input type="checkbox" name="agent" %s/></td>
    <td> <input type="submit" value="Change Fields"></td>
  </form>
  </td></tr>
  <table  cellspacing="0" width="100%%">'''
ROW = '<tr bgcolor=" %s">%s</tr>\n'
END = '</table></body></html>'
COLOR = ['white','lightgray']
END = '''</table></body></html>'''
           

設定 HTML 表單并不太神秘,本例的一個技巧是在 HTML中将那些已經檢查過的複選框中添加上字元串“checked”。

清單 3. config_refresher.py 腳本 (持久性)

records = registry.getComponent(Records)
if not records:
   records = Records()
   registry.setComponent(Records, records)
           

Twisted 系統資料庫像本系列前一部分描述的那樣工作。它就是儲存 Web 日記檔案中最新記錄的地方。最後,我們建立一個

Resource

,帶有一個相應的

.render()

方法——它完成真正的頁面建立:

清單 4. config_refresher.py 腳本 (呈現)

class Resource(resource.Resource):
    def render(self, request):
        showlist = []
        for field in request.args.keys():
            showlist.append(field_dict[field])
        showlist.sort()
        checked = [""] * len(fieldnames)
        for n in showlist:
            checked[n] = 'checked'
        request.write(TOP % tuple(checked))
        odd = 0
        for rec in records.getNew():
            hit = [field.strip('"') for field in log_fields(rec)]
            flds='\n'.join(['<td>%s</td>'%hit[n] for n in showlist])
            request.write(ROW % (COLOR[odd],
                                 uqp(flds).replace('&&',' &')))
            odd = not odd
        request.write(END)
        request.finish()
        return server.NOT_DONE_YET
resource = Resource()
           

Resource

中主要的新東西是對

request.args

屬性的通路。一般來說,該屬性類似于

cgi

子產品中的

FieldStorage

類——它收集與頁面請求一起傳遞的任何資訊,既包括 GET 資料,也包括 PUT 資料。Twisted的請求資料是所傳遞值的詞典;在我們的例子中,我們隻關心傳遞進來了哪些複選框的字段,以及未傳遞進哪些字段。如果我們想要檢查儲存在

request.args

中的一些值,則将遵循相同的模式。例如,您可能基于字段值将選項添加到過濾器(并選擇該過濾器帶有一個文本項或者一個 HTML 清單框)。

回頁首

利用 Woven 進行模闆化

我們到目前為止所介紹的動态頁面在概念上都類似于 CGI 方法。Twisted 異步伺服器比較快——它尤其節省時間,進而避免了為每個腳本請求打開一個新程序所帶來的開銷。但是

fastcgi

mod_python

獲得一個類似的加速。Twisted 在這一方面沒有什麼特殊的。

将 Web 應用開發上升到一個較高水準的方法之一就是使用 Woven。從概念上講,Woven 有些類似于 PHP、ASP (Active Server Pages)或 JSP (JavaServer Pages)。也就是說,Woven XHTML 頁面不僅僅是向浏覽器傳遞頁面,而且傳遞以程式設計方式填充的頁面的模闆或骨架。但是,對于代碼和 HTML 之間的分離,利用 Woven 比利用這些頁面嵌入技術要稍微複雜一些。您不是将 Python 代碼直接寫入到 Woven 模闆中,而是在一些普通的标記上定義一系列的自定義 XHTML 屬性,進而使外部代碼增強并處理頁面,然後再向浏覽器用戶端傳遞頁面。

model

屬性确定将用于擴充 XHTML 元素的資料。思路是,Model 表示應用程式的“業務邏輯”,即頁面的資料内容是如何确定的。而

view

屬性則确定所生成資料的特定表示。在 Woven 中還有 Controller 的概念,它是将節點(也就是 XHTML 元素)的 Model 和 View組合在一起的代碼。這一最後部分通常由一個

Page

對象來處理,該對象是一個可以被特殊化的類。

誠然,Woven 的術語有些難于了解,并且不幸的是,Twisted Matrix Web 站點的 HOWTO 文檔在闡述這些術語的時候,幾乎也是混淆使用。很難确切地闡述如何使用 Woven。我并不宣稱我自己完全了解 Woven 概念,但是 Twisted 的使用者 Alex Levy (請參見 參考資料,獲得他的頁面連結)幫助我開發了下面給出的例子。但是,您仍然可以利用 Woven 來做很多事情,是以是值得學習的。

開發 Woven 應用程式的第一步是建立一個或多個模闆檔案。這些模闆檔案就是具有特殊屬性的 XHTML 檔案,例如:

清單 5. WeblogViewer.xhtml 模闆

<html>
<head>
  <title>Weblog Viewer</title>
  <meta HTTP-EQUIV="Refresh" CONTENT="30" />
  <style type="text/css"><!--
    div.info {
      background-color: lightblue;
      padding: 2px dotted; }
    table th, table td {
      text-align: left;
      cellspacing: 0px;
      cellpadding: 0px; }
    table.log {
      border: 0px;
      width: 100%; }
    table.log tr.even { background-color: white; }
    table.log tr.odd  { background-color: lightgray; }
  --></style>
</head>
<body>
  <div class="info">
  You are displaying the contents of
  <code model="filename" view="Text">filename</code>.
  </div>
  <table  cellspacing="0" width="100%"
         class="log" model="entries" view="List">
    <tr bgcolor="yellow" pattern="listHeader">
      <th>Referrer</th><th/>
      <th>Resource</th>
    </tr>
    <tr pattern="listItem" view="alternateColor">
      <td model="referrer" view="Text">
          Referrer</td>
      <td>-></td>
      <td model="request_resource" view="Text">
          Resource</td>
    </tr>
    <tr pattern="emptyList">
      <td colspan="2">There is nothing to display.</td>
    </tr>
  </table>
</body>
</html>
           

Alex Levy 開發了這個模闆,并使用 CSS2 來控制元素的确切表示,顯示出的樣式比我的例子中的更好。很明顯,不管有沒有樣式表,頁面的基本布局都是相同的。

注意,配置設定給

<table>

元素的 View 是“List”,這個 View 與“Text”一樣是基本的 Woven View。另一方面, “alternateColor”是一個自定義的 View,我們定義在下面的代碼中。有些元素具有一個

pattern

屬性,控制 View 使用該屬性來定位比對的孩子。特别地,一個 List View 由一個可選的

listHeader

、一些

listItem

孩子(一個模闆标記,但是在生成時會擴充)和一個

emptyList

孩子(以免 Model 未定位到任何資料)組成。這些模式是 List View所使用的标準屬性;其他 Views 将利用其他模式來進行擴充。

這一版本的 weblog 伺服器的代碼建立了一個自定義的 Twisted 伺服器。不是基于用戶端的請求來更新,我們向伺服器的 Reactor 添加了一個對

update()

函數的重複回叫;這與本系列前一部分中的

tlogmaker.py

完全一緻。在開始研究自定義的 Page 資源之前,我們先來看看設定代碼:

清單 6. WeblogViewer.py 自定義 Twisted 伺服器

import webloglib as wll
import os, sys
from urllib import unquote_plus as uqp
from twisted.internet import reactor
from twisted.web import microdom
from twisted.web.woven import page, widgets
logfile = '../access-log'
LOG = open(logfile)
RECS = []
NUM_ROWS = 25
def update():
    global RECS
    RECS.extend(LOG.readlines())
    RECS = RECS[-NUM_ROWS*3:]
    reactor.callLater(5, update)
update()
           

有趣的東西在于我們對類

twisted.web.woven.page.Page

的自定義。我們所做的大部分事情都是不可思議的,因為您需要定義特别指定的屬性和方法。

清單 7. WeblogViewer.py Twisted 伺服器 (續)

class WeblogViewer(page.Page):
    """A Page used for viewing Apache access logs."""
    templateDirectory = '~/twisted/www'
    templateFile = "WeblogViewer.xhtml"
    # View factories and updates
    def wvupdate_alternateColor(self, request, node, data):
        """Makes our table rows alternate CSS classes"""
        # microdom.lmx is very handy; another example is located here:
        # http://twistedmatrix.com/documents/howto/picturepile#auto0
        tr = microdom.lmx(node)
        tr['class'] = ('odd','even')[data['_number']%2]
    # Model factories
    def wmfactory_filename(self, request):
        """Returns the filename of the log being examined."""
        return os.path.split(logfile)[1]
    def wmfactory_entries(self, request):
        """Return list of dict objects representing log entries"""
        entries = []
        for rec in RECS:
            hit = [field.strip('"') for field in wll.log_fields(rec)]
            if hit[wll.status] == '200' and hit[wll.referrer] != '-':
                # We add _number so our alternateColor view will work.
                d = {'_number': len(entries),
                     'ip': hit[wll.ip],
                     'timestamp': hit[wll.timestamp],
                     'request': hit[wll.request],
                     'request_resource': hit[wll.request].split()[1],
                     'status': hit[wll.status],
                     'bytes': hit[wll.bytes],
                     'referrer': uqp(hit[wll.referrer]).\
                                     replace('&&',' &'),
                     'agent': hit[wll.agent],
                    }
                entries.append(d)
        return entries[-NUM_ROWS:]
resource = WeblogViewer()
           

我們的自定義 Page 做了三類事情。第一類是設定模闆,以便與該資源一起使用。

第二類是使用字首為

wv

(Woven view)的神奇方法來定義一個自定義的 View。我們在自定義的 View 中真正所做的全部事情是将

class

屬性設定為 CSS 樣式表中的兩個值中的一個,以使交錯的行顯示不同的顔色。但是您可以使用一個類似于 DOM 的 API 來根據自己的喜好處理代碼。

第三類事情是有趣的。通過在 Model 本身的名稱前面加上

wmfactory_

字首,我們定義了兩個 Model。因為

filename

以 Text View 顯示,是以最好是傳回一個字元串。同樣,

entries

以 List View 顯示,是以應該将一列項作為傳回值。但是,XHTML 模闆中使用的

referrer

request_resource

這兩個 Model該如何呢?不用為這兩個模型定義自定義的方法。但是, 圍繞利用這些Model的節點的

listItem

模式有一個可用的詞典——

entries

詞典由

.wmfactory_entries()

傳回。而該詞典又包含

request_resource

referrer

的關鍵字;您不需要一個自定義的方法來支援 Model,隻要一個帶有必需關鍵字的詞典就行了。因為

referrer

節點的 View 是 Text,是以說詞典包含的值應該是字元串(如果不是這樣,Woven 将進行強制轉換)。

基于自定義的

WeblogViewer.py

資源建立一個自定義的伺服器與我們以前讨論過的一樣。建立一個伺服器,然後再啟動它:

% mktap web --resource-script=WeblogViewer.py --port 8080 % twistd -f web.tap

回頁首

在最後一部分中

這篇介紹隻涉及了 Woven 的一些皮毛。該軟體包中還有許多複雜的功能,我希望自己給出的例子能對模闆化系統起到抛磚引玉的作用。

下一次,在關于 Twisted 這一系列的最後一部分中,我将介紹一些零碎的東西,包括對安全性的一個簡要概述。我們還将介紹 Twisted 軟體包中包含的一些特殊協定和伺服器。

第 4 部分 保護客戶機與伺服器

在第1、2和3部分中,伺服器與客戶機具有的共同之處在于它們的操作完全是在一個經過編碼的明文會話中進行的。不過在有些時候,您還會希望您的連接配接能夠避開窺視者的眼睛(或者免遭竄改與欺騙)。

用于決定是否允許通路伺服器資源的協定是很有意思的,但是在這一部分中,我還是想考慮一下與真正的連接配接級加密有關的協定。不過在一般的應用背景中,您也許可以研究一下面向Web的機制,例如 RFC-2617 中描述的 Basic Authentication,它在 Apache 和其他的 Web 伺服器中都實作了。Twisted 包 twisted.cred 是一種通用且複雜的架構,它在用于一般目的的 Twisted 伺服器中提供認證伺服器,而并不局限于 Web 伺服器。

在 Internet 上進行連接配接級的加密,有兩種廣泛應用的 API:SSL 和 SSH。前面一種,SSL(Secure Sockets Layer,安全套接字層),在 Web 浏覽器和 Web 伺服器中廣泛實作;不過從原理上說,SSL并沒有理由非得結合到 HTTP 協定上不可。SSL 結合了一種公鑰基礎設施,連同一個基于 Certificate Authorities 的“可信 Web”(“web-of-trust”)。SSL 會建立一個會話密鑰,在某個特定連接配接的整個生命期中,SSL 都用這個密鑰對其進行标準的對稱加密。

Twisted 中的确包括了 SSL 架構;不過就和 Twisted 中的大多數東西一樣,沒有詳細的文檔來說明 SSL 是如何工作的。我試着下載下傳了兩個可能支援的包,嘗試讓 Twisted v.1.0.6 的腳本 test_ssl.py 運作起來(請看 參考資料),不過在撰寫本文的時候還沒有成功。

另一個廣泛應用于連接配接級加密的 API 是 SSH(Secure Shell,安全Shell),它因與其同名的那個工具(小寫的 ssh )而出名。SSL 與 SSH 共享了很多底層加密算法,不過 SSH 着重于建立加密的 shell 連接配接(用以取代那些容易受到窺探的程式/協定,比如 telnet 和 rsh)。Twisted 使您能夠編寫定制的 SSH 客戶機和伺服器,這可是件非常好的事情。您不僅可以編寫一個基本的互動式遠端 shell,就好像 ssh 和 sshd 提供的客戶機與伺服器一樣,而且您還可以建立專用性更強的工具,讓更高層的應用能夠利用這些安全連接配接。

SSH Weblog 客戶機

下面接着讨論本系列文章的例子,我建立了一個工具,用來檢查我的 Web 伺服器 log 檔案中的點選率,不過這次是在一條加密的 SSH 通道上完成的。這個目标實際上也是現實的 -- 也許我不希望監聽我的資料包流的人公然看到我的點選率。

Twisted 包自身并沒有相應的支援子產品,顯然也沒有确切的文檔來說明其原理。在深入工作之前,我需要弄清楚 twisted.conch 包中的

import Crypto

那一行究竟有什麼用處。它的名字顯然是一種提示,不過我對由 Andrew Kuchling 維護的 Python 加密庫也有一定程度的了解(請看 參考資料中的連結)。搜尋一下Google,下載下傳,然後安裝,Twisted 的 test_conch.py 就很順利地運作起來了。這樣就可以開始建立定制的 SSH 客戶機了。

我是基于 Twisted 檔案 doc/examples/sshsimpleclient.py 中提供的例子建立客戶機的;您也許還想看看那個例子中還有些什麼。twisted.conch 像大多數 Twisted 元件一樣,包含若幹層,其中的每一層都是可以定制的。我猜想“conch”這個名字在安全 Shell 的世界中代替了“shell”的角色。

傳輸層是一個定制的

SSHClientTransport

。我們可以定義若幹個方法,不過至少需要定義

.verifyHostKey()

.connectionSecure()

。在實作時,我們完全信任所有的主機密鑰,隻是通過傳回一個

defer.succeed

對象,将控制交回給異步反應器(reactor)的核心。當然了,如果您打算根據已知密鑰驗證某台主機,您可以在

.verifyHostKey()

中實作。

建立通道的過程也就是其他幾層加入進來的時候。

SSHUserAuthClient

的子類完成實際的登入認證工作;如果認證成功,它就建立起一條連接配接(我将連接配接定義為

SSHConnection

的子類)。這條連接配接緊接着建立一個通道——即

SSHChannel

的子類。我将這條通道簡單命名為

Channel

,實際的定制工作正是通過它來完成的。明确地說就是,這條通道實作了資料和指令的發送與接受。下面讓我們來看看我定制的客戶機:

清單1. ssh-weblog.py

#!/usr/bin/env python
"""Monitor a remote weblog over SSH
  USAGE: ssh-weblog.py [email protected] logfile
"""
from twisted.conch.ssh import transport, userauth, connection, channel
from twisted.conch.ssh.common import NS
from twisted.internet import defer, protocol, reactor
from twisted.python import log
from getpass import getpass
import struct, sys, os
import webloglib as wll
USER,HOST,CMD = None,None,None
class Transport(transport.SSHClientTransport):
    def verifyHostKey(self, hostKey, fingerprint):
        print 'host key fingerprint: %s' % fingerprint
        return defer.succeed(1)
    def connectionSecure(self):
        self.requestService(UserAuth(USER, Connection()))
class UserAuth(userauth.SSHUserAuthClient):
    def getPassword(self):
        return defer.succeed(getpass("password: "))
    def getPublicKey(self):
        return  # Empty implementation: always use password auth
class Connection(connection.SSHConnection):
    def serviceStarted(self):
        self.openChannel(Channel(2**16, 2**15, self))
class Channel(channel.SSHChannel):
    name = 'session'    # must use this exact string
    def openFailed(self, reason):
            print '"%s" failed: %s' % (CMD,reason)
    def channelOpen(self, data):
        self.welcome = data   # Might display/process welcome screen
        d = self.conn.sendRequest(self,'exec',NS(CMD),wantReply=1)
    def dataReceived(self, data):
        recs = data.strip().split('\n')
        for rec in recs:
            hit = [field.strip('"') for field in wll.log_fields(rec)]
            resource = hit[wll.request].split()[1]
            referrer = hit[wll.referrer]
            if resource=='/kill-weblog-monitor':
                print "Bye bye..."
                self.closed()
                return
            elif hit[wll.status]=='200' and hit[wll.referrer]!='-':
                print referrer, ' -->', resource
    def closed(self):
        self.loseConnection()
        reactor.stop()
if __name__=='__main__':
    if len(sys.argv) < 3:
        sys.stderr.write('__doc__')
        sys.exit()
    USER, HOST = sys.argv[1].split('@')
    CMD = 'tail -f -n 1 '+sys.argv[2]
    protocol.ClientCreator(reactor, Transport).connectTCP(HOST, 22)
    reactor.run()
    
           

這個客戶機的整體結構與我們已經見過的大多數 Twisted 應用程式類似。它首先建立協定,然後在一個異步循環(換句話說就是在

reactor.run()

中)中監視是否有事件發生。

有趣的部分出現在

Channel()

的方法中。通道一旦打開,我們就執行一條定制的指令 -- 本例中是 Weblog 檔案中的一條

tail -f

指令,其名稱在指令行中指定。這時主機依舊完全是一個一般的 sshd 伺服器,而不具有任何 Twisted 特征,它自然而然地會開始發送回一些資料。資料一旦到達,

dataReceived()

方法就會對其進行解析(随着

tail

産生更多資料,這個過程也在不斷進行)。對于這個特定的客戶機而言,我們根據解析出來的 Weblog 的實際内容來決定何時結束 -- 這就相當于一種基于 Web 殺死監視程式的方法。雖然那種特定的配置可能并不常見,但是這個例子還是能夠說明如何在某種條件(可以是任何條件)成立的情況下切斷連接配接的基本概念。會話過程如下:

清單2. Weblog 螢幕會話執行個體

$ ./ssh-weblog.py [email protected] access-log
host key fingerprint: 56:54:76:b6:92:68:85:bb:61:d0:f0:0e:3d:91:ce:34
password:
http://gnosis.cx/publish/  --> /publish/whatsnew.html
http://gnosis.cx/publish/whatsnew.html  --> /home/hugo.gif
Bye bye...
           

這與本系列文章中建立的其他 Weblog 螢幕幾乎完全一樣。當從另一個視窗中讓浏覽器轉向 <http://gnosis.cx/kill-weblog-monitor> 的時候,上面的會話就結束了(否則,它就會無限期的監視下去)。

回頁首

修改SSH客戶機

如果是出于其他的目的而建立另外的SSH客戶機,也是一件很簡單的事情。例如,我可以将 ssh-weblog.py 拷貝為 scp.py,并隻對代碼進行一些修改。

_main_

主體中解析選項的方式就有些不同,docstring 也進行了調整;此外,我還簡單地修改了一下

.dataReceived()

方法,讓它來讀取資料:

清單3. scp.py (修改過的 Channel 方法)

def dataReceived(self, data):
    open(DST,'wb').write(data)
    self.closed()
    
           

(變量 CMD 的值設定為

"cat "+sys.argv[2]

。)

哈哈!我已經實作了很多 SSH 客戶機的 scp 工具。

這些例子都是“運作并收集”類型的工具。也就是說,它們在會話期間并沒有互動。不過你可以很簡單地建立另一個工具,另外在

Channel

方法中調用

self.conn.sendRequest()

。實際上如果客戶機是某種 GUI 客戶機,您就可以加入一些資料收集的表單,作為反應器中的回調。換句話說,當某個特定的表單結束之時,也許新的遠端指令會發送過來,這時收集結果、然後處理或顯示的工作就會再次開始。

回頁首

SSH Weblog 伺服器

SSH 伺服器使用的結構與客戶機大緻相同。與前面一樣,我對 doc/examples/sshsimpleserver.py 進行了簡化與定制,以适應我的例子。其中一種處理是:伺服器最好由用适當的密鑰和類配置過的的

SSHFactory

子類建立:

在我們的 SSH Weblog 伺服器中,我們為一名經過授權的使用者配置了密碼和使用者名。在本例中,這些配置是寫死的,但是您顯然可以将其存儲在其他的地方;比方說可以配置一個授權 Weblog 螢幕清單。下面讓我們來看看這個例子:

清單4. ssh-weblog-server.py

#!/usr/bin/env python2.3
from twisted.cred import authorizer
from twisted.conch import identity, error
from twisted.conch.ssh import userauth, connection, channel, keys
from twisted.conch.ssh.factory import SSHFactory
from twisted.internet import reactor, protocol, defer
import time
class Identity(identity.ConchIdentity):
    def validatePublicKey(self, data):
        return defer.succeed('')
    def verifyPlainPassword(self, password):
        if password=='password' and self.name == 'user':
            return defer.succeed('')
        return defer.fail(error.ConchError('bad password'))
class Authorizer(authorizer.Authorizer):
    def getIdentityRequest(self, name):
        return defer.succeed(Identity(name, self))
class Connection(connection.SSHConnection):
    def gotGlobalRequest(self, *args):
        return 0
    def getChannel(self, channelType, windowSize, maxPacket, data):
        if channelType == 'session':
            return Channel(remoteWindow=windowSize,
                      remoteMaxPacket=maxPacket, conn=self)
        return 0
class Channel(channel.SSHChannel):
    def channelOpen(self, data):
        weblog = open('../access.log')
        weblog.readlines()
        while 1:
            time.sleep(5)
            for rec in weblog.readlines():
                self.write(rec)
    def request_pty_req(self, data):
        return 1    # ignore, but this gets send for shell requests
    def request_shell(self, data):
        self.client = protocol.Protocol()
        self.client.makeConnection(self)
        self.dataReceived = self.client.dataReceived
        return 1
    def loseConnection(self):
        self.client.connectionLost()
        channel.SSHChannel.loseConnection(self)
class Factory(SSHFactory):
    publicKeys = {'ssh-rsa':keys.getPublicKeyString(
                            data=open('~/.ssh/id_rsa.pub').read())}
    privateKeys ={'ssh-rsa':keys.getPrivateKeyObject(
                            data=open('~/.ssh/id_rsa').read())}
    services = {'ssh-userauth': userauth.SSHUserAuthServer,
                'ssh-connection': Connection}
    authorizer = Authorizer()
reactor.listenTCP(8022, Factory())
reactor.run()
           

簡而言之,我們忽略了解析與格式化 Weblog 記錄的操作,不過這種一有新記錄就向一個打開的通道寫入的思想與客戶機的思想方式幾乎是相同的。當然了,在這種情況下,任何一般的 SSH 客戶機都可以連接配接到這個專用的伺服器。

清單5. Weblog 螢幕的示例會話

$ ssh gnosis.python-hosting.com -p 8022 -l user
[email protected]'s password:
141.154.146.89 - - [26/Aug/2003:02:47:40 -0500]
"GET /voting-project/August.2003/0010.html HTTP/1.1" 200 8986
"http://gnosis.python-hosting.com/voting-project/August.2003/0009.html"
"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/85
(KHTML, like Gecko) Safari/85"
[...]
           

就像客戶機的實作方法一樣,您可以在增強版本中加入更多的互動性;還可以定制通道的

.dataReceived()

方法,當資料從(一般的)客戶機傳來時,就可以作更多有用的工作。

回頁首

社會問題

很不幸的是,我在推薦 Twisted 架構的時候,最大的保留就是開發團隊之間的那種“狂野西部(wild west)”的感覺。這個軟體本身是非常強大的。但是即使在大多數開放源代碼項目中,不同釋出版本之間也缺乏足夠的 API 一緻性,而且文檔都很粗略。最好的方法是從郵件清單中尋求幫助;您或許可以獲得有用的答複。

正如寫作本文的目的一樣,我是在試圖填補示例與文檔以外的空白,Twisted 确實也在堅持建立能夠提供幫助的社群。我真心希望一段時間之後,文檔和郵件清單的品質都能夠得到改進;特别是那些在犄角旮旯裡藏着的 Twisted 架構工具,有些還真是令人印象深刻。

附加:

Twisted  官網:http://twistedmatrix.com/trac/

文章來源:

http://www.ibm.com/developerworks/cn/linux/network/l-twist/part1/

http://www.ibm.com/developerworks/cn/linux/network/l-twist/part2/

http://www.ibm.com/developerworks/cn/linux/network/l-twist/part3/

http://www.ibm.com/developerworks/cn/linux/network/l-twist/part4/