玩转PythonWeb框架之Tornado

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了玩转PythonWeb框架之Tornado相关的知识,希望对你有一定的参考价值。

(1)简介:

Tornado是一种Web服务器软件的开源版本,Tornado是非阻塞式服务器,速度很快。这得益于其非阻塞式和对epoll的运用,Tornado每秒可以处理数以千计的连接,因此Tornado是实时Web服务的一个理想的框架。

Tornado是现在应用最为广泛的Web框架,其具有以下优势:

1.轻量级Web框架

2.异步非阻塞IO处理

3.出色的抗负载能力

4.优秀的处理性能,不依赖多进程和多线程

(2)Tornado与Django的比较:

1.Django是重量级的Web框架,功能齐全,注重开发效率

2.Django内置管理后台

3.Django内置封装完善的ORM操作

4.Django提供Session功能

5.Django与Tornado相比,Django高耦合

6.Tornado与Django相比,入门门槛较高

(3)使用Tornado:

Tornado应该运行在类Unix平台,为了达到最佳的性能和扩展性,仅推荐Linux和BSD(充分利用Linux的epoll工具和BSD的kqueue达到高性能处理的目的

☆安装Tornado:

pip install tornado

☆在代码中导入Tornado:

import tornado.ioloop

import tornado.web

☆Tornado的执行流程:

1.执行Python文件,监听设置的端口

2.浏览器请求服务器,经过路由

3.路由匹配对应的处理器类

4.根据请求类型执行指定处理器类中的处理函数

5.返回处理结果,客户端浏览器渲染页面

 

创建处理器类,在类中定义HTTP方法:

class MainHandler(tornado.web.RequestHandler):

    def get(self):

        pass

 

    def post(self):

        pass

    ... # HTTP中的方法

为Tornado设置模板目录:

settings = {

    "template_path":"模板路径", # 设置模板目录

}

设置Tornado路由关系:

application = tornado.web.Application([(r"/", MainHandler),], **settings) # 传入设置参数

启动服务:

if __name__ == "__main__":

    application.listen(8009) # 设置监听端口

    tornado.ioloop.IOLoop.instance().start() # 启动服务

 

示例代码:

/controller/One.py

import tornado.ioloop

import tornado.web

 

mylist = []

mylist2 = []

 

class MainHandler(tornado.web.RequestHandler):

 

    def get(self):

        self.render("index.html", list = mylist, list2 = mylist,)

 

    def post(self):

        name = self.get_argument("name")

        love = self.get_argument("mylove")

        mylist.append(name)

        mylist2.append(love)

 

        self.render('index.html', list = mylist, list2 = mylist2,)

 

settings = {

    "template_path":"../views", # 设置模板目录

}

 

application = tornado.web.Application([(r"/index", MainHandler),], **settings) # 传入设置参数

 

if __name__ == "__main__":

    application.listen(8009) # 设置监听端口

tornado.ioloop.IOLoop.instance().start() # 启动服务

/views/index.html

<!DOCTYPE html>

<html>

<head>

    <meta charset="UTF-8">

    <title>Index</title>

</head>

<body style="background-color: darkorange;color: aliceblue">

<center>

    <h1>Hello,Tornado.</h1>

</center>

<center>

    <form action="/index" name="formdata" method="post">

        姓名:<input type="text" name="name">

        爱好:<select name="mylove" id="">

        <option value="movie">电影</option>

        <option value="football">足球</option>

        <option value="music">音乐</option>

    </select>

        <input type="submit" value="提交">

        <input type="reset" value="重置">

    </form>

</center>

<center>

    <h2>《提交的信息》</h2>

    <h3>

    {% for i in list %}

        姓名:{{ i }}

    {% end %}

 

    {% for x in list2 %}

        爱好:{{ x }}

    {% end %}

    </h3>

</center>

</body>

</html>

(4)路由系统:

Tornado中的URL对应的不是处理函数而是类,在处理类中定义HTTP方法。Tornado中的路由系统可以分为:静态路由、动态路由、请求方法路由、二级路由。

设置路由的时候可以使用URL对应的方式,也可以使用装饰器的方式进行路由映射。

装饰器映射的方式:

@root.route(‘路由URL)

def MethodHandler(self):

    Some...

root.run(host=’IP地址’, port=端口)

☆静态路由:

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

☆基于正则的动态路由:

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

同时处理器函数也可以传入此参数:

class MainHandler(tornado.web.RequestHandler):

    def get(self,id):
        self.render('index.html',id=id)

    def post(self):
        pass

使用装饰器操作:

@root.route('/wiki/<pagename>')

def callback(pagename):

    ...

 

@root.route('/object/<id:int>')

def callback(id):

    ...

 

@root.route('/show/<name:re:[a-z]+>')

def callback(name):

    ...

 

@root.route('/static/<path:path>')

def callback(path):

    return static_file(path, root='static')

☆请求方法路由:

@root.route('/hello/', method='POST')

# 如果使用@root.get()表示装饰器下的函数只接受get请求

def index():

    ...

 

@root.get('/hello/')

def index():

    ...

 

@root.post('/hello/')

def index():

    ...

 

@root.put('/hello/')

def index():

    ...

 

@root.delete('/hello/')

def index():

    ...

☆二级路由:

主机头

URL正则

Handler

safe

/index/\d*

IndexHandler

/admin/\w*

AdminHandler

/car/\w*

CarHandler

.*

/index/\w*

HomeHandler

/pro/\w*

ProHandler

/.*

AllHandler

 

application = tornado.web.Application('www.test.com$',[(r"/index/", MainHandler),], **settings)

使用装饰器的方式:

#!/usr/bin/env python

# -*- coding:utf-8 -*-

# file:index.py

from bottle import template, Bottle

from bottle import static_file

root = Bottle()

@root.route('/hello/')

def index():

    Some...

from framwork_bottle import app01

from framwork_bottle import app02

root.mount('app01', app01.app01)

root.mount('app02', app02.app02)

root.run(host='localhost', port=8888)

 

#!/usr/bin/env python

# -*- coding:utf-8 -*-

# file:app01.py

from bottle import template, Bottle

app01 = Bottle()

@app01.route('/hello/', method='GET')

def index():

    Some...

 

#!/usr/bin/env python

# -*- coding:utf-8 -*-

# file:app02.py

from bottle import template, Bottle

app02 = Bottle()

@app02.route('/hello/', method='GET')

def index():

    Some...

(5)模板引擎:

☆模板语法

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>
模板引擎语法</title>
</head>
<body>
    <!--
单个变量的引用-->
    {{ x }}
    <!--
单行Python代码-->
    % temp = "Hello"
    <!--
多行Python代码-->
    <%
        Some python codes...
    %>
    <!--HTML
Python代码混合-->
    {% for i in list %}
    <h3>{{ i }}</h3>
    {% end %}
<!—使用模板自定义编辑块-->
{% block RenderBody %}{% end %}
<!—在其他HTML文件中使用同样的方式在块中间填充数据-->
</body>
</html>

☆模板函数:

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 的別名

☆自定义UIMethod和UIModule:

定义:

# file:uimethods.py

def 自定义函数(self):

Some...

 

#!/usr/bin/env python

# -*- coding:utf-8 -*-

# uimodules.py

from tornado.web import UIModule

from tornado import escape

 

class 自定义类(UIModule):

 

    def render(self, *args, **kwargs):

        return escape.xhtml_escape('<h1>Test</h1>')

注册:

#!/usr/bin/env python

# -*- coding:utf-8 -*-

# file:test.py

import tornado.ioloop

import tornado.web

from tornado.escape import linkify

import uimodules as md

import uimethods as mt

 

class MainHandler(tornado.web.RequestHandler):

    def get(self):

        self.render('index.html')

 

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)

 

 

