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解密,获取值内容

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_urlcurrent_user 两个属性。

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

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

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 表单验证;

  • 在这里只定制 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'\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 的结果。

Torando 入门的更多相关文章

  1. Angular2入门系列教程7-HTTP(一)-使用Angular2自带的http进行网络请求

    上一篇:Angular2入门系列教程6-路由(二)-使用多层级路由并在在路由中传递复杂参数 感觉这篇不是很好写,因为涉及到网络请求,如果采用真实的网络请求,这个例子大家拿到手估计还要自己写一个web ...

  2. ABP入门系列(1)——学习Abp框架之实操演练

    作为.Net工地搬砖长工一名,一直致力于挖坑(Bug)填坑(Debug),但技术却不见长进.也曾热情于新技术的学习,憧憬过成为技术大拿.从前端到后端,从bootstrap到javascript,从py ...

  3. Oracle分析函数入门

    一.Oracle分析函数入门 分析函数是什么?分析函数是Oracle专门用于解决复杂报表统计需求的功能强大的函数,它可以在数据中进行分组然后计算基于组的某种统计值,并且每一组的每一行都可以返回一个统计 ...

  4. Angular2入门系列教程6-路由(二)-使用多层级路由并在在路由中传递复杂参数

    上一篇:Angular2入门系列教程5-路由(一)-使用简单的路由并在在路由中传递参数 之前介绍了简单的路由以及传参,这篇文章我们将要学习复杂一些的路由以及传递其他附加参数.一个好的路由系统可以使我们 ...

  5. Angular2入门系列教程5-路由(一)-使用简单的路由并在在路由中传递参数

    上一篇:Angular2入门系列教程-服务 上一篇文章我们将Angular2的数据服务分离出来,学习了Angular2的依赖注入,这篇文章我们将要学习Angualr2的路由 为了编写样式方便,我们这篇 ...

  6. Angular2入门系列教程4-服务

    上一篇文章 Angular2入门系列教程-多个组件,主从关系 在编程中,我们通常会将数据提供单独分离出来,以免在编写程序的过程中反复复制粘贴数据请求的代码 Angular2中提供了依赖注入的概念,使得 ...

  7. wepack+sass+vue 入门教程(三)

    十一.安装sass文件转换为css需要的相关依赖包 npm install --save-dev sass-loader style-loader css-loader loader的作用是辅助web ...

  8. wepack+sass+vue 入门教程(二)

    六.新建webpack配置文件 webpack.config.js 文件整体框架内容如下,后续会详细说明每个配置项的配置 webpack.config.js直接放在项目demo目录下 module.e ...

  9. wepack+sass+vue 入门教程(一)

    一.安装node.js node.js是基础,必须先安装.而且最新版的node.js,已经集成了npm. 下载地址 node安装,一路按默认即可. 二.全局安装webpack npm install ...

随机推荐

  1. ELK初步指南

    ELK的简单科普文章,加入了自己的一些理解. 内容包括ELK的基本介绍, 应用场景, 架构设计, 监控及自监控, 业界进展及推荐资料等. 用户故事 场景一 作为一个运维工程师, 某天虚拟机出现故障, ...

  2. JavaWeb -- Session应用实例 -- 随机中文验证码 检验

    注册页面 login.html <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE html ...

  3. Xcode 中代码提示不显示

    解决办法: Xcode->Window->Organizer->Projects选中你的项目,点击如下图Derived Data右侧的Delete按钮 DerivedData从字面上 ...

  4. java.sql.SQLException: Column count doesn't match value count at row 1 Query: insert into category values(null,?,?,?) Parameters: [1111111, 1111, 软件]

    java.sql.SQLException 问题: java.sql.SQLException: Column count doesn't match value count at row 1 Que ...

  5. C#winform拖拽实现获得文件路径

    1.关键知识点说明: 通过DragEnter事件获得被拖入窗口的“信息”(可以是若干文件,一些文字等等),在DragDrop事件中对“信息”进行解析.窗体的AllowDrop属性必须设置成true;且 ...

  6. Hibernate映射--基本类映射和对象关系映射(转)

    原文地址:http://blog.csdn.net/lovesummerforever/article/details/20901011   尊重原创,请访问原网址 回想一些我们在没有学习ssh的时候 ...

  7. 谈MicroMessageTest的开始创建

    一开始,创建一个可以看到的jsp前端页面. 只不过不是用纯jsp页面访问,而是用Servlet doGet跳转至jsp页面,req.getRequestDispatcher(jsp页面的全称 还是全地 ...

  8. OPcache

    1.介绍 OPcache 通过将 PHP 脚本预编译的字节码存储到共享内存中来提升 PHP 的性能, 存储预编译字节码的好处就是 省去了每次加载和解析 PHP 脚本的开销 2.配置 2.1 opcac ...

  9. C语言小程序(四)、杨辉三角

    输入要显示的杨辉三角的行数,会打印出金字塔型的杨辉三角,不过行数太多的话,效果不太好,可以再调整一下格式控制. #include <stdio.h> #include <stdlib ...

  10. Qt Quick中的信号与槽

    在QML中,在Qt Quick中,要想妥善地处理各种事件,肯定离不开信号与槽,本博的主要内容就是整理Qt 中的信号与槽的内容. 1. 链接QML类型的已知信号 QML中已有类型定义的信号分为两类:一类 ...