GVKun编程网logo

Tornado Web服务器中处理空白字符的解决方案

9

以上就是给各位分享TornadoWeb服务器中处理空白字符的解决方案,同时本文还将给你拓展20-2树莓派搭建服务器TornadoWeb服务器、42.TornadoWEB服务器入门、48.深入理解Tor

以上就是给各位分享Tornado Web服务器中处理空白字符的解决方案,同时本文还将给你拓展20-2 树莓派搭建服务器 Tornado Web服务器、42.Tornado WEB服务器入门、48.深入理解Tornado——一个异步web服务器、ajax – 如何使用Django与Tornado Web服务器?等相关知识,如果能碰巧解决你现在面临的问题,别忘了关注本站,现在开始吧!

本文目录一览:

Tornado Web服务器中处理空白字符的解决方案

Tornado Web服务器中处理空白字符的解决方案

Tornado模板引擎一直有一个坑,有时候你可能觉得并不影响正常使用,但强迫症就是受不了:模板会去掉每行前后的空格。

最后出来的页面就是这样:

不缩进真的很影响心情的好吧,特别是对一个python开发者。

国外一些Q&A对这个情况也有一些讨论,其中提到比较多的就是compress_whitespace。在github找到一个issue:https://github.com/tornadoweb/tornado/issues/178,就是在抱怨空白字符的问题。空白字符在<pre>中被去除,导致代码标签“<pre>”这块出问题。

我们看看tornado代码吧,这是Template类的构造函数:

复制代码 代码如下:

def __init__(self, template_string, name="<string>", loader=None,
             compress_whitespace=None, autoescape=_UNSET):
    self.name = name
    if compress_whitespace is None:
        compress_whitespace = name.endswith(".html") or \
            name.endswith(".js")

其中有个compress_whitespace参数,当name(模板地址)是以.html或.js结尾的时候,将compress_whitespace为真。

实际上最后在generate函数里,处理空格的代码:

复制代码 代码如下:

if writer.compress_whitespace and "<pre>" not in value:
    value = re.sub(r"([\t ]+)", " ", value)
    value = re.sub(r"(\s*\n\s*)", "\n", value)

当compress_whitespace为真,且html里没有"<pre>"的时候进入这个if语句。"<pre>"这个我估计就是解决上面那个issue用的,但明显是一个非常不pythonic的方式。

经过一番分析,可以发现,有这样一些方法可以避免“缩进”被去除:

1.Template的构造函数中,传入compress_whitespace=False。
2.在模板中加入"<pre>"。
3.模板文件不为.html或.js后缀,可以为.htm或.tpl等。
4.修改核心库代码。

第2种方法肯定是最烂的,不可能为了缩进问题去改模板。第3种方法只能算一个权衡之计,去避免麻烦而不是解决麻烦,不是我的风格,而且后缀改了往往影响编辑器里的代码高亮和代码补全。第1种方法应该是最好的,但实际上,我们在controller里调用模板是使用render()或render_string()来做的,而这两个函数是封装了Template对象的创建过程,我们根本接触不到Template的构造函数,所以也没法控制compress_whitespace的值。

所以希望官方能进行修改,让代码能pythonic。

我这里用第4种方法,直接去修改Tornado核心代码,将这几句注释掉:

20-2 树莓派搭建服务器 Tornado Web服务器

20-2 树莓派搭建服务器 Tornado Web服务器

Drive.google.com/drive/folders/1ahbeoEHkjxoo4NV1wReOmpoRWbl448z-

 

1.Tornado简介

Tornado一款使用 Python 编写的,相对简单的 非阻塞式 Web 服务器,它是非阻塞式服务器,而且速度相当快。得利于其 非阻塞的方式和对 epoll 的运用,Tornado 每秒可以处理数以千计的连接,因此 Tornado 是实时 Web 服务的一个 理想框架。

官方网站:http://www.tornadoweb.cn

官方文档:http://www.tornadoweb.cn/documentation

整个文档就一个网页,把http服务器常用的功能都讲述了一遍,如果有编程基础,应该很快就能上手。

2.Tornado安装

方式1:pip 安装:
sudo pip install tornado


方式2:源代码安装:
wget https://pypi.python.org/packages/source/t/tornado/tornado-4.3.tar.gz
tar xvzf tornado-4.3.tar.gz
cd tornado-4.3
python setup.py build
sudo python setup.py install

 

二、基本原理

 

借用Tornado官方的Hello World例子:

import tornado.ioloop
import tornado.web
 
class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("Hello,world")
 
application = tornado.web.Application([
    (r"/",MainHandler),])
 
if __name__ == "__main__":
    application.listen(8888)
    tornado.ioloop.IOLoop.instance().start()

  

大致的思路是:通过Tornado开启http端口侦听,预设好路由规则,当有请求符合路由规则时,调用对应的Handler进行处理,这也是大部分Web服务器的处理方式。支持get和post请求。

 

扩展一下,利用Tornado搭建一个http服务器,上面放一个网页,网页端传入需要控制的GPIO针脚编号和状态,服务器端接收到传入的参数,做相应的处理,即可实现控制GPIO针脚电平的输出了。



三、实现步骤