if __name__ == "__main__":

    application.listen(8009)

    tornado.ioloop.IOLoop.instance().start()

使用:

<!DOCTYPE html>

<html>

<head>

    <meta charset="UTF-8">

    <title></title>

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

</head>

<body>

    <h1>hello</h1>

    {% module 自定义类名(123) %}

    {{ 自定义函数() }}

</body>

(6)静态缓存:

对于静态文件,可以配置静态文件的目录和前段使用时的前缀,并且Tornaodo还支持静态文件缓存。

配置:

settings = {

    'static_url_prefix': '/static/',

}

使用:

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

静态文件缓存的实现:

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()

(7)公共组件:

Web框架的本质就是接受用户请求,处理用户请求,响应请求内容。由开发人员定制用户的请求处理,Web框架接管请求的响应和请求。当接受用户请求的时候会将请求的信息封装在Bottle中的request中,而请求做出响应封装在Bottle的response中,公共组件的本质就是为开发人员提供相关的接口。

request.headers  #请求头信息,可以通过请求头信息来获取相关客户端的信息

request.query #get请求信息,如果用户访问时这样的:http://127.0.0.1:8000/?page=123就必须使用request.query  使用GET方法是无法取到信息的

request.forms #post请求信息

request.files #上传文件信息

request.params #getpost请求信息,他是GETPOST的总和,其实他内部调用了request.get request.forms

