关于Tornado框架的个人学习心得。因为在学习Tornado前已经学习了Django。所以在这里许多知识点我没有特别详细的说明并且会跟与django做对比去学习!
Tornado框架
Tornado其实是一个十分轻量级的web服务器框架,组件十分的少,学习起来十分的轻松简单。因为tornado提供的开发功能并不强大,所以许多web开发中常用的功能组件都需要自己去写而不像django一样提供许多功能强大的组件直接供我们调用(如ORM、FROM表单验证和Session等等)。而Tornado最强大的一点是它的异步非阻塞功能!
所以本篇除了介绍它的基本操作外,还会自定义一些在web开发中常常需要用到的组件。而对于异步非阻塞的功能我在进阶篇中再去描述自己的一些愚见
安装:pip install tornado
基本操作
web服务器框架的基本操作简单说就是由路由系统接收用户发来的请求,并把请求转发给视图函数(C)处理,在视图处理过程中可能会涉及调用数据库操作(M),然后由视图函数交给模板引擎(V)渲染最后返回给用户。这就是web应用非常流行的MVC设计模式。学习tornado我们也是需要从这几个功能入手学习!
快速上手
先简单的快速上手Tornado的使用,使用Tornado大致上就是执行处理以下这些事情:
- 第一步:执行脚本,监听 8888 端口
- 第二步:浏览器客户端访问 /index --> http://127.0.0.1:8888/index
- 第三步:服务器接受请求,并交由对应的类处理该请求
- 第四步:类接受到请求之后,根据请求方式(post / get / delete ...)的不同调用并执行相应的方法
- 第五步:方法返回值的字符串内容发送浏览器
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import tornado.ioloop
import tornado.web
class MainHandler(tornado.web.RequestHandler): # 视图类
def get(self):
self.write("Hello, world") #相当于diango的return Httpresponse,即返回响应
#self.render('模板名') #去指定的模板路径去读取模板返回给前端
#self.redirect('URI') #跳转
settings = {
"template_path":'views',
} # 配置模板的路径(默认从当前执行文件目录去找,但是我们一般把模板放在views目录中)
application = tornado.web.Application([
(r"/index", MainHandler), # 路由映射(url-->某个类)
],**settings) #把配置文件放到里面
if __name__ == "__main__":
application.listen(8888)
tornado.ioloop.IOLoop.instance().start() # 把socket启动起来,监听请求
如果我们把所有的东西都写在一个.py文件中,业务量大的话就显得十分杂乱,所以我们可以灵性的把功能分到不同的目录中去!
每个目录放不同的py文件:
- controllers负责业务处理,放视图类的.py
- models负责数据库操作,放操作数据库.py
- views负责放我们的模板(一般都是HTML文件,就是我们django的template)
路由系统
路由系统其实就是 url 和"类“的对应关系,这里不同于其他框架,其他很多框架均是 url对应函数,Tornado中每个url对应的是一个类。 这个类就是视图,在MVC中也叫控制器,它是负责处理业务的模块!
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import tornado.ioloop
import tornado.web
"""
路由系统:
url --> 类 (根据method执行不同的方法)
"""
class MainHandler(tornado.web.RequestHandler):
def get(self):
self.write("Hello, world")
#self.render('模板名') #去指定的模板路径去读取模板返回给前端
#self.redirect('URI') #跳转
class LoginHandler(tornado.web.RequestHandler):
#登陆功能控制器(视图)
def get(self, *args,**kwagrs):
# get请求执行该方法
self.render('login.html',msg='')
def post(self,*args,**kwargs):
# post请求执行该方法
username = self.get_agrument('user') #get和post里面都去取
password = self.get_argument('pwd')
if username == "root" and password == '123':
self.set_cookie('is_login','Ture') #设置cookie
self.redireact('https://deehuang.github.io/')
else:
# 渲染模板并把msg传到模板中(可以传字典)
self.render('login.html',msg='用户名或密码错误')
class HomeHandler(tornado.web.RequestHandler):
#主页功能控制器(视图)
def get(self,*args,**kwargs):
login_staus = self.get_cookie('is_login') #获取cookie
if not login_staus:
self.redirect('/login')
return #如果不return函数会继续往下执行
self.write('欢迎登陆')
application = tornado.web.Application([
(r"/index", MainHandler),
(r"/login", LoginHandler),
(r"/home", HomeHandler),
])
if __name__ == "__main__":
application.listen(80)
tornado.ioloop.IOLoop.instance().start()
请求到视图类中会根据请求方式去执行不同的类方法,例如post请求去执行的就是post方法(注意:类中的方法是小写的)
取请求的内容一般会用self.get_argument()
方法。它是get和post方式都去取数据
如果要单独取get请求传来的数据可以使用self.get_query_argument()
如果要单独取post请求传来的数据可以使用self.get_body_argument()
如果要取全部的键值对只需要在使用self.get_arguments()
即可(同适用于get和post方式取值)
如果要取文件内容可以使用self.request.files['文件名']
其实用户请求的所有信息都封装在了控制器对象中的reques对象(self.request
)中,上面的get_argument等等方法内部其实都是去request中去取值!
总结控制器中常用的请求相关的方法:
"""控制器:"""
class Foo(tornado.web.RequestHandler):
def get(self):
self.render() #渲染模板,返回模板响应
self.write() #返回响应
self.redirect() #跳转
self.get_argument() #取请求内容(get&post都取)
self.get_arguments()
self.get_cookie() #获取cookie
self.set_cookie() #设置cookie
self.get_secure_cookie() #获取加密的cookie(依赖配置文件)
self.set_secure_cookie() #设置加密的cookie(依赖配置文件)
self.request.files['filename'] #获取上传的文件
self._headers #获取请求头信息
"""取文件内容的操作:"""
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']) #meta['body']拿到文件内容
补充:加密的cookie(依赖配置文件)
- 设置配置文件,需要添加一个随机字符串密钥(用来给cookie安全加密)
- 设置cookie的时候使用
self.set_secure_cookie()
- 取cookie的时候使用
self.get_secure_cookie()
实际上对于加密cookie内部实现的本质是:
写cookie过程:
- 将值进行base64加密
- 对除值以外的内容进行签名,哈希算法(无法逆向解析)
- 拼接 签名 + 加密值
读cookie过程:
- 读取 签名 + 加密值
- 对签名进行验证
- base64解密,获取值内容
如果还想获取更多请求的信息可以遵循这样的寻找顺序:
控制器对象(可以去父类tornado.web.RequestHandler
里面去找)
控制器对象中找不到就去self.request
去找(它的类型是tornado.httputil.HTTPSeverRequest
)
PS:寻找信息主要去它们的构造方法__init__()
里面去找就行了
模板
请求首先到路由系统,根据路由匹配分发到不同的控制器中去处理业务。控制器处理完业务后一般返回三个结果:
redirect()
、write()
和render()
redirect就是跳转,write()就是直接写返回给浏览器。两个功能都十分简单明了
而render较为复杂因为它做的就是模板引擎的渲染!就是这里要介绍的模板!
Tornao中的模板语言和django中类似,模板引擎将模板文件载入内存,然后将数据嵌入其中,最终获取到一个完整的字符串,再将字符串返回给请求者。
Tornado 的模板支持“控制语句”和“表达语句”:
控制语句是使用{% 和 %}包起来的 例如 {% if len(items) > 2 %}。
表达语句是使用{{ 和 }}包起来的,例如 {{ items[0] }}。
控制语句和对应的Python语句的格式基本完全相同。我们支持 if、for、while和try,这些语句逻辑结束的位置需要用{% end %}做标记。还通过extends和block 语句实现了模板继承。这些在 [template模块](http://github.com/facebook/tornado/blob/master/tornado/template.py)的代码文档中有着详细的描述。
"""Tornado常用的模板语法"""
{{ li[0] }} #索引取值
{% for i in range(10) %} {% end %} #循环语句
#继承
{% block CSS %}{% end %} #母版
{% extends '母版名'} {% block CSS %}{% end %} #子版 需要先在文件头使用extends标签引入母版
在模板中默认提供了一些函数、字段、类以供模板使用:
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 的別名
PS:Tornado的循环控制语句等都统一以{% end %}标签结尾,与django不太一样
模板语言还支持自定制拓展方法和类,它是通过UIMethod和UIModule这两个组件去实现的( 这两个组件类似于Django的simple_tag )。
其实通过UIMethod定制方法已经能实现所有的我们想生成的内容了,那么UIModule定制类存在的意义是什么呢?UIModule除了和UIMethod一样可以帮我们生成内容还可以帮我们添加CSS,JS。
下面来看下它们的实现方式:
-
定义:
-
定制方法:
#模块名为uimethods.py def tab(request): # 默认会传入HTTPSeverRequest 即默认将请求的信息传过来了 return 'UIMethod'
-
定制类:
"""模块名为uimodules""" from tornado.web import UIModule from tornado import escape #转义模块 class custom(UIModule): def css_files(self): #导入css文件,在页面使用链接式引入css文件 #默认会从静态文件夹static中去找(静态文件需在settings配置) return '文件路径' def embedded_css(self): #嵌入CSS #在页面<head>自动加入<style type='text/css'> 并把返回结果写入其中 return '.c1{display:None}' def javascript_files(self): #导入js文件,页面生成<script src='/static/xxx'> #默认会从静态文件夹static中去找(静态文件需在settings配置) return "文件路径" def embedded_javascript(self): #嵌入js #页面会生成<script>并把返回内容写到其中 return 'js代码' def render(self, *args, **kwargs): #模板直接调用类就会执行render方法 return escape.xhtml_escape('<h1>deehuang</h1>') #把要传入前端的html字符串进行转义 #使得后端渲染到模板的html字符串不能被浏览器渲染(xss防御机制)
*Ps:UIMethod中如果return的是一个html标签,tornado内部会自动帮我们转义为字符串到浏览器显示而不是渲染该标签。如果我们想关掉自动转义功能需要在settings里面设置
autoescape:None
。但实际运用中不允许这样使用(会遭到xss攻击),所以我们只能在前端模板中使用raw标签告诉模板引擎这句是不转义的:{% raw tab() %}
而UIModules中是不会自动转义的,需要我们使用escape方法来对要传入前端的html字符串进行转义
-
-
注册:在配置文件中settings中把定制的方法或类模块放进去,这样Tornado才找得到
import uimodules as md import uimethods as mt settings = { 'template_path': 'template', 'static_path': 'static', 'static_url_prefix': '/static/', 'ui_methods': mt, 'ui_modules': md, } application = tornado.web.Application([ (r"/index", MainHandler), ], **settings)
-
使用
{% module Custom(123) %} # 定制的类 {{ tab() }} #定制的方法
拓展:模板引擎内部是怎么实现的?
对于模板语言的整个流程,其本质就是处理html文件内容将html文件内容转换成函数,然后为该函数提供全局变量环境(即:我们想要嵌套进html中的值和框架自带的值),再之后执行该函数从而获取到处理后的结果,再再之后则执行UI_Modules继续丰富返回结果,例如:添加js文件、添加js内容块、添加css文件、添加css内容块、在body内容第一行插入数据、在body内容最后一样插入数据,最终,通过soekct客户端对象将处理之后的返回结果(字符串)响应给请求用户。
实用功能
静态文件
对于静态文件,可以配置静态文件的目录和前端使用时的前缀
settings = {
'template_path': 'template',
'static_path': 'static',
#前端模板使用使的前缀 模板标签{{ static_url('模块名') }}就会从/static/模块名去找
'static_url_prefix': '/static/',
}
application = tornado.web.Application([
(r"/index", MainHandler),
], **settings)
csrf
Tornado中的跨站请求伪造和Django中的相似
"""设置配置文件"""
settings = {
"xsrf_cookies": True,
}
application = tornado.web.Application([
(r"/", MainHandler),
(r"/login", LoginHandler),
], **settings)
""" 前端模板:在表单中使用 """
<form action="/new_message" method="post">
{{ xsrf_form_html() }}
<input type="text" name="message"/>
<input type="submit" value="Post"/>
</form>
"""前端模板 在ajax中使用:本质上就是去获取本地的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 + ")"));
}});
};
拓展功能之自定义Session
在这里我们希望自定制一个session组件来方便我们在tornado的控制器中处理业务的时候可以实现对session的快速调用存储。
-
知识储备:
-
关于多继承:
super是自动按照顺序查找(深度优先)
如果不想按顺序查找可以使用类名.方法名(self)
的方式去指定执行某个父类的属性方法
有个老生常谈的问题self是谁?self永远是调用方法的对象 -
关于面向对象:
class Foo(object): def __getitem__(self, key): print '__getitem__',key def __setitem__(self, key, value): print '__setitem__',key,value def __delitem__(self, key): print '__delitem__',key obj = Foo() result = obj['k1'] # 对象['key']会触发__getitem__ #obj['k2'] = 'deehuang' # 对象['key']='xxx'会触发__setitem__ #del obj['k1'] #触发__delitem__
-
Tornado控制器中的钩子(Hook)
在控制器中,提供了一个我们可以自定义拓展操作的钩子函数 initialize。请求过来后首先会实例化我们的控制器对象,实例化的时候就会执行钩子函数initialize方法(因为在实例化过程执行构造方法的最后调用了钩子)。最后再去根据请求方式执行相应的函数。class MainHandler(tornado.web.RequestHandler): def initialize(self): self.A = 123 def get(self): print(self.A) self.write('Hello world')
"""结合多继承的知识使用,完成对控制器拓展操作""" class Foo(tornado.web.RequestHandler): def initalize(self): self.A = 123 super(Foo,self).initialize() #使用super顺序调用其他父类不影响后面类的定制方法 class MainHandler(Foo): def get(self): print(self.A) self.write('Hello world')
-
-
session的实现机制:
-
生成随机字符串
-
写入用户Cookie
-
后台存储
-
在控制器中
self.session['xx']
实现session机制,就需要用到上面说到的知识储备了!
-
-
session框架
import tornado.ioloop import tornado.web import hashlib import os, time """ session格式: #一个随机字符串代表的是一个用户 #每个用户都对应一个值,它是一个字典用以存储用户的信息(在这里叫做用户) { '每个用户都有一个随机字符串': {'uesr':deehuang,is_login:ture} } """ class Cache(object): """ session可以储存在内存,数据库,硬盘或缓存中,使用构造配置文件来让用户可以指定存储方式 Cache表示将session保存在内存中 """ def __init__(self): self.session_container = {} def __contanins__(self,item): """使用 x in obj 语法会触发该函数,用来判断sessionid是否在该容器里面""" return item in self.session_container def initial(self,random_str): """用来给每个用户的sessionid 创建字典""" self.session_container[random_str] = {} def open(self): """ 存储在内存的话不需要用到open 这里只是作规范,因为如果储存在文件或者数据库的话都要涉及打开或者连接操作 """ pass def close(self): """ 存储在内存的话不需要用到close 这里只是作规范,因为如果储存在文件或者数据库的话都要涉及打开或者连接操作 """ pass def get(self,random_str,key): #获取用户字典信息 #避免取值的时候字典中没有匹配的键会报错,使用get方法去字典取值(不存在返回None) self.session_container[random_str].get(key) def set(self,key,random_str,value): #设置用户信息 self.session_container[random_str][key] = value def delete(self,random_str,key): #删除用户字典中的某一键值对 del self.session_container[random_str][key] def clear(self,random_str): # 清除用户的session字典 del self.session_container[random_str] class File(object): """session存储在文件,方法同上规范化""" pass class Memcache(object): """session存储在缓存,方法同上规范化""" pass class DataBase(object): """session存储在数据库,方法同上规范化""" pass P = Cache #配置文件,表示的是session的存储方式 class Session(object): def __init__(self, handler): # 从钩子函数传过来的控制器对象中有设置和获取cookie的方法 self.handler = handler self.random_str = None self.ppp = P() # 储存session的容器 self.ppp.open() #去用户请求传来的cookie中获取sessionid client_random_str = self.handler.get_cookie('session_id') if not client_random_str: #如果没有sessionid:表示是新用户 则给它创建一个随机字符串(sessionid) self.random_str = self.create_random_str() self.ppp.initial(self.random_str) #创建用户字典 else: #有sessionid去session中去匹配用户然后将信息写入用户字典中 #因为id有可能是用户伪造的并非是我们sever生成所以要在这里做一些判断: if client_random_str in self.ppp: """老用户""" self.random_str = client_random_str else: """非法用户(伪造s_id)""" self.random_str = self.create_random_str() self.ppp.initial(self.random_str) #创建用户字典 #把s_id设置到cookie中并设置超时时间(参数expries=当前时间+失效时间,单位是s) #如果sessionid存在该方法内部不会重复去设置,只是会更新失效时间而已! ctime = time.time() #获取当前时间 self.handler.set_cookie('session_id',self.random_str,expires=ctime+1800) self.ppp.close() def create_random_str(self): """生成随机字符串""" v = str(time.time()) m = hashlib.md5() m.update(bytes(v,encoding='utf-8')) return m.hexdigest() def __getitem__(self, key): """获取session中用户信息""" self.ppp.open() v = self.ppp.get(self.random_str,key) self.ppp.close() return v def __setitem__(self, key, value): """my_session['key']=value会触发该方法并将key和value传入""" #后台存储--->设置用户session字典 self.ppp.open() self.ppp.set(self.random_str,key,value) self.ppp.close() def __delitem__(self, key): self.ppp.open() del self.ppp.delete(self.random_str,key) self.ppp.close() def clear(self): """清空当前用户的session""" self.ppp.open() self.ppp.clear(random_str) self.ppp.close() class BaseHandler(tornado.web.RequestHandler): """拓展控制器功能""" def initialize(self): #self是MianHandler对象 # my_session['k1']访问 __getitem__ 方法 self.my_session = Session(self) #把对象传到Session中 class HomeHandler(BaseHandler): def get(self): user = self.my_session['user'] if not user: # 用户字典中没有user值,表示没有登陆 self.redirect('http://deehuang.github.io') else: self.write(user) class LoginHandler(BaseHandler): def get(self): self.my_session['user'] = 'root' self.redirect('/home') settings = { 'template_path': 'template', 'static_path': 'static', 'static_url_prefix': '/static/', 'cookie_secret': 'woshisuijimiyao', 'login_url': '/login' } application = tornado.web.Application([ (r"/home", HomeHandler), (r"/login", LoginHandler), ], **settings) if __name__ == "__main__": application.listen(8888) tornado.ioloop.IOLoop.instance().start()
session框架的原理是不分语言不分web框架的,每个框架内部都是这样去实现的
结语
以上是个人学习之路,如有误,欢迎指正!参考文献