Torando 入门

1. 前言

Tornado 是使用 Python 编写的一个强大的、可拓展性的 Web 服务器/框架。与其他主流 Web 服务器框架有着明显区别:Tornado 支持异步非阻塞框架。同时它处理速度非常快,每秒钟可以处理数以千计的链接,是一个理想的 Web 框架。

下载安装

# pip安装
pip3 install tornado
 
# 源码安装
tar xvzf tornado-4.4.1.tar.gz
cd tornado-4.4.1
python setup.py build
sudo python setup.py install

Tornado 主要模块

# 主要模块
web        # FriendFeed 使用的基础 Web 框架,包含了 Tornado 的大多数重要的功能
escape     # XHTML, JSON, URL 的编码/解码方法
database   # 对 MySQLdb 的简单封装,使其更容易使用
template   # 基于 Python 的 web 模板系统
httpclient # 非阻塞式 HTTP 客户端,它被设计用来和 web 及 httpserver 协同工作
auth       # 第三方认证的实现(包括 Google、Facebook、Yahoo BBAuth、FriendFeed...)
locale     # 针对本地化和翻译的支持
options    # 命令行和配置文件解析工具,针对服务器环境做了优化

# 底层模块
httpserver # 服务于 web 模块的一个非常简单的 HTTP 服务器的实现
iostream   # 对非阻塞式的 socket 的简单封装,以方便常用读写操作
ioloop     # 核心的 I/O 循环

2. 快速上手

Tornado 请求生命周期

  • 程序启动:获取配置文件生成 URL 映射(根据映射找到对应的类处理请求),创建 socket 对象,将其添加到 epoll 中,然后循环监听 socket 对象变化。
  • 接收处理请求:接收客户端socket发送的请求(socket.accept),获取请求头信息,根据请求头获取请求 URL,然后根据路由映射关系匹配相关类。匹配成功就处理请求,处理完毕将响应返回给客户端,最后关闭 socket。

2.1 牛刀小试

一个简单的小示例:

#!/usr/bin/env python
# -*- coding:utf-8 -*-

import tornado.ioloop
import tornado.web


class MainHandler(tornado.web.RequestHandler):
    # 请求方式(get、post、delete...)
    def get(self):
        self.write("Hello, world")

# 路由映射
application = tornado.web.Application([
    (r"/index", MainHandler),
])

if __name__ == "__main__":
    application.listen(8888)    # 监听 8888 端口
    tornado.ioloop.IOLoop.instance().start()

访问:http://127.0.0.1:8888/index,效果如下图所示:

1.2 路由系统

在 Django 中有 CBV 和 FBV 之分,url 可以对应函数也可以对应类,但是 Torando 一个 URL 对应一个类。

处理请求、请求方法

程序根据不同的 URL 正则匹配不同的类,并交付给 tornado.web.RequestHandler 的子类处理 子类处理。子类再根据请求方法调用不同的函数进行处理,最终将处理结果返回给浏览器。

self.write("<h1>Hello, World</h1>")    # html代码直接写在浏览器客户端
self.render("index.html")              # 返回html文件,调用render_string(),内部其实是打开并读取文件,返回内容
self.redirect("http://www.baidu.com",permanent=False) # 跳转重定向,参数代表是否永久重定向

name = self.get_argument("name")       # 获取客户端传入的参数值
name = self.get_arguments("name")      # 获取多个值,类别形式
file = self.request.files["filename"]  # 获取客户端上传的文件

raise tornado.web.HTTPError(403)       # 返回错误信息给客户端

重写 tornado.web.RequestHandlerself.initialize() 函数

tornado.web.RequestHandler 构造函数 init() 中有一个 self.initialize 函数,它是一个 Tornado 框架提交的 钩子函数,用于子类初始化,为每个请求调用。

在后面自定义 session 可以用到。

1.3 模板系统

1.3.1 静态文件及模板配置

# 配置静态文件和模板文件
settings = {
    'static_path': 'static',        # 静态文件目录名
    'static_url_prefix': '/static/',  # 静态文件 url 前缀,可以是其他
    'template_path': 'templates',

}


# 生成路由规则
application = tornado.web.Application([
    # (r"/index", MainHandler),
    # (r'/login', LoginHandler),
], **settings)

静态文件缓存的实现

