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

    技术2022-05-11  67

    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.1Host: www.example.com服务器响应的第一行告知客户端响应的HTTP版本和状态码。有如请求一样,响应包含了头字段,并用空行隔开消息主体。如下是HTTP响应的例子:HTTP/1.1 200 OKContent-Type: text/plainContent-Length: 17Connection: CloseHello HTTP world!设置简单的HTTP服务器,需要写一个Protocol允许客户端连接。查找空行标志着头字段的结束。然后发送HTTP响应,例子4-1展示了简单的HTTP实现。from twisted.protocols import basicfrom twisted.internet import protocol,reactorclass 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=HttpEchoProtocolreactor.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 httpclass 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=MyRequestHandlerclass MyHttpFactory(http.HTTPFactory):    protocol=MyHttpif __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 httpdef 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=FunctionHandledRequestclass MyHttpFactory(http.HTTPFactory):    protocol=MyHttpif __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/peoplehttp://example.com/people/charleshttp://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,serverclass 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,httpfrom twisted.enterprise import adbapi,util as dbutilDB_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,httpfrom twisted.internet import reactorfrom twisted.python import logimport syslog.startLogging(sys.stdout)class ProxyFactory(http.HTTPFactory):    protocol=proxy.Proxyreactor.listenTCP(8001,ProxyFactory())reactor.run()运行simpleproxy.py脚本将会在8001端口启动代理服务器。在浏览器中设置这个代理服务器可以作为代理进行测试。对log.startLogging的调用将会把HTTP日志信息记录在stdout中,并可以直接查看。$ python simpleproxy.py2005/06/13 00:22 EDT [-] Log opened.2005/06/13 00:22 EDT [-] __main__.ProxyFactory starting on 8001... ...这虽然给出了一个代理服务器,但是实际上没什么用处。例子4-8提供了更多的功能,可以跟踪最常使用的网页。import sgmllib.refrom twisted.web import proxy,httpimport sysfrom twisted.python import loglog.startLogging(sys.stdout)WEB_PORT=8000PROXY_PORT=8001class 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 wordsclass 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+1class 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 clientclass 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)