作者: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('')
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
|
读者应该参考我先前的一篇技巧文章,以了解客户机应用程序的详细信息。但是必须注意下面的更改。主客户机循环增加了两行:
清单 3. 增强的(阻塞)客户机循环
|
回页首
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
|
在这个示例中,我们定义了一个定制工厂,并将一些初始化代码从 _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
|
您可以看到,我们所做的所有工作主要是用 application
替换 reactor
。虽然类 Application
也有一个 .run()
方法,但我们仍然使用其 .save()
方法来创建一个 Fibonacci.tap
文件。运行该服务器的操作如下所示:
清单 6. 运行 fib_server.py
|
连接到该服务器的客户机如果只是间歇地需要新数字,而不需要尽快地得到新数字的话,那么它应该在其循环中使用 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) 将 标记放在 HTML 头中,使显示保持最新;(2) 一旦发生新的访问就间歇地重写 Weblog.html 文件本身。第二个任务只需要一个一直运行的后台进程,例如:
清单 1. logmaker.py Weblog 刷新器脚本
|
模块 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 页面
|
文件级变量 resource
很特殊 - 它需要指向 twisted.web.resource.Resource
子类的实例,该类定义了 .render()
方法。您在所处理的目录中想包括多少动态页面就可以包括多少,并且可以自动处理每个页面。
回页首
使用 Twisted 来更新静态页面
在我的第一篇 Twisted 文章中所提出的定时回调技术可以用来定期更新上面所讨论的 Weblog.html 文件。也就是说,您可以用非阻塞 twisted.internet.reactor.callLater()
调用来替换 logmaker.py
中的 time.sleep()
调用:
清单 3. tlogmaker.py Weblog 刷新器脚本
|
logmaker.py
和 tlogmaker.py
的差别不大 - 两者都可以在后台启动并且都可以让它们一直运行以更新页面 referesher.html
。更有趣的是可以将 tlogmaker.py
目录构建到 Twisted 服务器中,而不是仅让它在后台进程中运行。这非常简单,我们只需要在该脚本结尾处再添加两行:
from twisted.web import static
resource = static.File("~/twisted/www")
还可以除去对 twisted.internet.reactor.run()
的调用。通过这些更改,使用下面两行脚本创建服务器:
|
然后像前面那样使用 twistd
来运行已创建的 web.tap
服务器。现在 Web 服务器自己可以使用其标准核心分派循环每五秒钟刷新一下页面 Weblog.html。
回页首
使 Weblog 变成动态页面
处理 Web 日志的另一种方法是每次收到请求时使用动态页面来生成最新访问量。但是,每次接收到这样的一个请求就读取整个 access-log
文件并不是个好主意 - 忙碌的网站在日志文件中可能有几千条记录,反复读取这些记录非常耗时间。更好的办法是让 Twisted 服务器自己拥有一个针对日志文件的文件句柄,只在需要时才读取 新记录。
在某种程度上,让服务器维护文件句柄正是 tlogmaker.py
所做的工作,但是它将最新的记录存储在文件而不是存储在内存中。但是,这种方法强迫我们围绕该持久性功能编写整个服务器。让各个动态页面分别向服务器发出自己的持久性请求会更加好。例如,通过这种方法您可以添加新的有状态动态页面,而不必停止或改变长期运行的(和通用的)服务器。页面分配的持久性的关键是 Twisted 的 注册表。例如,下面是一个处理 Weblog 的动态页面:
清单 4. www/Weblog.rpy 动态 Weblog 页面
|
一开始会对注册表产生的疑惑是 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
|
您可以很自由地在持久性类中放置您喜欢的任何方法 - 注册表只是在各次对动态页面的调用之间将实例保存在内存中。
回页首
下一次
在本文中,我们研究了 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 (设置)
|
与我们在前面两个部分所看到的一些导入不同,我将字段名称映射到它们在 log_fields()
返回的元组中的位置。还请注意自定义 persist
模块的使用,该模块将在 Twisted Web 服务器的内存中保存 weblog,所以不必在每次客户端请求记录时都读取整个日记文件。接下来介绍 HTML 模板:
清单 2. config_refresher.py 脚本 (模板)
|