def get_content_version(cls, abspath):
    """Returns a version string for the resource at the given path.

        This class method may be overridden by subclasses.  The
        default implementation is a hash of the file's contents.

        .. versionadded:: 3.1
        """
    data = cls.get_content(abspath)
    hasher = hashlib.md5()
    if isinstance(data, bytes):
        hasher.update(data)
    else:
        for chunk in data:
            hasher.update(chunk)
    return hasher.hexdigest()

1.3.2 模板语言

Tornado 模板语言类似于 Django 的模板语言,很多语法都相似。如:控制语句都用 % % 包裹、表达语句都用 {{ }} 包裹等等。

1、简单示例

app.py

#!/usr/bin/env python
# -*- coding:utf-8 -*-

import tornado.ioloop
import tornado.web


class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.render("index.html", list_info=[11, 22, 33])

settings = {
    'static_path': 'static',        # 静态文件目录名
    'static_url_prefix': '/static/',  # 静态文件 url 前缀,可以是其他
    'template_path': 'templates',

}

application = tornado.web.Application([
    (r"/index", MainHandler),
], **settings)

if __name__ == "__main__":
    application.listen(8888)
    tornado.ioloop.IOLoop.instance().start()

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    {{ list_info }}

    <p>{{ list_info[0] }}</p>
    {% for item in list_info %}
        <li>{{ item }}</li>
    {% end %}
</body>
</html>

Tips:

  • Tornado 支持 if、for、while 和 try 等语句,以 {% end %} 结尾,区别于 Django
  • 取列表或元组之类中单独某个元素时,与 Python 取法类似:{{ list_info[0] }},Django:{{ list_info.0 }}
  • Tornado 也支持模板继承,通过 extendsblock 实现
  • 跨站请求伪造:form 表单提交时使用{% raw xsrf_form_html() %},类似于 Django 的 csrf_token

Tornado 在模板中默认提供了一些函数、字段、类

这些函数、字段或类可以直接拿来使用,不需要再定义:

escape          # tornado.escape.xhtml_escape 的別名
xhtml_escape    # tornado.escape.xhtml_escape 的別名
url_escape      # tornado.escape.url_escape 的別名
json_encode     # tornado.escape.json_encode 的別名
squeeze         # tornado.escape.squeeze 的別名
linkify         # tornado.escape.linkify 的別名
datetime        #  Python 的 datetime 模组
handler         # 当前的 RequestHandler 对象
request         # handler.request 的別名
current_user    # handler.current_user 的別名
locale          # handler.locale 的別名
_               # handler.locale.translate 的別名
static_url      # for handler.static_url 的別名
xsrf_form_html  # handler.xsrf_form_html 的別名

比如 static_url() 函数可以用来找到静态文件:

<link rel="stylesheet" href="{{ static_url('css/hj.css') }}">

自定义函数

Tornado 模板系统还支持自定义函数,可以像 Python 一样传入参数使用:

app.py

def func(name):
    return 'Hello, ' + name

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        # h = '<h1>raw 转义</h1>'
        # self.render("index.html", **{'h': '<h1>raw 转义</h1>', 'list_info': "[11, 22, 33]"})

        person_list = [
            {
                'name': 'rose',
                'age': 18,
                'length': 170
            }
        ]
        self.render('index.html', person_list=person_list, func=func)

{% for person in person_list %}
        <li>{{ func(person['name']) }}</li>
{% end %}


关于 Tornado 中转义

后台带有 <、> 传到前端模板中会被转义为:&lt;、&gt; 等,要想不被转义,而输出的是原始字符串,可以使用以下三种方法:

  • 使用 {% raw 字符串 %}
  • 整个程序关闭转义功能:在 Application 构造函数中传递 autoescape=None 即可被关闭
  • 每个模板中关闭转义功能:在模板中添加 {% autoescape None %} 即可关闭

对于已经关闭了转义功能的模板文件,若想对特殊字段转义,可以在模板文件中使用 escape() 函数:{{ escape(字符串) }}

h = '<h1>raw 转义</h1>'

self.render('index.html', h=h)

# <p>{{ h }}</p>
# <p>{% raw h %}</p>

# {% autoescape None %}

# {{ escape(h) }}

Tips:

  • Firefox 中会直接弹出 alert 窗口
  • Chrome 中需要在 self.write() 前添加响应头 set_header("X-XSS-Protection", 0) 解决

