[《Twisted网络编程必备》翻译] 第4章

4.0 WEB服务器

即使是很保守的说,现在的很多软件是基于WEB开发的。人们将大量时间花费在WEB浏览器上面,包括阅读HTML页面、电子邮件、管理日志、进入数据库的记录、更新Wiki页面和写weblog。
即使你不打算写严格的WEB应用,WEB界面也更加容易提供适合于跨平台的UI。在你的应用中包含轻量级的WEB服务器将会提供更多的附属功能。这一章将会展示如何使用Twisted开发一个WEB服务器,并介绍你一些构建WEB应用的方法。当然还提供了HTTP代理服务器。
这一章提供了一些HTTP协议的常识。这些一些构建WEB服务器所必须的知识。实际上,本书所涉及的HTTP知识还远远不够,还需要如《HTTP: The Definitive Guide》等等的书,还有不可替代的HTTP的RFC文档RFC2616(http://www.faqs.org/rfcs/rfc2616.html)。





4.1 响应HTTP请求

HTTP是一个简单的协议接口。客户端发送请求,服务器端发送响应,然后关闭连接。你可以自己实现一个HTTP的Protocol来练习接收连接,读取请求,并发送HTTP格式的响应。

4.1.1 下面如何做?

每个HTTP请求开始于单行的HTTP方法,紧接着是URL,然后是HTTP版本。随后是一些行的头字段。一个空行(by gashero)标志着头字段的结束。头字段后面就是请求主体,例如提交的HTML表单数据。
如下是一个HTTP请求的例子。这个请求询问服务器通过GET方法获取www.example.com/index.html资源,并使用HTTP版本1.1。
GET /index.html HTTP/1.1
Host: www.example.com
服务器响应的第一行告知客户端响应的HTTP版本和状态码。有如请求一样,响应包含了头字段,并用空行隔开消息主体。如下是HTTP响应的例子:
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 17
Connection: Close

Hello HTTP world!
设置简单的HTTP服务器,需要写一个Protocol允许客户端连接。查找空行标志着头字段的结束。然后发送HTTP响应,例子4-1展示了简单的HTTP实现。
from twisted.protocols import basic
from twisted.internet import protocol,reactor
class HttpEchoProtocol(basic,LineReceiver):
    def __init__(self):
        self.lines=[]
        self.gotRequest=False
    def lineReceived(self,line):
        self.lines.append(line)
        if not line and not self.gotRequest:
            self.sendResponse()
            self.gotRequest=True
    def sendResponse(self):
        responseBody="You said: /r/n/r/n"+"/r/n".join(self.lines)
        self.sendLine("HTTP/1.0 200 OK")
        self.sendLine("Content-Type: text/plain")
        self.sendLine("Content-Length: %i"%len(responseBody))
        self.sendLine("")
        self.transport.write(responseBody)
        self.transport.loseConnection()
f=protocol.ServerFactory()
f.protocol=HttpEchoProtocol
reactor.listenTCP(8000,f)
reactor.run()
运行webecho.py脚本启动服务器。你可以看到服务器运行在http://localhost:8000。你可以获得请求的回显,即原请求的报文。

4.1.2 它们如何工作?

HTTPEchoProtocol懂得如何响应每一个请求。从客户端收到的数据将会存储在self.lines中。当看到一个空行时,可以知道头字段结束了。它发送回一个HTTP响应。第一行包含了HTTP版本和状态码,在这种情况下,200是成功("OK"是附加的便于阅读的状态码描述)。下一对行是Content-Type和Content-Length头字段,用于告知客户端内容格式和长度。HTTPEchoProtocol发送一个空行来结束头字段,然后发送响应主体,回显客户端请求报文。






4.2 解析HTTP请求

HTTPEchoProtocol类提供了有趣的HTTP入门,但是距离使用功能还很遥远。它并不去分析请求头和资源位置,HTTP(by gashero)方法也只支持一种。如果想要建立一个真正的WEB服务器,你可以用一种更好的方式来解析和响应请求。下面的实现展示这些。

4.2.1 下面如何做?

写twisted.web.http.Request的子类并重载process方法来处理当前请求。Request对象已经包含了process需要调用的HTTP请求所有信息,所以只需要决定如何响应。例子4-2展示了如何运行基于http.Request的HTTP服务器。
from twisted.web import http
class MyRequestHandler(http.Request):
    pages={
        '/':'<h1>Home</h1>Home Page',
        '/test':'<h1>Test</h1>Test Page',
        }
    def process(self):
        if self.pages.has_key(self.path):
            self.write(self.pages[self.path])
        else:
            self.setResponseCode(http.NOT_FOUND)
            self.write("<h1>Not Found</h1>Sorry, no such page.")
        self.finish()
class MyHttp(http.HTTPChannel):
    requestFactory=MyRequestHandler
class MyHttpFactory(http.HTTPFactory):
    protocol=MyHttp
if __name__=='__main__':
    from twisted.internet import reactor
    reactor.listenTCP(8000,MyHttpFactory())
    reactor.run() #by gashero
运行requesthandler.py将会在8000端口启动一个WEB服务器。可以同时阅览主页(http://localhost:8000/)和测试页/test(http://localhost:8000/test)。如果你指定了想要访问其他页面,将会得到错误信息。

4.2.2 它们如何工作?

http.Request类解析了进入的HTTP请求并提供了制造响应的接口。在例子4-2中,MyRequestHandler是http.Request的一个子类提供了process方法。process方法将会被请求调用并接收。有责任在产生一个响应之后调用self.finish()来指出响应完成了。MyRequestHandler使用path属性来寻找请求路径。并在pages字典中指定匹配路径。如果匹配成功,MyRequestHandler使用write方法发送响应文本到响应。
注意write仅在写入响应的部分实体主体时才使用,而不是在生成原始HTTP响应时。setResponseCode方法可以用于改变HTTP状态码。twisted.web.http模块提供了所有HTTP状态码的定义,所以可用http.NOT_FOUND来代替404错误。
Tip:Request.setResponseCode带有一个可选的第二个参数,一个易读的状态消息。你可以感觉到很方便于twisted.web.http模块包含了内置列表描述普通的状态码,也就是缺省使用的。
Request类也提供setHeader方法来添加响应头字段。MyRequestHandler使用setHeader来设置Content-Type头为text/html,这个设置告诉浏览器对响应主体使用HTML格式。
twisted.web.http模块提供了两种附加类允许将Request的子类转换成功能性的WEB服务器。HTTPChannel类是一个Protocol,可以创建Request对象来应付每个连接。创建HTTPChannel时使用你的Request子类,重载requestFactory类属性。HTTPFactory是一个ServerFactory,添加附加特性,包含日志方法加上Request对象,这种日志格式包括Apache和其他多种日志格式。






4.3 处理POST数据和HTML表单

前面的实验展示了通过客户端请求生成静态HTML。这个实验展示了允许书写代码控制响应的生成,并且处理HTML表单提交的数据。

4.3.1 下面如何做?

写一个函数来处理Request对象并产生响应。设置字典来映射每一个可用路径到WEB站点来让函数出路路径请求。使用Request.args字典存取提交的HTML表单数据。例子4-3展示了生成单页HTML表单的WEB服务器,另一个页面是通过表单数据显示的。
from twisted.web import http
def renderHomePage(request):
    colors='red','blue','green'
    flavors='vanilla','chocolate','strawberry','coffee'
    request.write("""
<html>
<head>
    <title>Form Test</title>
</head>
<body>
    <form action="posthandler" method="POST">
        Your Name:
        <p>
            <input type="text" name="name">
        </p>
        What's your favorite color?
        <p>
""")
    for color in colors:
        request.write(
            "<input type='radio' name='color' value='%s'>%s<br/>"%(
            color,color.capitalize()))
    request.write("""
        </p>
        What kinds of ice cream do you like?
        <p>
        """)
    for flavor in flavors:
        request.write(
            "<input type='checkbox' name='flavor' value='%s'>%s<br/>"%(
            flavor,flavor.capitalize()))
    request.write("""
        </p>
        <input type='submit'/>
    </form>
</body>
</html>
""")
    request.finish()
def handlePost(request):
    request.write("""
    <html><head><title>Posted Form Datagg</title>
        </head>
        <body>
        <h1>Form Data</h1>
    """)
    for key,values in request.args.items():
        request.write("<h2>%s</h2>"%key)
        request.write("<ul>")
        for value in values:
            request.write("<li>%s</li>"%value)
        request.write("</ul>")
    request.write("""
        </body></html>
    """)
    request.finish()
class FunctionHandleRequest(http.Request):
    pageHandlers={
        '/':renderHomePage,
        '/posthandler':handlePost,
    }
    def process(self):
        self.setHeader("Content-Type","text/html")
        if self.pageHandlers.has_key(self.path):
            handler=self.pageHandlers[self.path]
            handler(self)
        else:
            self.setResponseCode(http.NOT_FOUND)
            self.write("<h1>Not Found</h1>Sorry, no such page.")
            self.finish()
class MyHttp(http.HTTPChannel):
    requestFactory=FunctionHandledRequest
class MyHttpFactory(http.HTTPFactory):
    protocol=MyHttp
if __name__=='__main__':
    from twisted.internet import reactor
    reactor.listenTCP(8000,MyHttpFactory())
    reactor.run()
运行formhandler.py脚本。将会在8000端口运行WEB服务器。进入http://localhost:8000可以找到表单主页。按照如下填写一些字段信息。
然后点击提交按钮,你的浏览器将会发送表单数据到页面formhandler使用HTTP的POST请求。当它接受到表单数据时,formhandler回展示提交过的字段和值。

4.3.2 它们是如何工作的?

例子4-3定义了两个函数来处理请求,renderHomePage和handlePost。FunctionHandleRequest是Request的子类,其属性pageHandler定义了路径映射功能。process方法查找路径,并尝试在pageHandlers中匹配路径。如果匹配成功,则FunctionHandleRequest传递自身到匹配函数,并且由对方负责处理;如果匹配失败,则返回404 Not Found响应。
renderHomePage函数设置处理器到/,站点的根路径。它生成的HTML表单将会提交数据到页面/formhandler。这个处理器函数/formhandler是handlePost,将会响应页面列表并提交数据。handlePost遍历值Request.args,这个字典属性包含了请求提交的所有数据。
Tip:在这种情况下,发送的表单数据在HTTP POST请求的主体中。当请求发送HTTP GET时,Request.args将会包含所有提交的URI查询字段值。你可以修改这个行为,通过改变表单生成器renderHomePage的method属性,从POST到GET,重启服务器,就可以重新提交表单。
一个HTML表单可以有多个字段具有相同的名字。例如,表单4-3中允许选中多个复选框,所有的名字都是flavor。不像很多其他的框架,http.Request并不对你隐藏什么:代替了映射字段名到字符串,Request.args映射每个字段值到列表。如果你知道要取哪一个值,那么可以只获取列表的第一个值。






4.4 管理资源等级

WEB应用中的路径通常使用分级目录管理。例如如下URL:
http://example.com/people
http://example.com/people/charles
http://example.com/people/charles/contact
这里可以很清楚的看出等级划分。页面/people/charles是/people的子页面,而页面/people/charles/contact是/people/charles的子页面。等级中的每个页面都有特定的意义:/people/charles是一个人,而/people/charles/contact是一项数据,charles的数据。
WEB服务器的缺省行为是将PATH等级映射到磁盘上的文件。客户端的每次请求都是对应特定的资源,服务器查找文件并定位磁盘路径,然后将文件内容或者可执行文件的执行结果作为响应。在WEB应用中可以人为的生成一个对应路径文件的内容。例如,你的数据并没有存储在磁盘上,而是在一个关系数据库或者另一个服务器上。或者你想要在请求时自动创建资源。类似这种情况,最好的办法是为等级浏览创建自己的逻辑。
编写自己的资源管理逻辑可以帮助你管理安全。而不是打开WEB服务器的整个目录,你可以有选择的控制哪些文件是可以访问的。

4.4.1 下面如何做?

twisted.web.resource和twisted.web.static还有twisted.web.server模块提供了比twisted.web.http.Resource更高层次的请求管理类,你可以使用这些来设置一个WEB服务器来处理多种逻辑等级的资源。例子4-4使用了这些类来建立十六进制颜色代码。请求资源/color/hex,hex是十六进制的颜色代码,你可以得到一个背景为#hex的页面。对应每一种可能出现的颜色可能,服务器动态创建资源。
from twisted.web import resource,static,server
class ColorPage(resource.Resource):
    def __init__(self,color):
        self.color=color
    def render(self,request):
        return """
        <html>
        <head>
            <title>Color: %s</title>
            <link type='text/css' href='/styles.css' rel='Stylesheet' />
        </head>
        <body style='background-color: #%s'>
            <h1>This is #%s.</h1>
            <p style='background-color: white'>
            <a href='/color/'>Back</a>
            </p>
        </body>
        </html>
        """ % (self.color, self.color, self.color)
class ColorRoot(resource.Resource):
    def __init__(self):
        resource.Resource.__init__(self)
        self.requestedColors=[]
        self.putChild('',ColorIndexPage(self.requestColors))
    def render(self,request):
        # redirect /color -> /color/
        request.redirect(request.path+'/')
        return 'please use /colors/ instead.'
    def getChild(self,path,request):
        if path not in self.requestedColors:
            self.requestedColors.append(path)
        return ColorPage(path)
class ColorIndexPage(resource.Resource):
    def __init__(self,requestColorsList):
        resource.Resource.__init__(self)
        self.requestedColors=requestedColorsList
    def render(self,request):
        request.write("""
        <html>
        <head>
          <title>Colors</title>
          <link type='text/css' href='/styles.css' rel='Stylesheet' />
        </head>
        <body>
        <h1>Colors</h1>
        To see a color, enter a url like
        <a href='/color/ff0000'>/color/ff0000</a>. <br />
        Colors viewed so far:
        <ul>""")
        for color in self.requestedColors:
            request.write(
                "<li><a href='%s' style='color: #%s'>%s</a></li>" % (
                color, color, color))
        request.write("""
        </ul>
        </body>
        </html>
        """)
        return ""
class HomePage(resource.Resource):
    def render(self,request):
        return """
        <html>
        <head>
            <title>Colors</title>
            <link type='text/css' href='/styles.css' rel='Stylesheet' />
        </head>
        <body>
        <h1>Colors Demo</h1>
        What's here:
        <ul>
            <li><a href='/color'>Color viewer</a></li>
        </ul>
        </body>
        </html>
        """
if __name__=='__main__':
    from twisted.internet import reactor
    root=resource.Resource()
    root.putChild('',HomePage())
    root.putChild('color',ColorRoot())
    root.putChild('styles.css',static.File('styles.css'))
    site=server.Site(root) #by gashero
    reactor.listenTCP(8000,site)
    reactor.run()
例子4-4引用了静态文件。所以需要在resourcetree.py脚本目录下创建一个styles.css文件。内容如下:
body {
    font-family: Georgia, Times, serif;
    font-size: 11pt;
}
h1 {
    margin: 10px 0;
    padding: 5px;
    background-color: black;
    color: white;
}
a {
    font-family: monospace;
}
p {
    padding: 10px;
}
运行resourcetree.py脚本,将会在8000端口启动一个WEB服务器。下面是服务器全部可用路径:
/        主页
/css        虚拟的CSS资源
/css/styles.css    静态文件styles.css
/colors/    颜色查看页面
/colors/hexcolor 按照背景色为#hexcolor的页面
尝试通过http://localhost:8000/colors/00abef来访问,将会得到背景色为#00abef的页面,大约是亮蓝色。
可以随便试试其他颜色。同样可以进入http://localhost:8000/,选择可选答案。

4.4.2 它们如何工作?

例子4.4从twisted.web包中引入了几个类:resource.Resource、static.File、server.Site。每个resource.Resource对象做两件事。首先,定义请求的资源如何处理。第二,定义请求子资源的Resource对象。
例如查看类ColorRoot。在这个类的实例稍后被加入了/colors这个等级资源。初始化时,ColorRoot使用putChild方法插入ColorIndexPage这个资源座位''资源。这意味着所有对/colors/的请求都由ColorIndexPage对象来处理。
你可以把他们想象为等价的,但是/stuff和/stuff/是不同的。浏览器在解释相对路径时,对是否加上斜线的处理方法是不同的。在第一个例子中,对"otherpage"的请求会解释为"http://example.com/otherpage",在第二个例子中解释为"http://example.com/stuff/otherpage"。
如果你不清楚(explicit)服务器代码,这个问题可能会再次郁闷你。最好是预先设计好是否需要在URI末尾加上斜线,并重定向请求。Resource类将会简化这些操作。如果设置了addSlash属性为True,一个Resource会自动在找不到对应资源时自动在URL末尾添加斜线来再次查找。
render方法定义了一个Resources匹配了请求的路径时所作的动作。Resource.render处理简单的工作如同例子4-3中的请求处理器一样:获取一个Request对象作为唯一参数,处理请求并将响应发送到客户端。Resource.render有一些你很有必要知道的特性(caveat)。首先,它会返回一个字符串。这在大多数情况下是很方便的,你可以直接返回需要发动到客户端的数据,Resource将会发送到客户端并关闭响应。但是如果你使用了request.write来手动写入响应,则render仍然返回一个字符串。你可以返回一个空字符串来确保不向响应中加入任何东西。

有些时候,你可能在render中需要启动一个deferred操作。在这种情况下,在你的Deferred回调之前你不可以写入任何响应。你可能会想,我是否可以让Deferred返回一个字符串呢?不过很可惜,你不可以,这也是Resource对象多个不足当中的一个;本文后也讨论了为什么不能使用类做主要的WEB开发方法。不过,你可以使用魔术值twisted.web.server.NOT_DONE_YET,可以告知Resource有些事情是异步的而且尚未完成,直到你调用了request.finish()。然后调用request.finish()直到写完了所有的响应数据。(查看例子4-5了解这种技术)

ColorRoot这个Resource负责处理/colors这个路径下的请求。事实上ColorRoot仅仅是子资源的容器。ColorRoot.render调用request.redirect,一个工具函数来设置HTTP状态码为302(临时移走)并写入一个"Location:"头字段来重定向客户端请求的地址到另一个地址,在这种情况下/colors/(包含一个斜线)。注意,尽管它告诉客户端转到另外一个位置,但是render仍然返回一个字符串。

Resource提供了另一种可选的render方法。你可以通过下划线的分割来处理不同的HTTP方法:render_GET、render_POST、还有其他的。这一例子在第五章的例子5-1中有所讨论。

ColorRoot还有一个方法,getChild。这个方法用于扩展的等级制度中获取子对象。getChild方法设计用来动态管理子对象资源。一个Resource的getChild方法调用,发生在请求一个Resource等级之下的请求时。注册匹配的路径使用putChild方法。缺省时,getChild会返回404(无法找到)响应。但是你可以重载这些方法,有如ColorRoot那样。ColorRoot的getChild方法使用ColorPage对象来初始化子路径。ColorPage可以响应请求,使用最近的十六进制颜色值。

static.File类是Resource的子类,提供了磁盘上文件或目录的服务。初始化一个static.File对象并传递一个文件名做参数。使用static.File比自己装入文件更好,因为可以很好的处理大文件而并不占用过多内存,也不会因为读写硬盘而导致整个服务器停止响应。如果以一个目录初始化static.File,则其下的文件和子目录都会提供服务。

即使不使用server.Site和Resource树对象来管理WEB服务器,你可以使用static.File来处理请求。你可以使用临时的static.File来处理请求所需的文件内容,如:

static.File('file.txt').render(request)

你也可以改变static.File使用的MIME类型来在更改contentTypes属性。这个属性是一个词典,用来管理文件扩展名到MIME的映射。

server.Site类是一个Factory用来初始化Resource对象。它处理HTTP请求并分割请求路径成段并按照Resource树来找到对应的Resource来处理请求。






4.5 将WEB数据存储在数据库中

大部分WEB应用程序都需要使用SQL数据库来做数据存储。Twisted应用程序也一样,也许你并不希望使用标准Python SQL库。标准的SQL库会在查询时阻塞:每次你调用一个查询,查询函数将会暂停你的应用程序直到服务器返回结果。这将会浪费大量的时间,特别是查询需要大量处理的时候,或者连接到服务器的速度很慢。在Twisted中使用SQL数据库,你需要使用Deferred来运行查询,允许你的应用继续做事情,直到返回结果。

Twisted提供了SQL库在twisted.enterprise包。tiwsted.enterprise包并不是真正的包含SQL驱动;它只是提供一些数据库支持中可能(potentially)会遇到的问题。当然,twisted.enterprise提供了异步的API来代替Python数据库接口模块。如果需要,它将会使用线程来防止(prevent)数据库查询的阻塞。你可以使用twisted.enterprise来使用SQL数据库,只要(as long as)你拥有一个DB-API兼容(compatible)的数据库模块即可。

Twisted未来的WEB功能开发

在twisted.web.resource包和twisted.web.server包中的这些类供支持,有如他们的名字,这些只提供基本功能。他们有一些明显(的缺点,特别是缺少(lack)对Resource.render的Deferred结果支持,和Resource.getChild的支持。这些都是未来不建议使用的,而建议使用下一代的WEB包,叫做twisted.web2。不幸的是twisted.web2正在大量的开发之中,以致于在本书中提到的例子可能在出版时不再工作了。而且,twisted.web2拥有不同的API,它将会使用不同的等级方式来处理请求。

因为在不久twisted.web.resource和twisted.web.server将会淘汰,所以应该避免对这些的过度依赖。作为一个简单的服务器,你可以就近(approach)使用Example4-3,并使用twisted.http.Request的子类。twisted.web2也会提供向后兼容(backward-compatible)的接口来支持Resource对象,所以你确保例子4-5和4-6的代码应该易于移植。

如果你使用twisted建立WEB应用程序,你可以使用Nevow(发音"nouveau")WEB服务器框架,你可以从http://www.nevow.com下载。Nevow将会给你比HTTP请求更多的工作类:提供更多你所见现代WEB服务器更多的功能,包括XML模板系统。也提供类似于livepage的独一无二的功能,这将允许你通过一个持续(persistent)的连接发送JavaScript代码到浏览器。Nevow的设计是基于Twisted的,这意味着你可以到处使用Deferred,并且可以非常开放的发布开放的源码。

有如twisted.web2,Nevow还没有到达稳定API的时候,所以此处略。但是它现在已经很稳定(mature)以致于人们使用它作为WEB站点。你可以寻找最新的文档到http://www.nevow.com。

4.5.1 我如何做?

首先确保你所使用的数据库模块是Python DB-API兼容的。然后创建一个twisted.enterprise.adbapi.ConnectionPool对象来引用你的数据库驱动和连接信息。下面的例子4-6使用了MySQLdb模块来连接到( by gashero)MySQL数据库。这将会运行一个很小的weblog应用并存储提交的信息。

from twisted.web import resource,static,server,http
from twisted.enterprise import adbapi,util as dbutil

DB_DRIVER="MySQLdb"
DB_ARGS={
    'db':'test',
    'user':'username',
    'passwd':'password',
    }

class HomePage(resource.Resource):
    def __init__(self,dbConnection):
        self.db=dbConnection
        resource.Resource.__init__(self)
    def render(self,request):
        query="select title,body from posts order by post_id desc"
        self.db.runQuery(query).addCallback(/
            self._gotPosts,request).addErrback(/
            self._dbError,request)
        return server.NOT_DONE_YET
    def _gotPosts(self,results,request):
        request.write("""
        <html>
        <head><title>MicroBlob</title></head>
        <body>
          <h1>MicroBlog</h1>
          <i>Like a blog, bug less useful</i>
          <p><a href="/new">New Post</a></p>
        """)
        for title,body in results:
            request.write("<h2>%s</h2>"title)
            request.write(body)
        request.write("""
        </body>
        </html>
        """)
        request.finish()
    def _dbError(self,failure,request):
        request.setResponseCode(/
            http.INTERNAL_SERVER_ERROR)
        request.write("Error fetching posts:%s"%/
            failure.getErrorMessage())
        request.finish()

class NewPage(resource.Resource):
    def render(self,request):
        return """
        <html>
        <head><title>New Post</title></head>
        <body>
          <h1>New Post</h1>
          <form action="save" method="post">
          Title:<input type="text" name="title"/><br/>
          Body:<br/>
          <textarea cols="70" name="body"></textarea><br/>
          <input type="submit" value="Save"/>
          </form>
        </body>
        </html>
        """

class SavePage(resource.Resource):
    def __init__(self,dbConnection):
        self.db=dbConnection
        resource.Resource.__init__(self)
    def render(self,request):
        title=request.args['title'][0]
        body=request.args['body'][0]
        query="""
        INSERT INTO posts (title,body) VALUES
        (%s,%s)"""%(dbutil.quote(title,"char"),
        dbutil.quote(body,"text"))
        self.db.runQuery(query).addCallback(
            self._saved,request).addErrback(
            self._saveFailed,request)
        return server.NOT_DONE_YET
    def _saved(self,result,request):
        request.redirect("/")
        request.finish()
    def _saveFailed(self,failure,request):
        request.setResponseCode(/
            http.INTERNAL_SERVER_ERROR)
        request.write("Error saving record: %s"%(/
            failure.getErrorMessage()))
        request.finish()

class RootResource(resource.Resource):
    def __init__(self,dbConnection):
        resource.Resource.__init__(self)
        self.putChild('',HomePage(dbConnection))
        self.putChild('new',NewPage())
        self.putChild('save',SavePage(dbConnection))

if __name__=="__main__":
    from twisted.internet import reactor
    dbConnection=adbapi.ConnectionPool(DB_DRIVER,**DB_ARGS)
    f=server.Site(RootResource(dbConnection))
    reactor.listenTCP(8000,f)
    reactor.run()

例子4-6的代码会调用一个SQL表格叫做posts。可以按照如下SQL语句来创建这个表格:

CREATE TABLE posts(
    post_id int NOT NULL auto_increment,
    title varchar(255) NOT NULL,
    body text,
    PRIMARY KEY (post_id)
);

然后运行databaseblog.py来在端口8000启动服务器。然后你可以看到主页。如果至今仍然没有发生错误,那么数据库连接会工作的很好。尝试点击New Post连接并尝试登记一篇blog。提交这个表单,你将会回到主页,并可以直接看到刚才提交的blog,而现在已经保存在了数据库当中了。

4.5.2 它如何工作?

例子4-6使用了3个Resource类:HomePage、NewPage、SavePage。HomePage连接数据库并显示当前已有的提交。NewPage提供了表单供输入新的提交。SavePage处理提交的表单并将信息插入数据库。

首先,databaseblog.py脚本创建一个twisted.enterprise.adbapi.ConnectionPool对象。一个ConnectionPool管理一组数据库连接并允许发送SQL查询到数据库。初始化一个ConnectionPool对象需要数据库驱动的名字作为第一个参数,其他附加参数或关键字也将会传给数据库驱动来初始化。

当ConnecitonPool对象创建之后,将会传递给HomePage和SavePage类,供他们使用SQL查询。为了运行这些查询,HomePage和SavePage必须异步处理请求。他们开始使用ConnectionPool.runQuery方法来执行数据库查询,然后返回Deferred。例子4-6同样展示了如何为Deferred设置回调和错误回调处理器来完成向客户端发送响应结果。在任何情况下,render方法返回server.NOT_DONE_YET来指明响应正在进行异步处理。





4.6 运行HTTP代理服务器

除了HTTP服务器和客户端以外,twisted.web还包含了HTTP代理服务器的支持。一个代理服务器是一个服务器和一个客户端。他接受来自客户端的请求(作为服务器)并将他们转发到服务器(作为客户端)。然后将响应发送回客户端。HTTP代理服务器可以提供很多有用的服务:缓存、过滤和使用情况报告。下面的例子展示了如何使用Twisted构建一个HTTP代理服务器。

4.6.1 下面如何做?

twisted.web包包含了twisted.web.proxy,这个模块包含了HTTP代理服务器。例子4-7构建了一个简单的代理服务器。

from twisted.web import proxy,http
from twisted.internet import reactor
from twisted.python import log
import sys
log.startLogging(sys.stdout)
class ProxyFactory(http.HTTPFactory):
    protocol=proxy.Proxy
reactor.listenTCP(8001,ProxyFactory())
reactor.run()

运行simpleproxy.py脚本将会在8001端口启动代理服务器。在浏览器中设置这个代理服务器可以作为代理进行测试。对log.startLogging的调用将会把HTTP日志信息记录在stdout中,并可以直接查看。

$ python simpleproxy.py
2005/06/13 00:22 EDT [-] Log opened.
2005/06/13 00:22 EDT [-] __main__.ProxyFactory starting on 8001
... ...

这虽然给出了一个代理服务器,但是实际上没什么用处。例子4-8提供了更多的功能,可以跟踪最常使用的网页。

import sgmllib.re
from twisted.web import proxy,http
import sys
from twisted.python import log
log.startLogging(sys.stdout)
WEB_PORT=8000
PROXY_PORT=8001
class WordParser(sgmllib.SGMLParser):
    def __init__(self):
        sgmllib.SGMLParser.__init__(self)
        self.chardata=[]
        self.inBody=False
    def start_body(self,attrs):
        self.inBody=True
    def end_body(self):
        self.inBody=False
    def handle_data(self,data):
        if self.inBody:
            self.chardata.append(data)
    def getWords(self):
        #解出单词
        wordFinder=re.compile(r'/w*')
        words=wordFinder.findall("".join(self.chardata))
        words=filter(lambda word: word.strip(), words)
        print "WORDS ARE", words
        return words
class WordCounter(object):
    ignoredWords="the a of in from to this that and or but is was be can could i you they we at".split()
    def __init__(self):
        self.words=()
    def addWords(self,words):
        for word in words:
            word=word.lower()
            if not word in self.ignoredWords:
                currentCount=self.words.get(word,0)
                self.words[word]=currentCount+1
class WordCountProxyClient(proxy.ProxyClient):
    def handleHeader(self,key,value):
        proxy.ProxyClient.handleHeader(self,key,value)
        if key.lower()=="content-type":
            if value.split(';')[0]=='text/html':
                self.parser=WordParser()
    def handleResponsePart(self,data):
        proxy.ProxyClient.handleResponsePart(self,data)
        if hasattr(self,'parser'):
            self.parser.feed(data)
    def handleResponseEnd(self):
        proxy.ProxyClient.handleResponseEnd(self)
        if hasattr(self,'parser'):
            self.parser.close()
            self.father.wordCounter.addWords(self.parser.getWords())
            del(self.parser)
class WordCountProxyClientFactory(proxy.ProxyClientFactory):
    def buildProtocol(self,addr):
        client=proxy.ProxyClientFactory.buildProtocol(self,addr)
        #升级proxy.proxyClient对象到WordCountProxyClient
        client.__class__=WordCountProxyClient
        return client
class WordCountProxyRequest(proxy.ProxyRequest):
    protocols={'http':WordCountProxyClientFactory)
    def __init__(self,wordCounter,*args):
        self.wordCounter=wordCounter
        proxy.ProxyRequest.__init__(self,*args)
class WordCountProxy(proxy.Proxy):
    def __init__(self,wordCounter):
        self.wordCounter=wordCounter
        proxy.Proxy.__init__(self)
    def requestFactory(self,*args)
        return WordCountProxyRequest(self.wordCounter,*args)
class WordCountProxyFactory(http.HTTPFactory):
    def __init__(self,wordCount):
        self.wordCounter=wordCounter
        http.HTTPFactory.__init__(self)
    def buildProtocol(self,addr):
        protocol=WordCountProxy(self.wordCounter)
        return protocol
#使用WEB接口展示记录的接口
class WebReportRequest(http.Request):
    def __init__(self,wordCounter,*args):
        self.wordCounter=wordCounter
        http.Request.__init__(self,*args)
    def process(self):
        self.setHeader("Content-Type",'text/html')
        words=self.wordCounter.words.items()
        words.sort(lambda(w1,c1),(w2,c2): cmp(c2,c1))
        for word,count in words:
            self.write("<li>%s %s</li>"%(word,count))
        self.finish()
class WebReportChannel(http.HTTPChannel):
    def __init__(self,wordCounter):
        self.wordCounter=wordCounter
        http.HTTPChannel.__init__(self)
    def requestFactory(self,*args):
        return WebReportRequest(self.wordCounter,*args)
class WebReportFactory(http.HTTPFactory):
    def __init__(self,wordCounter):
        self.wordCounter=wordCounter
        http.HTTPFactory.__init__(self)
    def buildProtocol(self,addr):
        return WebReportChannel(self.wordCounter)
if __name__=='__main__':
    from twisted.internet import reactor
    counter=WordCounter()
    prox=WordCountProxyFactory(counter)
    reactor.listenTCP(PROXY_PORT,prox)
    reactor.listenTCP(WEB_PORT,WebReportFactory(counter))
    reactor.run()
运行wordcountproxy.py将浏览器的代理服务器设置到8001端口。浏览其他站点,或者访问http://localhost:8000/,将可以看到访问过的站点的单词频率。

4.6.2 它是如何工作的?

在例子4-8中有很多个类,但大多数是连接用的。只有很少的几个做实际工作。最开始的两个类WordParser和WordCounter,用于从HTML文档中解出单词符号并计算频率。第三个类WordCountProxy(by gashero)客户端包含了查找HTML文档并调用WordParser的任务。
因为代理服务器同时作为客户端和服务器,所以需要使用大量的类。有一个ProxyClientFactory和ProxyClient,提供了Factory/Protocol对来支持客户端连接。为了响应客户端的连接,需要使用ProxyRequest,它是HTTPFactory的子类,还有Proxy,是http.HTTPChannel的子类。它们对建立一个普通的HTTP服务器是有必要的:HTTPFactory使用Proxy作它的协议(Protocol),而代理Proxy HTTPChannel使用ProxyRequest作为它的RequestFactory。如下是客户端发送请求的事件处理流程:
·客户端建立到代理服务器的连接。这个连接被HTTPFactory所处理。
·HTTPFactory.buildProtocol会创建一个Proxy对象用来对客户端发送和接收数据。
·当客户端发送请求时,Proxy创建ProxyRequest来处理。
·ProxyRequest查找客户端请求的服务器。并创建ProxyClientFactory并调用reactor.connectTCP来通过factory连接服务器。
·一旦ProxyClientFactory连接到服务器,就会创建ProxyClient这个Protocol对象来发送和接收数据。
·ProxyClient发送原始请求。作为响应,它将客户端发来的请求发送给服务器。这是通过调用self.father.transport.write实现的。self.father是一个Proxy对象,正在管理着客户端连接对象。
这是一大串类,但实际上是分工明确的将工作由一个类传递到另一个类。但这是很重要的。为代理模块的每一个类提供一个子类,你可以完成每一步的控制。
整个例子4-8中最重要的一个技巧。ProxyClientFactory类有一个buildProtocol方法,可以包装好,供ProxyClient作为Protocol。它并不提供任何简单的方法来提供你自己的ProxyClient子类。解决办法是使用Python的__class__属性来替换升级ProxyClient对象来放回ProxyClientFactory.buildProtocol,这些将ProxyClient改变为WordCountProxyClient。
作为代理服务器的附加功能。例子4-8提供了标准的WEB服务器,在8000端口,可以显示从代理服务器来的当前单词统计数量。由此可见,在你的应用中包含一个内嵌的HTTP服务器是多么的方便,这种方式可以很好的提供给远程来显示相关状态信息。
 
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值