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.RequestHandler
的 self.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 也支持模板继承,通过
extends
和block
实现 - 跨站请求伪造:
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 中转义
后台带有 <、>
传到前端模板中会被转义为:<、>
等,要想不被转义,而输出的是原始字符串,可以使用以下三种方法:
- 使用
{% 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 模板继承,也是使用 extends
和 block
来实现模板继承:
母版 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
使用 UIMothods
和 UIModules
,可以使得在模板文件中也可以调用执行 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_cookie
和 get_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解密,获取值内容
3.1 基于 Cookie 实现用户登录认证
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_url
和 current_user
两个属性。
- 当前用户未登录:
current_user
为 False,否则为设置的cookie
- 对于未登录的用户:会被定位到
login_url
指定的 URL,非法 post 请求将返回 403 Forbidden HTTP 响应
3.3 通过 JavaScript 操作 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 表单验证;
- 在这里只定制
StringField
和EmailField
两种类型字段 - 表单类型只有最简单的
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'\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01...\x1f\xff\xd9',
'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
的结果。