1.3.2 模板继承

类似于 Django 模板继承,也是使用 extendsblock 来实现模板继承:

母版 base.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    {% block css %} {% end %}
</head>
<body>
    <div class="page-container">
        <h1>模板继承</h1>
    </div>
    
    {% block content %} {% end %}

    {% block js %} {% end %}
</body>
</html>

子模板 index.html

{% extends 'base.html' %}

{% block content %}
    <h1>子模板</h1>

{% end %}

1.3.3 include 引入其他组件

其他 HTML 组件

<div>
    <p>模板继承</p>
    <ul>
        <li>extends</li>
        <li>include</li>
    </ul>
</div>

index.html

{% extends 'base.html' %}

{% block content %}
    <h1>子模板</h1>
    {% include 'test.html' %}
{% end %}

1.3.4 UIMothods 和 UIModules

使用 UIMothodsUIModules,可以使得在模板文件中也可以调用执行 Python 的类和函数。分为以下几个步骤:

  • 定义两个 py 文件,在其中定义好要执行的函数或类
  • app.py 中注册上一步定义的 py 文件
  • 在模板中调用

uimethods.py

def tab(self):
    return 'UIMethod'

uimodules.py

from tornado.web import UIModule
from tornado import escape

class custom(UIModule):

    def render(self, *args, **kwargs):
        return escape.xhtml_escape('<h1>UIModules</h1>')

app.py

#!/usr/bin/env python
# -*- coding:utf-8 -*-

import tornado.ioloop
import tornado.web
import uimethods as mt
import uimodules as md


class MainHandler(tornado.web.RequestHandler):
    def get(self):

        self.render('index.html')

settings = {
    'static_path': 'static',        # 静态文件目录名
    'static_url_prefix': '/static/',  # 静态文件 url 前缀,可以是其他
    'template_path': 'templates',
    'ui_methods': mt,
    'ui_modules': md
}

application = tornado.web.Application([
    (r"/index", MainHandler),
], **settings)


if __name__ == "__main__":
    application.listen(8888)
    tornado.ioloop.IOLoop.instance().start()

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>

</head>
<body>
      <h1>hello</h1>
      <p>{% module custom(123) %}</p>
      <p>{{ tab() }}</p>
</body>
</html>

运行结果如下图:

3. Cookie

1、操作 cookie

  • get_cookie():获取 cookie
  • set_cookie():设置 cookie
  • clear_cookie():去除 cookie,一般用于登出设置
class MainHandler(tornado.web.RequestHandler):
    def get(self):
        if not self.get_cookie("mycookie"):
            self.set_cookie("mycookie", "myvalue")
            self.write("您的Cookie尚未设置!")
        else:
            self.write("你的cookie已经设定好了!")

2、加密 cookie

所谓加密 cookie,即已经签名过防止伪造的 cookie,一般情况我们都会在 cookie 中存储登录用户的 ID 等个人信息。但是 cookie 也容易被恶意客户端伪造,从而攻陷服务端,所以对 cookie 进行必要的加密是很有必要的。

Tornado 通过 set_secure_cookieget_secure_cookie 方法直接支持了这种功能。 要使用这些方法,你需要在创建应用时提供一个密钥,名字为 cookie_secret。 你可以把它作为一个关键词参数传入应用的设置中:

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        if not self.get_secure_cookie("mycookie"):
            self.set_secure_cookie("mycookie", "myvalue")
            self.write("您的Cookie尚未设置!")
        else:
            self.write("你的cookie已经设定好了!")
             
application = tornado.web.Application([
    (r"/", MainHandler),
], cookie_secret="61oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=")	# 提供的密匙

签名 Cookie 的本质是:

写cookie过程:

  • 将值进行base64加密
  • 对除值以外的内容进行签名,哈希算法(无法逆向解析)
  • 拼接 签名 + 加密值

读cookie过程:

  • 读取 签名 + 加密值
  • 对签名进行验证
  • base64解密,获取值内容

app.py

# !/usr/bin/env python
# -*- coding:utf-8 -*-

import tornado.ioloop
import tornado.web

class BaseHandler(tornado.web.RequestHandler):
    def get_current_user(self):
        return self.get_secure_cookie("login_user")		# 获取 cookie


