使用 Twisted 框架进行网络编程,第 3 部分

有状态 Web 服务器和模板化

Comments

与 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 软件包中包含的一些特殊协议和服务器。


相关主题

  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文.
  • Twisted 附带有大量的文档和例子。浏览 Twisted Matrix 主页,以进一步了解 Twisted 是如何工作的,以及利用 Twisted 实现了什么。
  • 您也可以在 Twisted Matrix Web 站点阅读 Woven templating framework
  • 本系列的第 1 部分介绍了利用 Twisted 框架进行网络编程和异步联网( developerWorks, 2003年7月)。
  • 本系列的第 2 部分中,我们设置了一个 Web 服务器,并实现了基本的服务( developerWorks, 2003年6月)。
  • developerWorks 技巧“ 异步 SAX”中给出了 weblog 服务器的一个简单版本 ( developerWorks, 2003年5月)。
  • 在 David 的 Web 站点,您可以下载 Webloglib 模块以及与该 Twisted 系列有关的其他东西。
  • developerWorks Linux 专区找到 针对 Python 开发人员的更多文章

评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Linux
ArticleID=49552
ArticleTitle=使用 Twisted 框架进行网络编程,第 3 部分
publish-date=09142003