request.GET #get请求信息

request.POST #post和上传信息,上传文件信息,和post信息

request.cookies #cookie信息

request.environ #环境相关,如果上面的这些请求信息没有满足需求,就在这里找

(8)XSS跨站脚本攻击与CSRF跨域伪造请求:

XSS

恶意攻击者往Web页面里插入恶意脚本代码,当用户浏览该页之时,嵌入其中Web里面的脚本代码会被执行,从而达到恶意攻击用户的特殊目的。

CSRF配置:

settings = {

    "xsrf_cookies": True,

}

1.  普通的表单使用:

<form action=" " method="post">

  {{ xsrf_form_html() }}

  Some...

</form>

2.  Ajax使用:

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 + ")"));

    }});

};

(9)Cookie与Session:

Tornado中可以对cookie进行操作,使用set_cookie(‘ Key’,’Value’)方法进行设置cookie,使用get_cookie(‘Key’)获取Cookie值。

由于Cookie很容易被客户端伪造,假如需要在cookie中保存当前的用户登录状态,需要对cookie进行签名。通过set_secure_cookie(‘Key’,’Value’)get_secure_cookie(‘Key’)方法设置和使用,但是需要在使用的时候创建一个密钥,叫做cookie_secret

settings = {

    'cookie_secret': '一堆字符串'

}

签名Cookie的本质:

写入cookie的过程:

1.  将值进行base64加密

2.  对除去值以外的内容进行签名,使用无法逆向破解的哈希算法

3.  拼接签名与加密值

读取cookie的过程:

1.  读取加密的内容

2.  对签名进行验证

3.  进行base64解密,获取值的内容

 

JavaScript操作cookie:

function setCookie(name,value,expires){

    var current_date = new Date();

    current_date.setSeconds(current_date.getSeconds() + 5);

    document.cookie = name + "= "+ value +";expires=" + current_date.toUTCString();

}

自定义Session:

#!/usr/bin/env python

# -*- coding:utf-8 -*-

import tornado.web

import tornado.ioloop

 

container = {}

class Session:

    def __init__(self, handler):

        self.handler = handler

        self.random_str = None

 

    def __genarate_random_str(self):

        import hashlib

        import time

        obj = hashlib.md5()

        obj.update(bytes(str(time.time()), encoding='utf-8'))

        random_str = obj.hexdigest()

        return random_str

 

    def __setitem__(self, key, value):

        # container中加入随机字符串

        # 定义专属于自己的数据

        # 在客户端中写入随机字符串

        # 判断,请求的用户是否已有随机字符串

        if not self.random_str:

            random_str = self.handler.get_cookie('__session__')

            if not random_str:

                random_str = self.__genarate_random_str()

                container[random_str] = {}

            else:

                # 客户端有随机字符串

                if random_str in container.keys():

                    pass

                else:

                    random_str = self.__genarate_random_str()

                    container[random_str] = {}

            self.random_str = random_str

 

        container[self.random_str][key] = value

        self.handler.set_cookie("__session__", self.random_str)

 

    def __getitem__(self, key):

        # 获取客户端的随机字符串

        # container中获取专属于我的数据

        # 专属信息【key

        random_str =  self.handler.get_cookie("__session__")

        if not random_str:

            return None

        # 客户端有随机字符串

        user_info_dict = container.get(random_str,None)

        if not user_info_dict:

            return None

        value = user_info_dict.get(key, None)

        return value