class LoginHandler(BaseHandler):
    def get(self):
        self.render('login.html')

    def post(self):
        username = self.get_argument('user')
        password = self.get_argument('password')
        print(username, password)
        if username == 'rose' and password == '123':
            self.set_secure_cookie('login_user', username)		# 设置 cookie
            self.redirect('/')
        else:
            self.render('login.html', **{'status': '用户名或密码错误'})


class WelcomeHandler(BaseHandler):
    @tornado.web.authenticated		# 用户认证装饰器
    def get(self):
        self.render('index.html', user=self.current_user)


class LogoutHandler(BaseHandler):
    def get(self):
        if (self.get_argument("logout", None)):
            self.clear_cookie("login_user")
            self.redirect("/")


settings = {
    'template_path': 'templates',
    'static_path': 'static',
    'static_url_prefix': '/static/',
    'cookie_secret': 'aiuasdhflashjdfoiuashdfiuh',
    "login_url": "/login",
    "xsrf_cookies": True,		# 相当于 Django 的 csrf_token
}

application = tornado.web.Application([
    (r'/', WelcomeHandler),
    (r"/login", LoginHandler),
    (r"/logout", LogoutHandler),
], **settings)

if __name__ == "__main__":
    application.listen(8888)
    tornado.ioloop.IOLoop.instance().start()

login.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <form action="/login" method="post">
        {% raw xsrf_form_html() %}
        <p><input type="text" name="user"></p>
        <p><input type="password" name="password"></p>
        <p><input type="submit" value="登录"></p>
    </form>
</body>
</html>

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>

</head>
<body>
      <h3>Welcome back, {{ user }}</h3>
</body>
</html>

3.2 Torando 自带认证功能 authenticated 装饰器

authenticated 可以帮助我们认证当前用户是否登录,依赖于 login_urlcurrent_user 两个属性。

  • 当前用户未登录:current_user 为 False,否则为设置的 cookie
  • 对于未登录的用户:会被定位到 login_url 指定的 URL,非法 post 请求将返回 403 Forbidden HTTP 响应

参考文章:tornado系列:用cookie进行用户验证

Cookie 存储在浏览器端,因此也可以使用 JavaScript 操作 Cookie,下面是一个如何设置 Cookie 过期的小示例:

/* 设置cookie,指定秒数过期  */
/* 参数:
domain   指定域名下的cookie
path       域名下指定url中的cookie
secure    https使用
*/

function setCookie(name,value,expires){
    var temp = [];
    var current_date = new Date();
    current_date.setSeconds(current_date.getSeconds() + 5);
    document.cookie = name + "= "+ value +";expires=" + current_date.toUTCString();
}

更多有关于 jQuery 操作 Cookie的方法:jQuery Cookie

4. 跨站请求伪造

Tornado 中跨站请求伪造(CSRF)类似于 Django,不过需要事先在 settings 配置好:

app.py

settings = {
    "xsrf_cookies": True,
}
application = tornado.web.Application([
    (r"/", MainHandler),
    (r"/login", LoginHandler),
], **settings)

使用:index.html

<form action="/login" method="post">
  {{ xsrf_form_html() }}			<!--生成一个这样的 input 标签:<input type="hidden" name="_xsrf" value="2|dde24a6d|ad9cb54babbe1bb9b43f4360867b2ff3|1559549174">-->
  <input type="text" name="user"/>
  <input type="submit" value="提交"/>
</form>

使用 Ajax 发送 post 请求时:

/*获取本地 cookie,再携带 cookie 发送请求*/
function getCookie(name) {
    var r = document.cookie.match("\b" + name + "=([^;]*)\b");
    return r ? r[1] : undefined;
}

jQuery.postJSON = function(url, args, callback) {
    args._xsrf = getCookie("_xsrf");
    $.ajax({url: url, data: $.param(args), dataType: "text", type: "POST",
        success: function(response) {
        callback(eval("(" + response + ")"));
    }});
};

5. 自定义 session

session其实就是定义在服务器端用于保存用户回话的容器,其必须依赖 cookie 才能实现。下面我们来自定义一个内存级别 session,当然也可以将其存储到 reids 中。

有关 cookies 的方法:

get_cookie()        # 获取 cookie
set_cookie()        # 设置 cookie

