Tornado框架入门

关于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大致上就是执行处理以下这些事情:

  1. 第一步:执行脚本,监听 8888 端口
  2. 第二步:浏览器客户端访问 /index --> http://127.0.0.1:8888/index
  3. 第三步:服务器接受请求,并交由对应的类处理该请求
  4. 第四步:类接受到请求之后,根据请求方式(post / get / delete ...)的不同调用并执行相应的方法
  5. 第五步:方法返回值的字符串内容发送浏览器
#!/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()方法。它是getpost方式都去取数据
如果要单独取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(依赖配置文件)

  1. 设置配置文件,需要添加一个随机字符串密钥(用来给cookie安全加密)
  2. 设置cookie的时候使用self.set_secure_cookie()
  3. 取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不太一样

模板语言还支持自定制拓展方法和类,它是通过UIMethodUIModule这两个组件去实现的( 这两个组件类似于Django的simple_tag )。
其实通过UIMethod定制方法已经能实现所有的我们想生成的内容了,那么UIModule定制类存在的意义是什么呢?UIModule除了和UIMethod一样可以帮我们生成内容还可以帮我们添加CSS,JS。

下面来看下它们的实现方式:

  1. 定义:

    • 定制方法:

      #模块名为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字符串进行转义

  2. 注册:在配置文件中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)
    
  3. 使用

    {% 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的快速调用存储。

  1. 知识储备:

    • 关于多继承:
      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')
      
  2. session的实现机制:

    • 生成随机字符串

    • 写入用户Cookie

    • 后台存储

    • 在控制器中self.session['xx']实现session机制,就需要用到上面说到的知识储备了!

  3. 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框架的,每个框架内部都是这样去实现的

结语

以上是个人学习之路,如有误,欢迎指正!参考文献

posted @ 2021-02-10 01:21  .Jochen  阅读(134)  评论(0编辑  收藏  举报