1.前端html页面

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <Meta charset="utf-8">
    <Meta http-equiv="X-UA-Compatible" content="IE=edge">
    <Meta name="viewport" content="width=device-width,initial-scale=1">
    <title>树莓派Web控制中心</title>
    <script src="static/js/jquery-3.2.1.min.js" type="text/javascript"></script>
    <script src="static/js/bootstrap.min.js" type="text/javascript"></script>
    <link href="static/css/bootstrap.css" rel="stylesheet" type="text/css" />
    <link href="static/css/font-awesome-4.7.0/css/font-awesome.min.css" rel="stylesheet"
        type="text/css" />
    <style type="text/css">
        .page-header { margin: 20px 0; border-bottom: 1px solid #eee; padding-bottom: 0; text-align: center; }
        .btn-item { text-align: center; }
        i { margin-right: 3px; display: inline-block; }
        h1 { text-align: center; }
        .tip { font-weight: bold; color: black; }
        .lead { font-size: small; }
        .gpio-item { text-align: center; }
        .btn-gnd,.btn-gpio { padding: 10px 5px; margin-bottom: 5px; width: 100%; font-size: small; }
        .gpio .row { margin-top: 5px; }
    </style>
</head>
<body>
    <div>
        <div>
            <h3>
                树莓派Web控制中心</h3>
            <p>
                用于控制连接到树莓派的各种传感器
            </p>
        </div>
        <div>
            <divhttps://www.jb51.cc/tag/heading/" target="_blank">heading">
                设备</div>
            <div>
                <div>
                    <div>
                    </div>
                    <div>
                        <a><i></i>关机</a>
                    </div>
                    <div>
                        <a><i></i>重启</a>
                    </div>
                    <div>
                    </div>
                    <script type="text/javascript">
                        var url = "/";
                        $(function () { 
                            $(".btn-trigger").click(function () {
                                var text = $(this).text().replace(/ /g,"").replace(/\n/g,"").replace(/\r/g,"").replace(/\t/g,"");
                                var cmd = "";
                                switch (text) {
                                    case "关机":
                                        cmd = "sudo shutdown -h Now";
                                        break;
                                    case "重启":
                                        cmd = "sudo reboot";
                                        break;
                                }
                                if (confirm("确定要执行该命令吗?")) {
                                    $.ajax({
                                        type: "POST",url: url,data: {
                                            action: "run-shell-cmd",cmd: cmd
                                        },success: function (result) {
                                            //$(".tip").html(result);
                                        }
                                    });
                                }
                            });
                        });
                    </script>
                </div>
            </div>
        </div>
        <div>
            <divhttps://www.jb51.cc/tag/heading/" target="_blank">heading">
                GPIO (蓝色->低电平,红色->高电平)</div>
            <div>
                <div>
                    <div>
                        左侧
                    </div>
                    <div>
                        右侧
                    </div>
                </div>
                <div>
                    <div>
                        <button disabled="disabled">
                            GND (9) 左05</button>
                    </div>
                    <div>
                        <button disabled="disabled">
                            GND (6) 右03</button>
                    </div>
                </div>
                <div>
                    <div>
                        <apin="17">17 (11) 左06</a>
                    </div>
                    <div>
                        <apin="18">18 (12) 右06</a>
                    </div>
                </div>
                <div>
                    <div>
                        <apin="27">27 (13) 左07</a>
                    </div>
                    <div>
                        <button disabled="disabled">
                            GND (14) 右07</button>
                    </div>
                </div>
                <div>
                    <div>
                        <apin="22">22 (15) 左08</a>
                    </div>
                    <div>
                        <apin="23">23 (16) 右08</a>
                    </div>
                </div>
                <div>
                    <div>
                        <button disabled="disabled">
                            GND (25) 左13</button>
                    </div>
                    <div>
                        <apin="24">24 (18) 右09</a>
                    </div>
                </div>
                <div>
                    <div>
                        <apin="5">05 (29) 左15</a>
                    </div>
                    <div>
                        <button disabled="disabled">
                            GND (20) 右10</button>
                    </div>
                </div>
                <div>
                    <div>
                        <apin="6">06 (31) 左16</a>
                    </div>
                    <div>
                        <apin="25">25 (22) 右11</a>
                    </div>
                </div>
                <div>
                    <div>
                        <apin="13">13 (33) 左17</a>
                    </div>
                    <div>
                        <button disabled="disabled">
                            GND (30) 右15</button>
                    </div>
                </div>
                <div>
                    <div>
                        <apin="19">19 (35) 左18</a>
                    </div>
                    <div>
                        <apin="12">12 (32) 右16</a>
                    </div>
                </div>
                <div>
                    <div>
                        <apin="26">26 (37) 左19</a>
                    </div>
                    <div>
                        <button disabled="disabled">
                            GND (34) 右17</button>
                    </div>
                </div>
                <div>
                    <div>
                        <apin="20">20 (37) 右19</a>
                    </div>
                    <div>
                        <apin="16">16 (36) 右18</a>
                    </div>
                </div>
                <div>
                    <div>
                        <button disabled="disabled">
                            GND (39) 右20</button>
                    </div>
                    <div>
                        <apin="21">21 (40) 右20</a>
                    </div>
                </div>
                <script type="text/javascript">
                    $(function () {
                        $(".btn-gpio").click(function () {
                            var gpio = $(this).attr("pin");
                            if ($(this).hasClass("btn-danger")) {
                                $(this).removeClass("btn-danger").addClass("btn-primary");
                            } else {
                                $(this).removeClass("btn-primary").addClass("btn-danger");
                            }
                            var status = $(this).hasClass("btn-danger") ? 1 : 0;
                            $.ajax({
                                type: "POST",data: {
                                    action: "set-gpio-pin",pin: gpio,status: status
                                },success: function (result) {
                                    //$(".tip").html(result);
                                }
                            });
 
                        });
                    })
                </script>
            </div>
        </div>
    </div>
</body>
</html>


  

2.Python脚本

#coding: utf8
import sys
import RPi.GPIO as GPIO
import time
import os
import tornado.ioloop
import tornado.web
import tornado.httpserver
import tornado.options
from tornado.options import define,options
 
#初始化
def init():
    GPIO.setwarnings(False)
    GPIO.setmode(GPIO.BCM)
 
#设置GPIO针脚电平输出
def SetPinStatus(pin,status):
    GPIO.cleanup(int(pin))
    GPIO.setup(int(pin),GPIO.OUT)
    if(int(status)==0):
        GPIO.output(int(pin),GPIO.LOW)
    else:
        GPIO.output(int(pin),GPIO.HIGH)
    
#路由处理	
class IndexHandler(tornado.web.RequestHandler):
        def get(self):
                self.render("index.html")
        def post(self):
                init()
		action=self.get_argument(‘action‘)
		#设置GPIO针脚电平输出
		if(action=="set-gpio-pin"):
		    pin = self.get_argument(‘pin‘)
                    status = self.get_argument(‘status‘)
		    SetPinStatus(pin,status)
		    self.write("true")
		#执行shell脚本,如:关机,重启
		elif(action=="run-shell-cmd"): 
		    cmd = self.get_argument(‘cmd‘)
                    os.system(cmd)					
                
if __name__ == ‘__main__‘:
    #控制台输出响应结果,正式环境可以不开启
    #tornado.options.parse_command_line()
    settings={
        "static_path":os.path.join(os.path.dirname(__file__),"static")
        }
    app = tornado.web.Application(
        handlers=[
            (r"/",IndexHandler),(r"(apple-touch-icon\.png)",tornado.web.staticfilehandler,dict(path=settings[‘static_path‘]))
        ],**settings)
    
    http_server = tornado.httpserver.HTTPServer(app)
    http_server.listen(8020)
    tornado.ioloop.IOLoop.instance().start()
    GPIO.cleanup()

  

3.目录结构

 

4.部署调试

在终端输入:python /var/www/html/pi/gpio/gpio.py (路径为我树莓派中文件路径,实际路径以你自己的环境为准。)

如果终端没报错,此时打开浏览器,输入地址:http://localhost:8020 即可访问到页面,8020是python脚本中侦听的端口,你可以自行设置。

5.运行结果

 

6.源码下载

http://download.csdn.net/download/a497785609/9990088

42.Tornado WEB服务器入门

42.Tornado WEB服务器入门

    整理了下网络上几片tornado 的入门教程,方便下后来者。

    Tornado是一个强大而可伸缩的web服务器,用Python语言开发而成。虽然是轻量级的,但是他很健壮,可以应用于正式的多种应用和工具。

    Tornado是基于一个叫公司FriendFeed开发的web框架,起初由Bret Taylor等人开发的,后来,在他们要求下,代码开源了。商业的web框架最多可以同时具有10000个连接,而Tornado开发目标是解决大规模并发网络(C10K Problem)问题的高性能web框架。它还包括了安全处理、用户认证、社交网络、异步模式及数据库、web接口函数的扩展服务。

    自200910月发布以来,获得许多团体支持,并被应用于各种不同的目标。除了FriendFeedFacebook之外,很多公司在生产中开始转向Tornado,包括QuoraTurntable.fm Bit.lyHipmunk MyYearbook

简单地说,如果你正在寻求大型CMS或大而慢的开发框架,Tornado不会是你的选择。Tornado并不要求你以特别的形式建立大型模型或一定风格的处理形式。你可以做到敏捷开发。你要创建可伸缩而友好的应用、实时的分析引擎或RESTful API,用这个框架是很好的选择。

Tornado起步

在类Linux系统中安装很容易,可以用PyPI获取,也可以从Github下载源代码,用以下命令安装:

$ curl -L -O http://github.com/downloads/facebook/tornado/tornado-2.1.1.tar.gz

$ tar xvzf tornado-2.1.1.tar.gz

$ cd tornado-2.1.1

$ python setup.py build

$ sudo python setup.py install

它没有windows系统的官方支持,但可以通过ActivePythonPyPM包管理器安装:

C:\> pypm install tornado

安装时,会带有演示模块。它包括建立博客、集成用来运行聊天服务的Facebook等。后面章节将一步一步地讲解一些例子。

简易web服务器

前文已经简单介绍了Tornado,下面将开始如何用Tornado写一个基础的web服务器。

Hello Tornado实例

Tornado是个可以用来写HTTP请求应答的框架。作为程序员,你需要编写匹配一个特别形式的HTTP请求应答。以下是一个全功能Tornado应用实例程序:

Example 1-1. The basics: hello.py

import tornado.httpserver

import tornado.ioloop

import tornado.options

import tornado.web

from tornado.options import define, options

define("port", default=8000, help="run on the given port", type=int)

class IndexHandler(tornado.web.RequestHandler):

    def get(self):

        greeting = self.get_argument(''greeting'', ''Hello'')

        self.write(greeting + '', friendly user!'')

if __name__ == "__main__":

    tornado.options.parse_command_line()

    app = tornado.web.Application(handlers=[(r"/", IndexHandler)])

    http_server = tornado.httpserver.HTTPServer(app)

    http_server.listen(options.port)

    tornado.ioloop.IOLoop.instance().start()

 

Tornado应用基本工作就是定义RequestHandler的子类。本例中,我们编写了一个简单的侦听给定端口号并应答根请求的应用。

而后在命令行下输入以下命令来测试:

$ python hello.py --port=8000

现在在浏览器中浏览http://localhost:8000/地址,或再开一个终端窗口来测试:

$ curl http://localhost:8000/

     Hello, friendly user!

$ curl http://localhost:8000/?greeting=Salutations

     Salutations, friendly user!

下面分代码块一句一句分析:

import tornado.httpserver

    import tornado.ioloop

    import tornado.options

    import tornado.web

程序开始,导入多个Tornado助手库。其中,还包括有其它的一些包,但至少需要导入以下四个才能使本演示实例运行:

from tornado.options import define, options

define("port", default=8000, help="run on the given port", type=int)

为读取命令行参数,Tornado包中含有助手库(tornado.options)。本例中使用它可以让我们特别指定HTTP服务端口。它是这样工作的:在define语句中给出选项参数会成为全局options对象,否则选项参数在命令行中以相同的名字给出。如果用户带--help参数运行程序,程序会输出定义的所有选项提示。如果用户没有提供我们定义参数的值,默认值会自动应用。Tornadotype参数做参数类型检查,如果类型错误,会抛出一个错误。本例中允许用户使用整数类型的port参数,我们也可以在程序中以options.port形式给出。如果用户没有指定,其默认值为8000

class IndexHandler(tornado.web.RequestHandler):

    def get(self):

        greeting = self.get_argument(''greeting'', ''Hello'')

        self.write(greeting + '', friendly user!'')

这是一个Tornado请求处理类。当处理请求时,Tornado实例化这个类并调用对应的HTTP请求的方法。本例中,我们只定义了一个get方法,意思是这个处理器将回应HTTP GET请求。我们在后在部分会看到更多HTTP方法接口。

greeting = self.get_argument(''greeting'', ''Hello'')

Tornado请求处理器类拥有许多有用的内建方法,其中包括get_argument,在这里用它来从HTTP请求参数中获取greeting参数,而Hello字符串作为默认值。

self.write(greeting + '', friendly user!'')

Tornado请求处理类另一个方法是write,他获取一个字符串参数并写入到HTTP应答中去。在这里,我们使用请求参数中的greeting参数,并输出到应答中。

if __name__ == "__main__":

    tornado.options.parse_command_line()

    app = tornado.web.Application(handlers=[(r"/", IndexHandler)])

以上几行用于运行Tornado应用程序。首先,我们使用Tornado options库分析命令行。然后创建Application类的实例。传给Application __init__方法最重要的参数是handlers。它指出哪个类用来处理哪个请求。

http_server = tornado.httpserver.HTTPServer(app)

http_server.listen(options.port)

tornado.ioloop.IOLoop.instance().start()

以上代码是一个模板式内容,传入一个Application对象给TornadoHTTPServer对象,它会监听我们在命令行在指出的端口(否则使用默认端口)。最后,我们创建TornadoIOLoop的实例,之后会指出程序准备好接收HTTP请求。

handlers参数

再看一下hello.py中的这一行:

app = tornado.web.Application(handlers=[(r"/", IndexHandler)])

这个参数很重要,值得仔细进步研究。它必须是一个元组的列表,其中每个元组第一个元素是正则式,第二个元素是一个RequestHandler类。在hello.py中,我们只给出了一对,你可以根据需要给出更多。

用正则指示特殊的地址

Tornado使用正则来匹配HTTP请求地址,它会自动把其中的正则表达包括在开始和结束正则符号中(例如''/''约定为''^/$''

当正则表达式中有捕获组时,组匹配内容会作为参数传给RequestHandler对象中的对应HTTP请求的方法。下一个例子中会看到这种情况。

更多

12程序比上一个例子包含更多内容,介绍更多Tornado基本概念。

Example 1-2. Handling input: string_service.py

import textwrap

import tornado.httpserver

import tornado.ioloop

import tornado.options

import tornado.web

from tornado.options import define, options

define("port", default=8000, help="run on the given port", type=int)

class ReverseHandler(tornado.web.RequestHandler):

    def get(self, input):

        self.write(input[::-1])

class WrapHandler(tornado.web.RequestHandler):

    def post(self):

        text = self.get_argument(''text'')

        width = self.get_argument(''width'', 40)

        self.write(textwrap.fill(text, width))

if __name__ == "__main__":

    tornado.options.parse_command_line()

    app = tornado.web.Application(

        handlers=[

            (r"/reverse/(\w+)", ReverseHandler),

            (r"/wrap", WrapHandler)

        ]

    )

    http_server = tornado.httpserver.HTTPServer(app)

    http_server.listen(options.port)

    tornado.ioloop.IOLoop.instance().start()

同上一例子一样,你可以用以下命令运行这个实例:

$ python string_service.py --port=8000

这个程序是以字符处理为目标的web服务基本框架。现在可以做两个操作,首先,到/reverse/stringGET请求会返回反转后的字符串:

$ curl http://localhost:8000/reverse/stressed

desserts

$ curl http://localhost:8000/reverse/slipup

pupils

第二,POST请求/wrap会传来指定的文本,并返回包装后的文本。如果POST中没有指定参数width,它就会是默认值40:

$ curl http://localhost:8000/wrap ?

-d text=Lorem+ipsum+dolor+sit+amet,+consectetuer+adipiscing+elit.

Lorem ipsum dolor sit amet, consectetuer

adipiscing elit.

这个string服务实例和上一个实例有许多相同的代码。让我们看下不同的代码。首先,看下传给Application对象的handlers参数:

app = tornado.web.Application(handlers=[

    (r"/reverse/(\w+)", ReverseHandler),

    (r"/wrap", WrapHandler)

])

在这个代码中,Application类实例化时带有两个RequestHandlers参数的handlers参数。第一个请求匹配的正则表达式:

/reverse/(\w+)

这个正则表达式匹配的请求是/reverse/后跟一个或多个字母数字混合字符。括号中的内容使Tornado保存匹配的字符串,并作为参数传给请求处理器的方法。看看这个反转字符串的处理器是如何工作的:

class ReverseHandler(tornado.web.RequestHandler):

    def get(self, input):

        self.write(input[::-1])

以上代码中,get方法有一个附加的参数input,这个参数会包含那个正则表达式匹配的字符串部分。

再看下WrapHandler的定义:

class WrapHandler(tornado.web.RequestHandler):

    def post(self):

        text = self.get_argument(''text'')

        width = self.get_argument(''width'', 40)

        self.write(textwrap.fill(text, width))

这个类处理url/wrap的请求,定义的是POST方法,意味着它接受HTTPPOST请求。

前面我们用过RequestHandler对象的get_argument方法来获取请求的附带的字符串。我们也可用这个方法获取POST请求的参数。当我们从POST请求体中取得文本和width参数后,就可以使用python内建的textwrap库来格式化处理获得的文本,并将结果字符串返回给HTTP response

有关RequestHandler的更多内容

到此为止,我们已经探索了公开的基本RequestHandler对象:如何从HTTP请求中获取参数,如何返回HTTP应答。以后章节中,还有更多的需要学习更多的内容。

HTTP请求的方法

以上例子中,每个RequestHandler类只定义了一个方法。然而,更有用的是在其中定义更多的方法。把相关的方法放在同一个类中是很有用的。例如:你可能用一个处理器类同时接受GETPOST请求来处理数据库中一个特定的ID。这里有一个假想的例子,GET请求获取ID相关信息关返回,POST请求提交相关数据到数据库中。

# matched with (r"/widget/(\d+)", WidgetHandler)

class WidgetHandler(tornado.web.RequestHandler):

    def get(self, widget_id):

        widget = retrieve_from_db(widget_id)

        self.write(widget.serialize())

    def post(self, widget_id):

        widget = retrieve_from_db(widget_id)

        widget[''foo''] = self.get_argument(''foo'')

        save_to_db(widget)

例子中我们只使用了GETPOST方法,而Tornado支持任何可用的HTTP方法(GETPOSTPUTDELETEHEADOPTIONS)。可以在RequestHandler类中定义上述任一个方法,并匹配一个特定的名字的请求。以下是另一个假想的例子,HEAD请求只是根据ID返回其是否存在,GET方法返回这个对象。

# matched with (r"/frob/(\d+)", FrobHandler)

class FrobHandler(tornado.web.RequestHandler):

    def head(self, frob_id):

        frob = retrieve_from_db(frob_id)

        if frob is not None:

            self.set_status(200)

        else:

            self.set_status(404)

    def get(self, frob_id):

        frob = retrieve_from_db(frob_id)

        self.write(frob.serialize())

HTTP状态代码

正如前面的例子中,你可以调用set_status()方法设置HTTP状态代码。更为重要的是在一些情形下Tornado会自动设置应答HTTP状态代码。以下是最常见的实例:

404 Not Found

HTTP请求路由没有匹配到任何与之相关联的RequestHandler

400 Bad Request

你调用不带默认值的get_argument()方法,它又没有这个参数。Tornado会自动返回400代码。

405 Method Not Allowed

请求的HTTP方法匹配的RequestHandler类没有定义对应方法

500 Internal Server Error

有不能退出程序的严重错误及没有捕获到的意外

200 OK

回应成功,并且没有设置其他的状态代码。

 

以上任何一个错误发生时,Tornado会默认返回带有简短信息的HTML代码和状态代码。你要改变默认的错误返回,可以覆盖RequestHandler类中的write_error方法。如下例:

import tornado.httpserver

import tornado.ioloop

import tornado.options

import tornado.web

from tornado.options import define, options

define("port", default=8000, help="run on the given port", type=int)

class IndexHandler(tornado.web.RequestHandler):

    def get(self):

        greeting = self.get_argument(''greeting'', ''Hello'')

        self.write(greeting + '', friendly user!'')

    def write_error(self, status_code, **kwargs):

        self.write("Gosh darnit, user! You caused a %d error." % status_code)

if __name__ == "__main__":

    tornado.options.parse_command_line()

    app = tornado.web.Application(handlers=[(r"/", IndexHandler)])

    http_server = tornado.httpserver.HTTPServer(app)

    http_server.listen(options.port)

    tornado.ioloop.IOLoop.instance().start()

下面就是HTTP POST请求而没有定义该方法的的回应。正常情况下返回默认信息,但是此处重载了write_error方法,获得了以下的回应:

$ curl -d foo=bar http://localhost:8000/

Gosh darnit, user! You caused a 405 error.


tornado开发学习之2.输入输出,数据库操作,内置模板,综合示例


使用python环境中的tornado进行web开发上篇已经解决了urlmap和基本运行机制的问题。接下来进行web编程就是一下几个问题

  • 1.输入数据的获取和输出运算结果
  • 2.数据库操作
  • 3.自带模板的使用

当然还有cookies和session等,这些杂项,不在本篇进行学习。

1.输入数据的获取和输入运算结果

WEB作为一种UI表达方式,最重要的是获取数据和表达运算结果,所以如何在一个web开发框架中获取url或post传递过来的参数十分重要,我建议学习所有的Web开发语言或框架都从此开始比较好。在tornado中从我自己的理解来看有两种获取参数的方式

  • a.通过urlmap做正则表达式,通过抓取组来直接获取参数

    这就需要我们在做URL配置时就想好要传递的参数如,我们需要给ArticleDetail类传递一个文章编号,我们知道这个编号只能是数字类型的。那么可以在配置时写成如下样子

    handlers = [(r''/detail/(\d+)'', ArticleDetail),]

    其中的(d+)就是一个正则的抓取组,用于获取多位的数字。 那么在ArticleDetail类的get或post函数实现时就要写成

    class ArticleDetail(BlogHandler): def get(self, id): ....具体的实现

    入口时多了一个id参数,当然你也可以用别的变量名,都会将URL匹配的结果放进这个参数传入的。

  • b.通过在get或post中使用tornado的RequestHandler的get_argument函数来获取参数

    为了同样获取到文章id这个参数,如果我们不在URL配置时做正则(一般我都不做,因为我对正则不很熟练),也可以在具体处理某个地址的类方法中使用get_argument来获得参数值,还是以获取文章编号为例,我们需要如下方式来编写Url配置

    handlers = [(r''/detail'', ArticleDetail),]

    在地址定义中我们取消了正则匹配组,让地址直接交给ArticleDetail类全权负责。 接下来在ArticleDetail中get或post函数的入口就不能在有除self意外的参数了,当使用/detail?id=1访问系统时

     class ArticleDetail(BlogHandler): def get(self): id=self.get_argument(''id'')#获取到id ....具体的实现 def post(self): id=self.get_argument(''id'')#获取到id ....具体的实现

对于输出,一般在任何WEB开发框架中都比较简单,tornado中就是简单的self.write(htmlsrc),其中htmlsrc就是即将交给浏览器显示的html文件的源码。一般是由模板引擎将数据与模板结合后字符串结果。

2.数据操作

WEB开发与其他开发一致,也需要涉及数据的持久化和数据的读取。WEB的UI是HTML解决软件的界面,参数的收集,事件的触发,结果的显示;开 发语言负责解决运算逻辑,数据读取和保存;数据库和数据文件或其他方式的持久化解决对象持久化和数据源问题(当然还有一种叫oracle的数据库!!!! 它太NB了,自己都能写WEB,也能写程序,与我理解上数据库应该干的活区别较大,请观众随意吐槽);所以解决参数获取和数据库操作,基本上就解决了 WEB开发的最大问题了(jquery?html?这也是很大的问题,但属于前端开发的主要学习方向)。

tornado内置了一个简单封装了Mysql的操作,pypm install python-mysql需要事先安装好驱动哦。公布出来的函数比较少,与直接使用python的数据接口基本一致,稍稍简单写。

  • a.数据连接

    #导入tornado的数据库包 from tornado import database #创建数据库连接,db_host数据库主机名,db_port开放的端口号,db_user用户名,db_passwd登录密码,idle_time最大空闲时间默认是7小时以秒为单位,mysql has gone away一般跟这个有关系。这种连接方式不支持连接池。较原始,效率还可以。 db = database.Connection(''%s:%s'' % (db_host, db_port), db_name, db_user, db_passwd, idle_time)

    创建数据库连接后就可以使用db来进行后边介绍的基本操作了。

  • b.查询 常用的查询方法如下

    1). query(),用于执行select 语句,返回的是行集list,例如

    users=db.query("select * from users") for u in users: print u.username,u.userpwd
    
    user=db.query("select * from users where id=%s",id) if user: print user[0].username,user[0].userpwd

    2).get(),返回的是符合条件的结果集的第一行

    user=db.get("select * from users where id=%s",id) if user: print user.username,user.userpwd

    3).execute(),用于执行select以外的语句,返回值基本无视

    rv=db.execute("update customer set sex=''男'' where id=10128") print rv

    基本上如果你的语句没写错,得到的都是0

  • c.数据结果

    知道如何查询后续就是如何利用查询得到的结果了。

    对于query()操作返回的是[]数组(命名位items),

    数组中每一个元素均是{}字典(每一个都是item)。

    如果确切知道字段名(属性名)如确切知道查询结果中至少有一行数据,包含名为title的字段,可以直接使用items[0].title得到数据。

    如果不确切知道字段名,或字段值可能为None,可使用items[0].get(''title'','''')来获取值。 最常用的方法是循环

    for item in items: print item.get(''title'','''')
  • d.其他操作

    • execute_rowcount(sql)用于获取查询或更新得到的记录行集的行数,特别有意思的是如果你要更新的内容本来已经是那个样子,如

      print db.execute_rowcount("update customer set sex=''女'' where id=10128") print db.execute_rowcount("update customer set sex=''女'' where id=10128")

      执行两遍得到的结果是1,0。我原来一直以为数据库很弱智的,看来是我那时候不懂事,很天真。

    • close()用于手动关闭数据连接,在小型应用中我们基本不会看到他的出现

3.自带的模板的使用

web编程其实可以不用模板的,但是那样不仅写的时候痛苦,写出来以后更痛苦

  • 写的时候,html代码与python代码混合,连接字符串非常费力

  • 改的时候很容易出现错误,python又不是编译执行的,上线了执行到某错误时,多恼火,低级愚蠢的错误容易犯

  • 换web的UI时这种写法根本没办法操作,那就是重写一次程序啊,杀人的心都有的

所以如果你开发了一个Web系统,但是没有用模板技术,这个系统就只能给钱多人傻的单位做了。

mako是我最喜欢的模板,但是学习tornado么,而且不是那种排版敏感的,先来学习用用它自带的。

web使用开发使用模板引擎我么要主要解决一下几个主要的问题,在tornado中我们一个个破解

- 1.路径和模板存放方式

善用os.path.dirname(file)来解决路径问题,例如我们的模板准备放在工程的根目录下的/T/目录中,对于模板存放的目录我们可以这样定义

loader = template.Loader(os.path.dirname(__file__)+"/T/")

当然我更倾向直接定义绝对路径的方法来解决这个问题,因为使用上也特别简单,而且可以把模板放在与源代码无关的目录中,上线使用时也更加安全

TP="D:\\T\\" loader=template.Loader(TP)

模板一般就是普通的文本文件,方便起见一般以html扩展名存放比较好。比如index.html,这样我们把index.html放入我们定义的模板加载路径后使用以下代码加载模板引擎

t=loader.load("index.html")

其中t就是我们直接可以使用的模板了。

  • 2.数据与模板如何结合

模板主要功能之一就是解决数据的显示方式,如何把我们的数据交给模板使用?首先我们来获取数据

cus1=db.query("select * from customers limit 3")#得到数据 cus2=db.query("SELECT * FROM customer LIMIT 5 OFFSET 5")#得到数据 htmlsrc=t.generate(datas1=cus1,datas2=cus2)#模板运算出的字符串作为html显示 self.write(htmlsrc)#向客户端发送结果

代码第三行的datas1和data2是在tornado模板文件中将要引用的变量名我们的模板编写如下

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
    "http://www.w3.org/TR/html4/loose.dtd"> <html> <body> <!--这里从datas1中读取数据--> <table style="border:1px solid"> {% for u in datas1 %} <tr> <td>{{ u.truename }}</td><td>{{ u.sex }}</td> </tr> {% end %} </table> <hr/> <table style="border:1px solid"> {% for u in datas2 %}<tr><td>{{ u.truename }}</td><td>{{ u.sex }}</td></tr> {% end %} </table> </body> </html>

可以高兴地讲:

1.tornado的模板不需要严格按照python的缩进来书写

2.tornado的模板使用{{变量或对象}}来获取具体的变量值或对象属性值

3.tornado的模板中使用{% for var in vars %}....{% end%}来实现循环

4.tornado的模板中使用{% if 条件 %}...{% elif 条件 %}...{% else %}...{% end %}方式实现模板内的判断

5.tornado的模板使用{% include 文件名 %}来包含子模板,同时也会运算子模板的运算逻辑

OK知道以上这些内容或许我们已经可以开始一个稍稍复杂一点儿程序了。下面我们来联系一下。

4.综合示例

制作的目标做一个对一个数据表进行增加、删除、修改和简单查询的程序 数据表我们假定为通讯录数据 为了简化程序的运行逻辑,尽可能不用ajax或其他需要一些javascript基础的写法 数据库当然使用mysql

我按照一般习惯的开发过程来完成这个demo,综合以上的知识,我尽量少写代码以外的文字了,写尽可能细致的注释。功能虽然特别简单,都是按照平时实际项目的开发规范和架构来执行的。

a.数据结构及访问权限

我们使用root用户来访问数据库,假定访问的密码是root123456,使用以下语句来创建数据表:

Create Table CREATE TABLE `d_contacts` ( `id` int(11) NOT NULL AUTO_INCREMENT, `truename` varchar(20) DEFAULT NULL, `sex` varchar(2) DEFAULT NULL, `phoneno` varchar(20) DEFAULT NULL, `email` varchar(20) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8
b.规划目录结构

我们假定开发的项目文件存放在D:\projecting目录中,项目名称叫GingerTornadoDemo1 image alt text

c.关键性源码解释
  • 1).处理类的基类 basehandler.py

代码如下,通过定义基础类,可以让后续的开发变得更加简单,这里使用了一个技巧性的变化。 利用了Aaron Swartz的web.py框架中的Storage类来简化参数获取。以前需要一个个用get_argument()来获取的参数现在只要用 i=self.input() 然后直接获取参数名如i.id就可以得到了。当然参数如果不存在仍然会报错的。获取后最好用utils包中的InitStorage做一次初始化(不会污 染已有属性值),确保调用无误。

 # -*- coding: utf-8 -*- import json
    __author__ = ''jy@cjlu.edu.cn'' from tornado.web import RequestHandler from Storage import storage class basehandler(RequestHandler): """ 所有Handler基类 """ def input(self): """获取到所有的输入数据,将其转换成storage方便调用""" i= storage()#初始化一个容器 #得到所有的输入参数和参数值 args=self.request.arguments #将参数写入i的属性 for a in args: i[a]=self.get_argument(a) #获取file类型的参数 i["files"]=storage(self.request.files) #获取path i["path"]=self.request.path #获取headers i["headers"]=storage(self.request.headers) return i

- 2).数据操作类 d_contacts.py