tornado.web.RequestHandler 构造函数 init() 中有一个 self.initialize 函数,它是一个 Tornado 框架提交的 钩子函数,用于子类初始化,为每个请求调用。

这里为了避免每个类都要定义 initialize(),我们定义了一个 BaseHandler(),要使用的类直接继承即可。

app.py

import tornado.ioloop
import tornado.web
# import tornado
from session import Session


class BaseHandler:
    def initialize(self):
        self.session = Session(self)

        super(BaseHandler, self).initialize()


class MainHandler(BaseHandler, tornado.web.RequestHandler):

    def get(self, *args, **kwargs):
        temp = self.session.get_value('is_login')
        if temp:
            self.write("Hello, world")
        else:
            self.redirect('/login')


class LoginHandler(BaseHandler, tornado.web.RequestHandler):
    """
    LoginHandler  和 MainHandler 都没有 init 构造方法,那就从它的父类中找
    它的父类中有一个 initialize() 方法,是一个钩子函数
    用于子类初始化,为每个请求调用,因此在执行调用这两个类时,会事先调用执行 initialize() 方法
    我们可以用来获取或设置用户的 cookies
    """
    def get(self, *args, **kwargs):

        self.render('login_session.html')

    def post(self, *args, **kwargs):
        user = self.get_body_argument('user')       # rose/123  获取表单中的值
        pwd = self.get_body_argument('password')
        print(user, pwd)

        if user == 'root' and pwd == '123':
            print(self.session.get_value('is_login'))
            self.session.set_value('is_login', True)
            self.redirect('/index')      # 重定向
        else:
            self.redirect('/login')

# 配置静态文件和模板文件
settings = {
    'static_path': 'static',        # 静态文件目录名
    'static_url_prefix': '/static/',  # 静态文件 url 前缀,可以是其他
    'template_path': 'templates',

}


# 生成路由规则
application = tornado.web.Application([
    (r"/index", MainHandler),
    (r'/login', LoginHandler),
], **settings)


if __name__ == "__main__":
    # 产生 socket 对象
    # 并将 socket 对象添加到 select 或 epoll(Linux)中,进行监听
    application.listen(8888)        # listen() 方法还可以指定 ip 和端口

    # 无限循环监听 socket 对象文件句柄是否发生变化
    tornado.ioloop.IOLoop.instance().start()

session.py

import uuid

# 存放 session

# container = {
#     '97470544-215a-4837-b904-eeee4b6974fa': {'is_login': True},
#     '随机字符串2': {'xxx': 'xxxxxxx'},
# }


class Session(object):
    container = {}

    def __init__(self, handler):
        """
        获取或设置用户 session
        :param handler: 为调用 Session() 的类的实例对象,如:LoginHandler() 的实例对象
        """
        # self.container = {}

        # 获取 cookie
        nid = handler.get_cookie('session_id')     # 相当于 self.get_cookie()
        print(Session.container)
        print('nid>>>>>>>>>>>>', nid)
        if nid:
            if nid in Session.container:
                pass
            else:
                nid = str(uuid.uuid4())
                Session.container[nid] = {}
        else:
            nid = str(uuid.uuid4())
            Session.container[nid] = {}

        handler.set_cookie('session_id', nid, max_age=1000)

        self.nid = nid
        self.handler = handler

    def set_value(self, key, value):
        """
        设置 session
        :param key:
        :param value:
        :return:
        """
        Session.container[self.nid][key] = value       # container['97470544-215a-4837-b904-eeee4b6974fa']['is_login] = True

    def get_value(self, key):
        """
        获取 session
        :param key:
        :return:
        """
        # print(self.container)
        return Session.container[self.nid].get(key)        # container['97470544-215a-4837-b904-eeee4b6974fa'].get('is_login)


使用 __getitem__、__setitem__、__delitem__ 进行改造

在上面 Session() 我们定义了两个函数 get_key()set_value() 来获取和设置 cookie。下面来看看我们自定义的 session 框架和 Django 内置的有什么区别:

# 自定制
self.session.get_value('is_login')
self.session.set_value('is_login', True)

# Django
sesiion['is_login'] = True
session.get('is_login')

