Twisted 网络编程 (II)

使用 Twisted 框架进行网络编程,第 2 部分实现 Web 服务器




文档选项



未显示需要 JavaScript
的文档选项


[url=javascript:document.email.submit();]将此页作为电子邮件发送[/url]
级别: 初级
David Mertz
(
[email=mertz@gnosis.cx?subject=%E4%BD%BF%E7%94%A8%20Twisted%20%E6%A1%86%E6%9E%B6%E8%BF%9B%E8%A1%8C%E7%BD%91%E7%BB%9C%E7%BC%96%E7%A8%8B%EF%BC%8C%E7%AC%AC%202%20%E9%83%A8%E5%88%86&cc=]mertz@gnosis.cx[/email]
), 程序员,博士, Gnosis Software, Inc.
2003 年  9 月  14 日
在这个关于 Twisted 系列的第一篇文章中,David 介绍了异步服务器编程。虽然正如 David 在本文中所说的,从某种角度而言 Web 服务器是另一种网络服务,但 Twisted 提供了许多用于编写 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 刷新器脚本
      
      
      

          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 = '''Dynamic Page
  Dynamic Page served by Twisted Matrix
'''
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 安全性问题。