天天看點

玩轉Tornado架構Tornado簡介Tornado與Django的比較使用TornadoTornado的執行流程快速開始Tornado路由系統模闆引擎靜态緩存公共元件XSS跨站腳本攻擊與CSRF跨域僞造請求Cookie與Session檔案上傳

文章目錄

  • Tornado簡介
  • Tornado與Django的比較
  • 使用Tornado
    • (1) 安裝
    • (2)導入
  • Tornado的執行流程
  • 快速開始
    • (1) 建立處理器類,在類中定義HTTP方法
    • (2) 為Tornado設定模闆目錄
    • (3) 設定Tornado的路由關系
    • (4) 啟動第一個伺服器
    • (5) 代碼執行個體
  • Tornado路由系統
    • (1)請求方法路由
    • (2) 二級路由
  • 模闆引擎
    • (1) 模闆文法
    • (2) 模闆函數
    • (3) 自定義UIMethod和UIModule
  • 靜态緩存
  • 公共元件
  • XSS跨站腳本攻擊與CSRF跨域僞造請求
  • Cookie與Session
  • 檔案上傳
    • (1) 使用Form表單
    • (2) 使用AjaxXMLHttpRequest
    • (3) 使用jQuery的Ajax
    • (4) 使用iframe預覽圖檔

Tornado簡介

Tornado是一種Web伺服器軟體的開源版本,Tornado是非阻塞式伺服器,速度很快。這得益于其非阻塞式和對epoll的運用,Tornado每秒可以處理數以千計的連接配接,是以Tornado是實時Web服務的一個理想的架構。Tornado是現在應用最為廣泛的Web架構,其具有以下優勢:

  • 輕量級Web架構
  • 異步非阻塞IO處理
  • 出色的抗負載能力
  • 優秀的處理性能,不依賴多程序和多線程

Tornado與Django的比較

  • Django是重量級的Web架構,功能齊全,注重開發效率
  • Django内置管理背景Django Admin
  • Django内置封裝完善的ORM操作
  • Django提供Session功能
  • Django與Tornado相比,Django高耦合
  • Tornado與Django相比,入門門檻較高

使用Tornado

(1) 安裝

pip install tornado
           

(2)導入

import tornado.ioloop
import tornado.web
           

Tornado的執行流程

  1. 執行Python檔案,監聽設定的端口
  2. 浏覽器請求伺服器,經過路由
  3. 路由比對對應的處理器類
  4. 根據請求類型執行指定處理器類中的處理函數
  5. 傳回處理結果,用戶端浏覽器渲染頁面

快速開始

(1) 建立處理器類,在類中定義HTTP方法

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        pass
    def post(self):
        pass
    ... # HTTP中的方法
           

(2) 為Tornado設定模闆目錄

settings = {
    "template_path":"模闆路徑", # 設定模闆目錄
}
           

(3) 設定Tornado的路由關系

application = tornado.web.Application([(r"/", MainHandler),], **settings) # 傳入設定參數
           

(4) 啟動第一個伺服器

if __name__ == "__main__":
    application.listen(8009) # 設定監聽端口
    tornado.ioloop.IOLoop.instance().start() # 啟動服務
           

(5) 代碼執行個體

# /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>
           

Tornado路由系統

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

(1)請求方法路由

@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():
    ...
           

(2) 二級路由

Host Header URL reg Handler
safe /index/\d* IndexHandler
safe /admin/\w* AdminHandler
safe /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...
           

模闆引擎

(1) 模闆文法

<!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>
           

(2) 模闆函數

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

(3) 自定義UIMethod和UIModule

定義:

# file:uimethods.py
def 自定義函數(self):
	# Some...
           
# 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(" target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow" commons.css")}}" rel="stylesheet" />
</head>
<body>
    <h1>hello</h1>
    {% module 自定義類名(123) %}
    {{ 自定義函數() }}
</body>
</html>
           

靜态緩存

對于靜态檔案,可以配置靜态檔案的目錄和前段使用時的字首,并且Tornaodo還支援靜态檔案緩存。

配置:

settings = {
    'static_url_prefix': '/static/',
}
           

使用:

<link href="{{static_url(" target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow" 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()
           

公共元件

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 #get和post請求資訊,他是GET和POST的總和,其實他内部調用了request.get request.forms
request.GET #get請求資訊
request.POST #post和上傳資訊,上傳檔案資訊,和post資訊
request.cookies #cookie資訊
request.environ #環境相關,如果上面的這些請求資訊沒有滿足需求,就在這裡找
           

XSS跨站腳本攻擊與CSRF跨域僞造請求

XSS:惡意攻擊者往Web頁面裡插入惡意腳本代碼,當使用者浏覽該頁之時,嵌入其中Web裡面的腳本代碼會被執行,進而達到惡意攻擊使用者的特殊目的。
  • CSRF配置
settings = {
    "xsrf_cookies": True,
}
           
  • 普通的表單使用
<form action=" " method="post">
  {{ xsrf_form_html() }}
  Some...
</form>
           
  • 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 + ")"));
    }});
};
           

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的過程
    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)
           

檔案上傳

(1) 使用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()
           

(2) 使用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>
           

(3) 使用jQuery的Ajax

<!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>
           

(4) 使用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>