现在的位置: 首页 > 综合 > 正文

[转]使用 Twisted Matrix 框架来进行网络编程

2013年01月08日 ⁄ 综合 ⁄ 共 25866字 ⁄ 字号 评论关闭

作者:David Mertz (mertz@gnosis.cx), 程序员,博士, 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 中的)子类包括 dnsftpgnutellahttpnntpshoutcast 以及其他许多协议。协议基本上应该知道如何处理连接的建立和断开,以及如何在连接中接收和发送数据。这些职责与基于 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 之外的其他类型的服务器,您也可以使用其他名称来代替 Webdnsconchnewstelnetimmanhole 等。这些名称中有些是常见的服务器,其他则特定于 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.pytlogmaker.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 border="1" 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 border="0" 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 异步服务器比较快——它尤其节省时间,从而避免了为每个脚本请求打开一个新进程所带来的开销。但是 fastcgimod_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 border="0" 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 模板中使用的 referrerrequest_resource 这两个 Model该如何呢?不用为这两个模型定义自定义的方法。但是, 围绕利用这些Model的节点的 listItem 模式有一个可用的词典—— entries 词典由 .wmfactory_entries() 返回。而该词典又包含 request_resourcereferrer 的关键字;您不需要一个自定义的方法来支持 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 user@host 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 gnosis@gnosis.cx 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 客户机,您就可以加入一些数据收集的表单,作为反应器中的回调。换句话说,当某个特定的表单结束之时,也许新的远程命令会发送过来,这时收集结果、然后处理或显示的工作就会再次开始。


【上篇】
【下篇】

抱歉!评论已关闭.