class BaseHandler(tornado.web.RequestHandler):

    def initialize(self):

        self.session = Session(self)

一致性哈希:

#!/usr/bin/env python

#coding:utf-8

 

import sys

import math

from bisect import bisect

 

 

if sys.version_info >= (2, 5):

    import hashlib

    md5_constructor = hashlib.md5

else:

    import md5

    md5_constructor = md5.new

 

 

class HashRing(object):

    """一致性哈希"""

   

    def __init__(self,nodes):

        '''初始化

        nodes : 初始化的节点,其中包含节点已经节点对应的权重

                默认每一个节点有32个虚拟节点

                对于权重,通过多创建虚拟节点来实现

                如:nodes = [

                        {'host':'127.0.0.1:8000','weight':1},

                        {'host':'127.0.0.1:8001','weight':2},

                        {'host':'127.0.0.1:8002','weight':1},

                    ]

        '''

       

        self.ring = dict()

        self._sorted_keys = []

 

        self.total_weight = 0

       

        self.__generate_circle(nodes)

       

           

           

    def __generate_circle(self,nodes):

        for node_info in nodes:

            self.total_weight += node_info.get('weight',1)

           

        for node_info in nodes:

            weight = node_info.get('weight',1)

            node = node_info.get('host',None)

               

            virtual_node_count = math.floor((32*len(nodes)*weight) / self.total_weight)

            for i in xrange(0,int(virtual_node_count)):

                key = self.gen_key_thirty_two( '%s-%s' % (node, i) )

                if self._sorted_keys.__contains__(key):

                    raise Exception('该节点已经存在.')

                self.ring[key] = node

                self._sorted_keys.append(key)

           

    def add_node(self,node):

        ''' 新建节点

        node : 要添加的节点,格式为:{'host':'127.0.0.1:8002','weight':1},其中第一个元素表示节点,第二个元素表示该节点的权重。

        '''

        node = node.get('host',None)

        if not node:

                raise Exception('节点的地址不能为空.')

               

        weight = node.get('weight',1)

       

        self.total_weight += weight

        nodes_count = len(self._sorted_keys) + 1

       

        virtual_node_count = math.floor((32 * nodes_count * weight) / self.total_weight)

        for i in xrange(0,int(virtual_node_count)):

            key = self.gen_key_thirty_two( '%s-%s' % (node, i) )

            if self._sorted_keys.__contains__(key):

                raise Exception('该节点已经存在.')

            self.ring[key] = node

            self._sorted_keys.append(key)

       

    def remove_node(self,node):

        ''' 移除节点

        node : 要移除的节点 '127.0.0.1:8000'

        '''

        for key,value in self.ring.items():

            if value == node:

                del self.ring[key]

                self._sorted_keys.remove(key)

   

    def get_node(self,string_key):

        '''获取 string_key 所在的节点'''

        pos = self.get_node_pos(string_key)

        if pos is None:

            return None

        return self.ring[ self._sorted_keys[pos]].split(':')

   

    def get_node_pos(self,string_key):

        '''获取 string_key 所在的节点的索引'''

        if not self.ring:

            return None

           

        key = self.gen_key_thirty_two(string_key)

        nodes = self._sorted_keys

        pos = bisect(nodes, key)

        return pos

   

    def gen_key_thirty_two(self, key):

       

        m = md5_constructor()

        m.update(key)

        return long(m.hexdigest(), 16)

       

    def gen_key_sixteen(self,key):

       

        b_key = self.__hash_digest(key)

        return self.__hash_val(b_key, lambda x: x)

 

    def __hash_val(self, b_key, entry_fn):

        return (( b_key[entry_fn(3)] << 24)|(b_key[entry_fn(2)] << 16)|(b_key[entry_fn(1)] << 8)| b_key[entry_fn(0)] )

 

    def __hash_digest(self, key):

        m = md5_constructor()

        m.update(key)

        return map(ord, m.digest())

 

 