此文件保存于models目录中,用来做dcontacts数据表的基本操作,我简单封装了一些基础的函数。有些可能未在本demo中使用 过,但我认为很重要也编写了出来。 在python中JAVA和C#中的失血模型由一个简单的Storage基本全部搞定,而且可以很方便的进行数据结构的调整和属性类型变化和增补属性。所 以这个类也可以看出来Java和C#那种长时间对我编码风格的侵害有多深了。所有的程序都可以放一个文件中解决,但我仍然要把他们的功能按照多层的思想来 细分。 这个类可以视作DAL类,如果有更复杂的业务逻辑操作还可以继承或调用dcontacts来编写更多的业务逻辑(至于是继承好还是调用好,我一般选择继承,虽然有违与封闭开发的面向对象接线的传统,但方便啊。谁用谁知道。)。

 # -*- coding: utf-8 -*- __author__ = ''jy@cjlu.edu.cn'' from config import sdb from Storage import storage def GC_set(obj): return "" class d_contacts: fields=["id","truename","sex","phoneno","email"] #定义好数据表有的数据 def getAll(self): """获取全部记录集合""" items=sdb.query("select * from d_contacts") if items: for i in items: i=storage(i) return items def delEntityById(self,id): """ 根据id来删除数据   """ return sdb.execute("delete from d_contacts where id=%s",id) def getEntityById(self,id): """ 根据id来或取对象实例""" if id: id=int(id) item=sdb.get("select * from d_contacts where id=%s",id) return storage(item) else: return None def getRowsBySqlwhere(self,sqlwhere): """ 根据sqlwhere来查询数据集合""" items=sdb.query("select * from d_contacts where id>0 "+sqlwhere) if items: for i in items: i=storage(i) return items def update(self,obj): """ 更新数据行集 """ obj=storage(obj) return sdb.execute("update d_contacts set truename=%s,sex=%s,phoneno=%s,email=%s where id=%s",obj.truename,obj.sex,obj.phoneno,obj.email,obj.id) def insert(self,obj): """ 写入数据 """ obj=storage(obj) return sdb.execute("insert into d_contacts (truename,sex,phoneno,email) values(%s,%s,%s,%s)",obj.truename,obj.sex,obj.phoneno,obj.email) def getRowBySqlwhere(self,sqlwhere): """ 根据sqlwhere来获取一个数据实例 """ item=sdb.get("select * from d_contacts where id>0 "+sqlwhere) if item: item=storage(item) return item
  • 3).具体业务控制实现类 f1001.py