要想实现和 Django 一样的效果,就需要用到面向对象中的 __getitem__、__setitem__、__delitem__ 三个方法,现在我们来修改下Session`:

def __getitem__(self, item):

    return Session.container[self.nid].get(item)

def __setitem__(self, key, value):
    Session.container[self.nid][key] = value

def __delitem__(self, key):
    del Session.container[self.nid][key]

将原来的 get_value()、set_value() 换成上面三个类的内置方法:

  • 当类对象进行获取操作时会触发 __getitem__
  • 当类对象进行修改操作时会触发 __setitem__
  • 当类对象进行删除操作时会触发 __delitem__

下面再来修改下 index.py,改变其获取设置 cookie 的方式:

class MainHandler(BaseHandler, tornado.web.RequestHandler):

    def get(self, *args, **kwargs):
        # temp = self.session.get_value('is_login')

        # 修改为这句
        temp = self.session['is_login'] 
        if temp:
            self.write("Hello, world")
        else:
            self.redirect('/login')



class LoginHandler(BaseHandler, tornado.web.RequestHandler):
    
    ...
   
    def post(self, *args, **kwargs):
       ...

        if user == 'root' and pwd == '123':
            # print(self.session.get_value('is_login'))
            # self.session.set_value('is_login', True)
           
           # 修改为以下这句
           self.session['is_login'] = True
            self.redirect('/index')      # 重定向
        else:
            self.redirect('/login')


将 session 存储到 redis 中

import uuid
import redis


class RedisSession(object):
   # container = {
       #  '97470544-215a-4837-b904-eeee4b6974fa': {'is_login': True},
        #     '随机字符串2': {'xxx': 'xxxxxxx'},
   # }

    def __init__(self, handler):
        # 获取用户cookie,如果有,不操作,否则,给用户生成随即字符串
        # 写给用户
        # 保存在session
        r = redis.StrictRedis(host='192.168.21.128', port=6379, db=0)
        keys = r.keys()


        nid = handler.get_cookie('session_id')      # 465e0198-9ce5-4ae4-9768-d6d4803e7b86
        if nid:
            if nid in keys:
                pass
            else:
                nid = str(uuid.uuid4())
                r.hset(nid, 'is_login', 1)
        else:
            nid = str(uuid.uuid4())
            r.hset(nid, 'is_login', 1)

        handler.set_cookie('session_id', nid, max_age=1000)
        # nid当前访问用户的随即字符串
        self.nid = nid
        # 封装了所有用户请求信息
        self.handler = handler

    def __getitem__(self, item):

        return RedisSession.container[self.nid].get(item)

    def __setitem__(self, key, value):
        RedisSession.container[self.nid][key] = value

    def __delitem__(self, key):
        del RedisSession.container[self.nid][key]

6. 自定制 Form 表单验证

学习过 Django 的朋友应该都知道,Django 内置有 Form 表单组件,可以用来生成表单,表单验证等等。而 tornado 没有自带 Form 表单验证,需要我们自定制。

下面我们来模拟自定制一个简单的 Form 表单验证;

  • 在这里只定制 StringFieldEmailField 两种类型字段
  • 表单类型只有最简单的 input 文本框,当然还有更多的,如:select、textarea、checkbox 等,就需要做更多的定制

app.py

import tornado.ioloop
import tornado.web
import re


class StringField:
    def __init__(self, name):
        self.regex = '^w+$'
        self.name = name
        self.error = ''
        self.value = ''

    def __str__(self):
        return "<input type='text' name='%s' value='%s'>" % (self.name, self.value)


class EmailField:
    def __init__(self, name):
        self.regex = '^w+@.*$'
        self.name = name
        self.error = ''
        self.value = ''     # 编辑/修改表单时,保留原有值

    def __str__(self):
        return "<input type='text' name='%s' value='%s'>" % (self.name, self.value)


class LoginForm:
    def __init__(self):
        self.user = StringField(name='user')
        self.email = EmailField(name='email')

    def is_valid(self, handler):
        cleaned_data = {}
        flag = True

        for k, v in self.__dict__.items():
            # k=user, v=<__main__.StringField object at 0x000001EC6BE33E80>
            # k=email v=  <__main__.EmailField object at 0x000001EC6BE33EB8>
            # user ^w+$、email ^w+@.*$

            temp = handler.get_body_argument(k)
            v.value = temp      # 赋值

            result = re.match(v.regex, temp)
            if result:
                cleaned_data[k] = temp
            else:
                v.error = '%s 错误' % k
                flag = False

        return flag, cleaned_data           # True {'user': 'rose', 'email': 'john@qq.com'}


class LoginHandler(BaseHandler, tornado.web.RequestHandler):

    def get(self, *args, **kwargs):
        obj = LoginForm()

        self.render('login.html', **{'obj': obj})

    def post(self, *args, **kwargs):
        obj = LoginForm()
        obj.is_valid(self)

        flag, cleaned_data = obj.is_valid(self)
        if flag:
            user = cleaned_data.get('user')
            email = cleaned_data.get('email')
            
            if user == 'root' and email== '123@qq.com':
                self.redirect('/index')      # 重定向
        else:
            self.render('login.html', **{'obj': obj})

前端 login.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" href="/static/css/test.css">
</head>
<body>
<h1>Tornado 框架</h1>

<form action="/login" method="post">
    <p>{% raw obj.user %} {{ obj.user.error }}</p>
    <p>{% raw obj.email %} {{ obj.email.error }}</p>

    <input type="submit" value="提交">
</form>
</body>
</html>

Tips: 千万不能忘记 raw !!!


用到知识点

  • __str__:用来定制对象字符串显示形式
  • __dict__:类、类对象属性字典
  • 基于Python实现的支持多个WEB框架的 Form表单验证组件:Tyrion中文文档(含示例源码)

7. 上传文件

7.1 Form 表单上传

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>

</head>
<body>
     <form id="my_form" name="form" action="/index" method="POST"  enctype="multipart/form-data" >
        <input name="file" id="my_file"  type="file" />
        <input type="submit" value="提交"  />
    </form>
</body>
</html>

app.py

#!/usr/bin/env python
# -*- coding:utf-8 -*-

import tornado.ioloop
import tornado.web


class MainHandler(tornado.web.RequestHandler):
    def get(self):

        self.render('index.html')

    def post(self, *args, **kwargs):
        file_metas = self.request.files["file"]
        print(file_metas)
        """file_metas = 
         [{'filename': '1.jpg', 'body': b'xffxd8xffxe0x00x10JFIFx00x01...x1fxffxd9', 
        'content_type': 'image/jpeg'}]

        """
        for meta in file_metas:
            file_name = meta['filename']
            with open(file_name, 'wb') as up:
                up.write(meta['body'])

settings = {
    'template_path': 'templates',
}

application = tornado.web.Application([
    (r"/index", MainHandler),
], **settings)


if __name__ == "__main__":
    application.listen(8000)
    tornado.ioloop.IOLoop.instance().start()

总结

request.files["name"]:获取上传文件对象(保存有文件名、文件二进制信息、文件类型等信息)

7.2 Ajax 上传

1、XMLHttpRequest

<input type="file" id="img" />
<input type="button" onclick="UploadFile();" />
<script>
    function UploadFile(){
        var fileObj = document.getElementById("img").files[0];

        var form = new FormData();
        form.append("k1", "v1");
        form.append("fff", fileObj);

        var xhr = new XMLHttpRequest();
        xhr.open("post", '/index', true);
        xhr.send(form);
    }
</script>

2、iframe

<form id="my_form" name="form" action="/index" method="POST"  enctype="multipart/form-data" >
    <div id="main">
        <input name="fff" id="my_file"  type="file" />
        <input type="button" name="action" value="Upload" onclick="redirect()"/>
        <iframe id='my_iframe' name='my_iframe' src=""  class="hide"></iframe>
    </div>
</form>

<script>
    function redirect(){
        document.getElementById('my_iframe').onload = Testt;
        document.getElementById('my_form').target = 'my_iframe';
        document.getElementById('my_form').submit();

    }

    function Testt(ths){
        var t = $("#my_iframe").contents().find("body").text();
        console.log(t);
    }
</script>

3、jQuery

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>

</head>
<body>
<input type="file" id="img"/>
<input type="button" onclick="UploadFile();"/>


<script src="{{ static_url('js/jquery-3.1.1.js') }}"></script>

<script>
    function UploadFile() {
        var fileObj = $("#img")[0].files[0];
//        console.log($("#img")[0].files[0]);
        var form = new FormData();
        form.append("k1", "v1");
        form.append("fff", fileObj);

        $.ajax({
            type: 'POST',
            url: '/index',
            data: form,
            processData: false,  // tell jQuery not to process the data
            contentType: false,  // tell jQuery not to set contentType
            success: function (arg) {
                console.log(arg);
            }
        })
    }
</script>
</body>
</html>

4、app.py

#!/usr/bin/env python
# -*- coding:utf-8 -*-

import tornado.ioloop
import tornado.web


class MainHandler(tornado.web.RequestHandler):
    def get(self):

        self.render('index.html')

    def post(self, *args, **kwargs):
        file_metas = self.request.files["fff"]
        # print(file_metas)
        for meta in file_metas:
            file_name = meta['filename']
            with open(file_name,'wb') as up:
                up.write(meta['body'])

settings = {
    'static_path': 'static',
    'static_url_prefix': '/static/',
    'template_path': 'templates',
}

application = tornado.web.Application([
    (r"/index", MainHandler),
], **settings)


if __name__ == "__main__":
    application.listen(8000)
    tornado.ioloop.IOLoop.instance().start()

8. 异步非阻塞

Tornado 不仅仅是个同步 Web 框架,同时也是一个非常有名的异步非阻塞框架(与 Node.js 一样),下面我们就探讨下如何基本使用异步非阻塞。

8.1 基本使用

#!/usr/bin/env python
# -*- coding:utf-8 -*-

import tornado.ioloop
import tornado.web
from tornado import gen
from tornado.concurrent import Future


class AsyncHandler(tornado.web.RequestHandler):
    @gen.coroutine
    def get(self):
        future = Future()
        future.add_done_callback(self.doing)	# 请求成功,回调函数
        yield future
        # 或
        # tornado.ioloop.IOLoop.current().add_future(future,self.doing)
        # yield future

    def doing(self, *args, **kwargs):
        self.write('async')
        self.finish()

application = tornado.web.Application([
    (r"/index", AsyncHandler),
])


if __name__ == "__main__":
    application.listen(8000)
    tornado.ioloop.IOLoop.instance().start()

访问:http://127.0.0.1:8000/index 时发现页面一直在转动,并未请求成功,连接也未断开。这是因为处理 get 请求的 函数被 @gen.coroutine 装饰(内部是一个协程),且 yield 了一个 Future 对象。该对象只有用户向其发送信号或者放置数据时,才会 “放行”,不然会一直等待。

8.2 同步与异步非阻塞对比

同步:

class SyncHandler(tornado.web.RequestHandler):

    def get(self):
        self.doing()
        self.write('sync')

    def doing(self):
        time.sleep(10)

异步:

class AsyncHandler(tornado.web.RequestHandler):
    @gen.coroutine
    def get(self):
        future = Future()
        tornado.ioloop.IOLoop.current().add_timeout(time.time() + 5, self.doing)
        yield future


    def doing(self, *args, **kwargs):
        self.write('async')
        self.finish()

8.3 httpclient类库

使用 httpclient 类库用于发送 HTTP 请求,当浏览器向 Tornado 服务端发送请求时,其内部也会向别的地址发送 HTTP 请求。什么时候请求回来,就什么时候响应浏览器发送的请求。

浏览器访问:http://127.0.0.1:8888/async,向服务器发起 get 请求,服务器内部通过 httpclient 向 Google 发送请求:

import tornado.web
import tornado.ioloop
from tornado import gen
from tornado import httpclient


# 方式一:
class AsyncHandler(tornado.web.RequestHandler):
    @gen.coroutine
    def get(self, *args, **kwargs):
        http = httpclient.AsyncHTTPClient()
        data = yield http.fetch("http://www.google.com")
        print('finished...', data)
        self.finish('请求成功~')


# 方式二:
# class AsyncHandler(tornado.web.RequestHandler):
#     @gen.coroutine
#     def get(self):
#         http = httpclient.AsyncHTTPClient()
#         yield http.fetch("http://www.google.com", self.done)
#
#     def done(self, response):
#         print('finished...')
#         self.finish('666')



application = tornado.web.Application([
    (r"/async", AsyncHandler),
])

if __name__ == "__main__":
    application.listen(8888)
    tornado.ioloop.IOLoop.instance().start()

因为国内无法正常访问:http://www.google.com,所有一段时间后,得到:500: Internal Server Error 的结果。

原文地址:https://www.cnblogs.com/midworld/p/11076015.html