"""

nodes = [

    {'host':'127.0.0.1:8000','weight':1},

    {'host':'127.0.0.1:8001','weight':2},

    {'host':'127.0.0.1:8002','weight':1},

]

 

ring = HashRing(nodes)

result = ring.get_node('98708798709870987098709879087')

print result

 

"""

自定义Session:

from hashlib import sha1

import os, time

 

 

create_session_id = lambda: sha1('%s%s' % (os.urandom(16), time.time())).hexdigest()

 

 

class Session(object):

 

    session_id = "__sessionId__"

 

    def __init__(self, request):

        session_value = request.get_cookie(Session.session_id)

        if not session_value:

            self._id = create_session_id()

        else:

            self._id = session_value

        request.set_cookie(Session.session_id, self._id)

 

    def __getitem__(self, key):

        # 根据 self._id ,在一致性哈希中找到其对应的服务器IP

        # 找到相对应的redis服务器,如: r = redis.StrictRedis(host='localhost', port=6379, db=0)

        # 使用python redis api 链接

        # 获取数据,即:

        # return self._redis.hget(self._id, name)

 

    def __setitem__(self, key, value):

        # 根据 self._id ,在一致性哈希中找到其对应的服务器IP

        # 使用python redis api 链接

        # 设置session

        # self._redis.hset(self._id, name, value)

 

 

    def __delitem__(self, key):

        # 根据 self._id 找到相对应的redis服务器

        # 使用python redis api 链接

        # 删除,即:

        return self._redis.hdel(self._id, name)

 

(10)文件上传:

使用Form表单:

<!DOCTYPE html>

<html>

<head>

    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>

    <title>上传文件</title>

</head>

<body>

    <form id="my_form" name="form" action="/index" method="POST"  enctype="multipart/form-data" >

        <input name="fff" id="my_file"  type="file" />

        <input type="submit" value="提交"  />

    </form>

</body>

</html>

#!/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 = {

    'template_path': 'template',

}

 

application = tornado.web.Application([

    (r"/index", MainHandler),

], **settings)

 

 

if __name__ == "__main__":

    application.listen(8000)

    tornado.ioloop.IOLoop.instance().start()

使用AjaxXMLHttpRequest:

<!DOCTYPE html>

<html>

<head>

    <meta charset="UTF-8">

    <title></title>

</head>

<body>

    <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("fff", fileObj);

 

            var xhr = new XMLHttpRequest();

            xhr.open("post", '/index', true);

            xhr.send(form);

        }

    </script>

</body>

</html>

使用Ajax-jQuery:

<!DOCTYPE html>

<html>

<head>

    <meta charset="UTF-8">

    <title></title>

</head>

<body>

    <input type="file" id="img" />

    <input type="button" onclick="UploadFile();" />

    <script>

        function UploadFile(){

            var fileObj = $("#img")[0].files[0];

            var form = new FormData();

            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>

使用iframe预览图片:

<!DOCTYPE html>

<html>

<head>

    <meta charset="UTF-8">

    <title></title>

</head>

<body>

    <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="" ></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>

</body>

</html>


以上是关于玩转PythonWeb框架之Tornado的主要内容,如果未能解决你的问题,请参考以下文章

Python Web框架Tornado的异步处理代码示例

python web框架之Tornado的简单使用

1Python全栈之路系列之Tornado Web框架

Python Web框架Tornado的异步处理代码演示样例

框架之Tornado(简单介绍)

Tornado入门之旅