这是所有操作数据表dcontacts的具体业务逻辑的实现。此文件存放于f10目录中,同过它调用models包的dcontacts等来实现对d_contacts数据的操作和视觉表达。 既然是demo我就顺便使用了一下简单的ajax操作,来完成增、删、改的功能。这样就基本涵盖了大部分的web开发的基础性功能的实现方式了。

 # -*- coding: utf-8 -*- __author__ = ''jy@cjlu.edu.cn'' import os from tornado import template from Storage import storage #导入工具函数 from utils import * #导入基础类 from basehandler import basehandler #导出持久类 from models.d_contacts import d_contacts #导入模板 from config import TP #创建持久对象 optobj=d_contacts() #建立模板,指定模板路径 tl=template.Loader(os.path.join(TP,"f10/T")) class List(basehandler): """ 用于处理显示列表    """ def get(self): #获取全部输入参数 i=self.input() #获取全部数据,继续改下去一定要考虑分页 i.recs=optobj.getAll() #调用模板 t=tl.load("f1001_list.html") #把数据推给模板进行运算 htmlsrc=t.generate(i=i) #显示运算结果 self.write(htmlsrc) def post(self): """由于不需要处理POST,所以不实现  """ pass class Update(basehandler): """ 用于处理显示修改界面,处理ajax修改、新增、删除的请求   """ def get(self): i=self.input() #初始化i的rec属性,让Rec具有与数据表字段名的全部属性 i.rec=InitStorage(i,["id","truename","phoneno","email"]) #从数据库中获取数据 i.rec=optobj.getEntityById(i.id) #调用模板 t=tl.load("f1001_update.html") #把数据推给模板进行运算 htmlsrc=t.generate(i=i) #显示运算结果 self.write(htmlsrc) def post(self): """
            处理写入数据删除数据和保存数据等操作
            """ def valid(v): """ 验证邮件是否正确
                """ if IsEmail(v.email): return True else: return False #特别要注意,因为是响应的ajax的请求,返回数据类型需指定为json self.set_header("Content-Type", "application/json") #做一个容器变量 v=storage() #获取到全部的输入值 i=self.input() #将输入参数中与数据表字段名一致的值赋值给v对象的属性 v=CopyData_INC(v,i,optobj.fields) #根据操作类型来进行处理 if i.act=="add": vv=valid(i) if vv==True: optobj.insert(v) #返回处理结果JSON值 self.write(JsonResult("OK")) else: self.write(JsonResult("NOOK")) if i.act=="update": vv=valid(i) if vv==True: optobj.update(v) self.write(JsonResult("OK")) else: self.write(JsonResult("NOOK")) if i.act=="del": optobj.delEntityById(i.id) self.write(JsonResult("OK"))

- 4).其他

开发必然需要用到html和javascript,是难点但不是我们学习tornado的重点。具体看代码吧。 我只解释一下f1001_update.html模板中一段javascript

 var rv={}; $("[rel=''v'']").each(function(){ rv[$(this).attr("id")]=$(this).val(); }); ....body中有如下元素.... <td><input type="text" rel="v" name="truename" id="truename" value=""/>*</td> <td><input type="text" rel="v" name="sex" id="sex" value=""/>*</td> <td><input type="text" rel="v" name="phoneno" id="phoneno" value=""/>*</td> <td><input type="text" rel="v" name="email" id="email" value=""/>*</td> ....

这是我几乎在所有项目的录入模板中都使用的一个小技巧,我额外地给所有需要获取值的控件增加了一个rel属性。 利用jquery的选择特性来遍历id后自动获取这些组件的值,再将值赋值给实现定义的字典rv。 这样做的好处是,你几乎从前台到后台都不需要在参数值的获取上再一个个地进行赋值了。rv对象可直接在ajax调用时使用,无需处理。ajax调用参数传给处理的类后,使用i=self.input(),又将所有的参数和值变成了i的属性和值。 接下来你要做的就是使用i这个已经赋值好的对象来做你的预算了,所有以往获取参数和传递参数的麻烦就全解决了。

简单的描述你肯定感受不到这样做的好处,但当你面对一个具有几十个参数需要录入,后台对应一个数据对象时,你就会深深地爱上我的这种写法。超级爽,不用写一个赋值和获取参数的代码啦。还是去体验源码吧。

  • 5).效果和源码下载

    列表功能:

image alt text

修改功能:

image alt text

最好的文档是源码:tornado综合示例

5.结论与其他

  • 本来想写3~5篇的帖子,发现两篇就搞定了
  • 肯定是疏漏了很多东西啊,权鉴、404、UI组件、分页等等,还有很多。但我认为那些不是主干。主干是我介绍的这些,有这些你就可以使用tornado来工作了
  • 我学习到这里感受tornado挺不错的,但感觉在开发的效率上来看,与web.py还存在相当大的差距。我自己的想法是使用tornado来做 web服务器,做urlmap。好了,其他的数据库组件还是继续使用web.py的,模板继续使用mako比较和我的胃口。因为我开始学习tornado 仅是因为它的性能不错,名气不小。而且在Aaron Swartz自杀后,web.py后续的发展给我自己在组件的选择上撬开了一个小小的缺口。别无它。
  • 喜欢的兄弟拿来学习用用,不喜欢也可以来吐槽。我感觉我基本说清楚了tornado的Web开发。言语有限,我对他的系统学习到此为止了,再有问题,个别看它的文档相信是小菜了。
代码不清楚的看http://www.youliaoo.com/post/20

48.深入理解Tornado——一个异步web服务器

48.深入理解Tornado——一个异步web服务器

原文地址:http://golubenco.org/?p=16

这篇文章的目的在于对Tornado这个异步服务器软件的底层进行一番探索。我采用自底向上的方式进行介绍,从轮训开始,向上一直到应用层,指出我认为有趣的部分。

所以,如果你有打算要阅读Tornado这个web框架的源码,又或者是你对一个异步web服务器是如何工作的感兴趣,我可以在这成为你的指导。

通过阅读这篇文章,你将可以:

  • 自己写一个Comet架构程序的服务器端部分,即使你是从拷贝别人的代码开始。
  • 如果你想在Tornado框架上做开发,通过这篇文章你将更好的理解Tornado web框架。
  • 在Tornado和Twisted的争论上,你将更有见解。

介绍

假设你还不知道Tornado是什么也不知道为什么应该对它感兴趣,那我将用简短的话来介绍Tornado这个项目。如果你已经对它有了兴趣,你可以跳去看下一节内容。

Tornado是一个用Python编写的异步HTTP服务器,同时也是一个web开发框架。该框架服务于FriendFeed网站,最近Facebook也在使用它。FriendFeed网站有用户数多和应用实时性强的特点,所以性能和可扩展性是很受重视的。由于现在它是开源的了(这得归功于Facebook),我们可以彻底的对它是如何工作的一探究竟。

我觉得对非阻塞式IO (nonblocking IO) 和异步IO (asynchronous IO  AIO)很有必要谈一谈。如果你已经完全知道他们是什么了,可以跳去看下一节。我尽可能的使用一些例子来说明它们是什么。

让我们假设你正在写一个需要请求一些来自其他服务器上的数据(比如数据库服务,再比如新浪微博的open api)的应用程序,然后呢这些请求将花费一个比较长的时间,假设需要花费5秒钟。大多数的web开发框架中处理请求的代码大概长这样:

def  handler_request(self, request):
    answ 
=  self.remote_server.query(request)  #  this takes 5 seconds
    request.write_response(answ)

如果这些代码运行在单个线程中,你的服务器只能每5秒接收一个客户端的请求。在这5秒钟的时间里,服务器不能干其他任何事情,所以,你的服务效率是每秒0.2个请求,哦,这太糟糕了。 

当然,没人那么天真,大部分服务器会使用多线程技术来让服务器一次接收多个客户端的请求,我们假设你有20个线程,你将在性能上获得20倍的提高, 所以现在你的服务器效率是每秒接受4个请求,但这还是太低了,当然,你可以通过不断地提高线程的数量来解决这个问题,但是,线程在内存和调度方面的开销是 昂贵的,我怀疑如果你使用这种提高线程数量的方式将永远不可能达到每秒100个请求的效率。

如果使用AIO,达到每秒上千个请求的效率是非常轻松的事情。服务器请求处理的代码将被改成这样:

def  handler_request(self, request):
    self.remote_server.query_async(request, self.response_received)     
def  response_received(self, request, answ):     #  this is called 5 seconds later
    request.write(answ)

AIO的思想是当我们在等待结果的时候不阻塞,转而我们给框架一个回调函数作为参数,让框架在有结果的时候通过回调函数通知我们。这样,服务器就可以被解放去接受其他客户端的请求了。

然而这也是AIO不太好的地方:代码有点不直观了。还有,如果你使用像Tornado这样的单线程AIO服务器软件,你需要时刻小心不要去阻塞什么,因为所有本该在当前返回的请求都会像上述处理那样被延迟返回。

关于异步IO,比当前这篇过分简单的介绍更好的学习资料请看 The C10K problem。

源代码

该项目由github托管,你可以通过如下命令获得,虽然通过阅读这篇文章你也可以不需要它是吧。

git clone git: // github.com / facebook / tornado.git

在tornado的子目录中,每个模块都应该有一个.py文件,你可以通过检查他们来判断你是否从已经从代码仓库中完整的迁出了项目。在每个源代码 的文件中,你都可以发现至少一个大段落的用来解释该模块的doc string,doc string中给出了一到两个关于如何使用该模块的例子。

IOLoop模块

让我们通过查看ioloop.py文件直接进入服务器的核心。这个模块是异步机制的核心。它包含了一系列已经打开的文件描述符(译者:也就是文件指针)和每个描述符的处理器(handlers)。它的功能是选择那些已经准备好读写的文件描述符,然后调用它们各自的处理器(一种IO多路复用的实现,其实就是socket众多IO模型中的select模型,在Java中就是NIO,译者注)。

可以通过调用add_handler()方法将一个socket加入IO循环中:

def  add_handler(self, fd, handler, events):
    
""" Registers the given handler to receive the given events for fd. """
    self._handlers[fd] 
=  handler
    self._impl.register(fd, events 
|  self.ERROR)

_handlers这个字典类型的变量保存着文件描述符(其实就是socket,译者注)到当该文件描述符准备好时需要调用的方法的映射(在Tornado中,该方法被称为处理器)。然后,文件描述符被注册到epoll(unix中的一种IO轮询机制,貌似,译者注)列表中。Tornado关心三种类型的事件(指发生在文件描述上的事件,译者注):READ,WRITE 和 ERROR。正如你所见,ERROR是默认为你自动添加的。

self._impl是select.epoll()和selet.select()两者中的一个。我们稍后将看到Tornado是如何在它们之间进行选择的。

现在让我们来看看实际的主循环,不知何故,这段代码被放在了start()方法中:

复制代码
def start(self):
    """Starts the I/O loop.
    The loop will run until one of the I/O handlers calls stop(), which
    will make the loop stop after the current event iteration completes.
    """
    self._running = True
    while True:
    [ ... ]
        if not self._running:
            break
        [ ... ]
        try:
            event_pairs = self._impl.poll(poll_timeout)
        except Exception, e:
            if e.args == (4, "Interrupted system call"):
                logging.warning("Interrupted system call", exc_info=1)
                continue
            else:
                raise
        # Pop one fd at a time from the set of pending fds and run
        # its handler. Since that handler may perform actions on
        # other file descriptors, there may be reentrant calls to
        # this IOLoop that update self._events
        self._events.update(event_pairs)
        while self._events:
            fd, events = self._events.popitem()
            try:
                self._handlers[fd](fd, events)
            except KeyboardInterrupt:
                raise
            except OSError, e:
                if e[0] == errno.EPIPE:
                    # Happens when the client closes the connection
                    pass
                else:
                    logging.error("Exception in I/O handler for fd %d",
                                  fd, exc_info=True)
            except:
                logging.error("Exception in I/O handler for fd %d",
                              fd, exc_info=True)
复制代码

poll()方法返回一个形如(fd: events)的键值对,并赋值给event_pairs变量。由于当一个信号在任何一个事件发生前到来时,C函数库中的poll()方法会返回 EINTR(实际是一个值为4的数值),所以"Interrupted system call"这个特殊的异常需要被捕获。更详细的请查看man poll。

在内部的while循环中,event_pairs中的内容被一个一个的取出,然后相应的处理器会被调用。pipe 异常在这里默认不进行处理。为了让这个类适应更一般的情况,在http处理器中处理这个异常是一个更好的方案,但是选择现在这样处理或许是因为更容易一些。

注释中解释了为什么使用字典的popitem()方法,而不是使用更普遍一点的下面这种做法(指使用迭代,译者注):

for  fd, events  in  self._events.items():

原因很简单,在主循环期间,这个_events字典变量可能会被处理器所修改。比如remove_handler()处理器。这个方法把fd(即文件描述符,译者注)从_events字典中取出(extracts,意思是取出并从_events中删除,译者注),所以即使fd被选择到了,它的处理器也不会被调用(作 者的意思是,如果使用for迭代循环_events,那么在迭代期间_events就不能被修改,否则会产生不可预计的错误,比如,明明调用了 remove_handler()方法删除了某个<fd, handler>键值对,但是该handler还是被调用了,译者注)。

(意义不大的)循环结束技巧

怎么让这个主循环停止是很有技巧性的。self._running变量被用来在运行时从主循环中跳出,处理器可以通过调用stop()方法把它设置 为False。通常情况下,这就能让主循环停止了,但是stop()方法还能被一个信号处理器所调用,所以,如果1)主循环正阻塞在poll()方法 处,2)服务端没有接收到任何来自客户端的请求3)信号没有被OS投递到正确的线程中,你将不得不等待poll()方法出现超时情况后才会返回。考虑到这 些情况并不时常发生,还有poll()方法的默认超时时间只不过是0.2秒,所以这种让主循环停止的方式还算过得去。

但不管怎样,Tornado的开发者为了让主循环停止,还是额外的创建了一个没有名字的管道和对应的处理器,并把管道的一端放在了轮询文件描述符列表中。当需要停止时,在管道的另一端随便写点什么,这能高效率的(意思是马上,译者注)唤醒主循环在poll()方法处的阻塞(貌似Java NIO的Windows实现就用了这种方法,译者注)。这里节选了一些代码片段:

复制代码
def __init__(self, impl=None):
    [...]
    # Create a pipe that we send bogus data to when we want to wake
    # the I/O loop when it is idle
    r, w = os.pipe()
    self._set_nonblocking(r)
    self._set_nonblocking(w)
    self._waker_reader = os.fdopen(r, "r", 0)
    self._waker_writer = os.fdopen(w, "w", 0)
    self.add_handler(r, self._read_waker, self.WRITE)
def _wake(self):
    try:
        self._waker_writer.write("x")
    except IOError:
        pass
复制代码

实际上,上述代码中存在一个bug:那个只读文件描述符r,虽然是用来读的,但在注册时却附加上了WRITE类型的事件,这将导致该注册实际不会被响应。正如我先前所说的,用不用专门找个方法其实没什么的,所以我对他们没有发现这个方法不起作用的事实并不感到惊讶。我在mail list中报告了这个情况,但是尚未收到答复。

定时器

另外一个在IOLoop模块中很有特点的设计是对定时器的简单实现。一系列的定时器会被以是否过期的形式来维护和保存,这用到了python的bisect模块:

def add_timeout(self, deadline, callback):
    """Calls the given callback at the time deadline from the I/O loop."""
    timeout = _Timeout(deadline, callback)
    bisect.insort(self._timeouts, timeout)
    return timeout

在主循环中,所有过期了的定时器的回调会按照过期的顺序被触发。poll()方法中的超时时间会动态的进行调整,调整的结果就是如果没有新的客户端请求,那么下一个定时器就好像没有延迟一样的被触发(意思是如果没有新的客户端的请求,poll()方法将被阻塞直到超时,这个超时时间的设定会根据下一个定时器与当前时间之间的间隔进行调整,调整后,超时的时间会等同于距离下一个定时器被触发的时间,这样在poll()阻塞完后,下一个定时器刚好过期,译者注)。

选择select方案

让我们现在快速的看一下poll和select这两种select方案的实现代码。Python已经在版本2.6的标准库中支持了epoll,你可 以通过在select模块上使用hasattr()方法检测当前Python是否支持epoll。如果python版本小于2.6,Tornado将用它 自己的基于C的epoll模块。你可以在tornado/epoll.c文件中找到它源代码。如果最后这也不行(因为epoll不是每个Linux都有 的),它将回退到selec._Select并把_EPoll类包装成和select.epoll一样的api接口。在你做性能测试之前,请确定你能使用 epoll,因为select在有大量文件描述符情况下的效率非常低。

复制代码
# Choose a poll implementation. Use epoll if it is available, fall back to
# select() for non-Linux platforms
if hasattr(select, "epoll"):
    # Python 2.6+ on Linux
    _poll = select.epoll
else:
    try:
        # Linux systems with our C module installed
        import epoll
        _poll = _EPoll
    except:
        # All other systems
        import sys
        if "linux" in sys.platform:
            logging.warning("epoll module not found; using select()")
        _poll = _Select
复制代码

通过上述阅读,我们的介绍已经涵盖了大部分IOLoop模块。正如广告中介绍的那样,它是一段优雅而又简单的代码。

从sockets到流

让我们来看看IOStream模块。它的目的是提供一个对非阻塞式sockets的轻量级抽象,它提供了三个方法:

  • read_until(),从socket中读取直到遇到指定的字符串。这为在读取HTTP头时遇到空行分隔符自动停止提供了方便。
  • read_bytes(),从socket中读取指定数量的字节。这为读取HTTP消息的body部分提供了方便。
  • write(),将指定的buffer写入socket并持续监测直到这个buffer被发送。
所有上述的方法都可以通过异步方式在它们完成时触发回调函数。

write()方法提供了将调用者提供的数据加以缓冲直到IOLoop调用了它的(指write方法的,译者注)处理器的功能,因为到那时候就说明socket已经为写数据做好了准备:

复制代码
def write(self, data, callback=None):
    """Write the given data to this stream.
    If callback is given, we call it when all of the buffered write
    data has been successfully written to the stream. If there was
    previously buffered write data and an old write callback, that
    callback is simply overwritten with this new callback.
    """
    self._check_closed()
    self._write_buffer += data
    self._add_io_state(self.io_loop.WRITE)
    self._write_callback = callback
复制代码

该方法只是用socket.send()来处理WRITE类型的事件,直到EWOULDBLOCK异常发生或者buffer被发送完毕。

读数据的方法和上述过程正好相反。读事件的处理器持续读取数据直到缓冲区被填满为止。这就意味着要么读取指定数量的字节(如果调用的是read_bytes()),要么读取的内容中包含了指定的分隔符(如果调用的是read_util()):

复制代码
def _handle_read(self):
    try:
        chunk = self.socket.recv(self.read_chunk_size)
    except socket.error, e:
        if e[0] in (errno.EWOULDBLOCK, errno.EAGAIN):
            return
        else:
            logging.warning("Read error on %d: %s",
                            self.socket.fileno(), e)
            self.close()
            return
    if not chunk:
        self.close()
        return
    self._read_buffer += chunk
    if len(self._read_buffer) >= self.max_buffer_size:
        logging.error("Reached maximum read buffer size")
        self.close()
        return
    if self._read_bytes:
        if len(self._read_buffer) >= self._read_bytes:
            num_bytes = self._read_bytes
            callback = self._read_callback
            self._read_callback = None
            self._read_bytes = None
            callback(self._consume(num_bytes))
    elif self._read_delimiter:
        loc = self._read_buffer.find(self._read_delimiter)
        if loc != -1:
            callback = self._read_callback
            delimiter_len = len(self._read_delimiter)
            self._read_callback = None
            self._read_delimiter = None
            callback(self._consume(loc + delimiter_len))
复制代码

如下所示的_consume方法是为了确保在要求的返回值中不会包含多余的来自流的数据,并且保证后续的读操作会从当前字节的下一个字节开始(先将流中的数据读到self.read_buffer中,然后根据要求进行切割,返回切割掉的数据,保留切割后的数据供下一次的读取,译者注):

def _consume(self, loc):
    result = self._read_buffer[:loc]
    self._read_buffer = self._read_buffer[loc:]
    return result

还值得注意的是在上述_handle_read()方法中read buffer的上限——self.max_buffer_size。默认值是100MB,这似乎对我来说是有点大了。举个例子,如果一个攻击者和服务端建 立了100个连接,并持续发送不带头结束分隔符的头信息,那么Tornado需要10GB的内存来处理这些请求。即使内存ok,这种数量级数据的复制操作 (比如像上述_consume()方法中的代码)很可能使服务器超负荷。我们还注意到在每次迭代中_handle_read()方法是如何在这个 buffer中搜索分隔符的,所以如果攻击者以小块形式发送大量的数据,服务端不得不做很多次搜索工作。归根结底,你应该想要将这个参数和谐掉,除非你真 的很希望那样(Bottom of line, you might want to tune this parameter unless you really expect requests that big 不大明白怎么翻译,译者注)并且你有足够的硬件条件。

HTTP 服务器

有了IOLoop模块和IOStream模块的帮助,写一个异步的HTTP服务器只差一步之遥,这一步就在httpserver.py中完成。

HTTPServer类它自己只负责处理将接收到的新连接的socket添加到IOLoop中。该监听型的socket自己也是IOLoop的一部分,正如在listen()方法中见到的那样:

复制代码
def listen(self, port, address=""):
    assert not self._socket
    self._socket = socket.(socket.AF_INET, socket.SOCK_STREAM, 0)
    flags = fcntl.fcntl(self._socket.fileno(), fcntl.F_GETFD)
    flags |= fcntl.FD_CLOEXEC
    fcntl.fcntl(self._socket.fileno(), fcntl.F_SETFD, flags)
    self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    self._socket.setblocking(0)
    self._socket.bind((address, port))
    self._socket.listen(128)
    self.io_loop.add_handler(self._socket.fileno(), self._handle_events,
                             self.io_loop.READ)


复制代码

除了绑定给定的地址和端口外,上述代码还设置了"close on exec"和"reuse address"这两个标志位。前者在应用程序创建子进程的时候特别有用。在这种情况下,我们不想让套接字保持打开的状态(任何设置了"close on exec"标志位的文件描述符,都不能被使用exec函数方式创建的子进程读写,因为该文件描述符在exec函数调用前就会被自动释放,译者注)。后者用来避免在服务器重启的时候发生“该地址以被使用”这种错误时很有用。

正如你所见到的,后备连接所允许的最大数目是128(注意,listen方法并不是你想象中的“开始在128端口上监听”的意思,译者注)。这意味着如果有128个连接正在等待被accept,那么直到服务器有时间将前面128个连接中的某几个accept了,新的连接都将被拒绝。我建议你在做性能测试的时候将该参数调高,因为当新的连接被抛弃的时候将直接影响你做测试的准确性。

在上述代码中注册的_handle_events()处理器用来accept新连接,并创建相关的IOStream对象和初始化一个HTTPConnection对象,HTTPConnection对象负责处理剩下的交互部分:

复制代码
def _handle_events(self, fd, events):
    while True:
        try:
            connection, address = self._socket.accept()
        except socket.error, e:
            if e[0] in (errno.EWOULDBLOCK, errno.EAGAIN):
                return
            raise
        try:
            stream = iostream.IOStream(connection, io_loop=self.io_loop)
            HTTPConnection(stream, address, self.request_callback,
                           self.no_keep_alive, self.xheaders)
        except:
            logging.error("Error in connection callback", exc_info=True)
复制代码

可以看到这个方法在一次迭代中accept了所有正在等待处理的连接。也就是说直到EWOULDBLOCK异常发生while True循环才会退出,这也就意味着当前没有需要处理accept的连接了。

HTTP头的部分的解析工作开始于HTTPConnection类的构造函数__init()__():

复制代码
def __init__(self, stream, address, request_callback, no_keep_alive=False,
             xheaders=False):
    self.stream = stream
    self.address = address
    self.request_callback = request_callback
    self.no_keep_alive = no_keep_alive
    self.xheaders = xheaders
    self._request = None
    self._request_finished = False
    self.stream.read_until("rnrn", self._on_headers)
复制代码

如果你很想知道xheaders参数的意义,请看这段注释:

如果xheaders为True,我们将支持把所有请求的HTTP头解析成X - Real - Ip和X - Scheme格式,而原先我们将HTTP头解析成remote IP和HTTP scheme格式。这种格式的HTTP头在Tornado运行于反向代理或均衡负载服务器的后端时将非常有用。

_on_headers()回调函数实际用来解析HTTP头,并在有请求内容的情况下通过使用read_bytes()来读取请求的内容部分。_on_request_body()回调函数用来解析POST的参数并调用应用层提供的回调函数:

复制代码
def _on_headers(self, data):
    eol = data.find("rn")
    start_line = data[:eol]
    method, uri, version = start_line.split(" ")
    if not version.startswith("HTTP/"):
        raise Exception("Malformed HTTP version in HTTP Request-Line")
    headers = HTTPHeaders.parse(data[eol:])
    self._request = HTTPRequest(
        connection=self, method=method, uri=uri, version=version,
        headers=headers, remote_ip=self.address[0])
    content_length = headers.get("Content-Length")
    if content_length:
        content_length = int(content_length)
        if content_length > self.stream.max_buffer_size:
            raise Exception("Content-Length too long")
        if headers.get("Expect") == "100-continue":
            self.stream.write("HTTP/1.1 100 (Continue)rnrn")
        self.stream.read_bytes(content_length, self._on_request_body)
        return
    self.request_callback(self._request)
def _on_request_body(self, data):
    self._request.body = data
    content_type = self._request.headers.get("Content-Type", "")
    if self._request.method == "POST":
        if content_type.startswith("application/x-www-form-urlencoded"):
            arguments = cgi.parse_qs(self._request.body)
            for name, values in arguments.iteritems():
                values = [v for v in values if v]
                if values:
                    self._request.arguments.setdefault(name, []).extend(
                        values)
        elif content_type.startswith("multipart/form-data"):
            boundary = content_type[30:]
            if boundary: self._parse_mime_body(boundary, data)
    self.request_callback(self._request)

复制代码

将结果写回客户端的工作在HTTPRequest类中处理,你可以在上面的_on_headers()方法中看到具体的实现。HTTPRequest类仅仅将写回的工作代理给了stream对象。

def write(self, chunk):
    assert self._request, "Request closed"
    self.stream.write(chunk, self._on_write_complete)

未完待续?

通过这篇文章,我已经涵盖了从socket到应用层的所有方面。这应该能给你关于Tornado是如何工作的一个清晰的理解。总之,我认为Tornado的代码是非常友好的,我希望你也这样认为。

Tornado框架还有很大一部分我们没有探索,比如wep.py(应该是web.py,译者注)这个实际与你应用打交道的模块,又或者是template engine模块。如果我有足够兴趣的话,我也会介绍这些部分。可以通过订阅我的RSS feed来鼓励我。

ajax – 如何使用Django与Tornado Web服务器?

ajax – 如何使用Django与Tornado Web服务器?

如何使用Django与Tornado Web服务器?
这很简单(特别是对于django 1.4)。

1 – 只需构建您的django项目(和应用程序),并确保它工作正常。

2-在根文件夹创建一个新的python文件(使用django-admin.py startproject的目录相同的目录)

3然后复制下面的代码,编辑os.environ [‘DJANGO_SETTINGS_MODULE’]行,并将其粘贴到新的.py文件中。

import os
import tornado.httpserver
import tornado.ioloop
import tornado.wsgi
import sys
import django.core.handlers.wsgi
#sys.path.append('/home/lawgon/') # path to your project ( if you have it in another dir).


def main():
    os.environ['DJANGO_SETTINGS_MODULE'] = 'myProject.settings' # path to your settings module
    application = django.core.handlers.wsgi.WsgiHandler()
    container = tornado.wsgi.WsgiContainer(application)
    http_server = tornado.httpserver.HTTPServer(container)
    http_server.listen(8888)
    tornado.ioloop.IOLoop.instance().start()

if __name__ == "__main__":
    main()

Django 1.6应该是这样的:

import os
import tornado.httpserver
import tornado.ioloop
import tornado.wsgi
from django.core.wsgi import get_wsgi_application

def main():
    os.environ['DJANGO_SETTINGS_MODULE'] = 'myproject.settings' # path to your settings module
    application = get_wsgi_application()
    container = tornado.wsgi.WsgiContainer(application)
    http_server = tornado.httpserver.HTTPServer(container)
    http_server.listen(8888)
    tornado.ioloop.IOLoop.instance().start()

if __name__ == "__main__":
    main()

关于Tornado Web服务器中处理空白字符的解决方案的介绍现已完结,谢谢您的耐心阅读,如果想了解更多关于20-2 树莓派搭建服务器 Tornado Web服务器、42.Tornado WEB服务器入门、48.深入理解Tornado——一个异步web服务器、ajax – 如何使用Django与Tornado Web服务器?的相关知识,请在本站寻找。

本文标签:

上一篇如何在WebForm中使用javascript防止连打(双击)(winform javascript)

下一篇ASP.NET WebForm中<%=%>与<%#%>的区别(asp.net webform和mvc)