1. 程式人生 > >Python+WEB框架之Tornado

Python+WEB框架之Tornado

前言

Tornado(龍捲風)和Django一樣是Python中比較主流的web框架,Tornado 和現在的主流 Web 伺服器框架也有著明顯的區別:Tornado自帶socket,並且實現了非同步非阻塞並對WebSocket協議天然支援;

 

一、Tornado框架的基本組成

Tonado由 路由系統、檢視、模板語言4大部分組成,如果習慣了使用Django你會感覺它功能單薄,但是隻有這樣才能足夠輕量,如果用到什麼功能就自己去GitHub上找現成的外掛,或者自實現;以下將對這些基本元件進行逐一介紹。

Django功能概覽:

socket:有 
  中介軟體:無(使用Python的wsgiref模組)
  路由系統:有
  檢視函式:有
  ORM操作:有
  模板語言:有
  simple_tag:有
  cokies:有
  session:有
  csrf:有
  xss:有
  其他:快取、訊號、Form元件、ModelFormm、Admin








tornado功能概覽:

  socket:有(非同步非阻塞、支援WebScoket)
  路由系統:有
  檢視函式:有
  靜態檔案:有
  ORM操作:無
  模板語言:有
  simple_tag:有,uimethod,uimodule
  cokies:有
  session:無
  csrf:有
  xss:有
  其他:無
Django和Tonado功能對比

 

二、Tornado自帶功能

1、Tornado執行流程

#準備安裝Tornado: pip install tornado

import tornado.ioloop
import tornado.web

class MainHandler(tornado.web.RequestHandler): #注意繼承RequestHandler 而不是redirectHandler
    def get(self):
        self.write('hellow ,world')


application
=tornado.web.Application([ (r'/index/',MainHandler) #路由 ]) if __name__ == '__main__': application.listen(8888) #建立1個socket物件 tornado.ioloop.IOLoop.instance().start() #conn,addr=socket.accept()進入監聽狀態
View Code

第一步:執行指令碼,監聽 8888 埠

第二步:瀏覽器客戶端訪問 /index  -->  http://127.0.0.1:8888/index/

第三步:伺服器接受請求,並交由對應的類處理該請求

第四步:類接受到請求之後,根據請求方式(post / get / delete ...)的不同調用並執行相應的方法

第五步:方法返回值的字串內容傳送瀏覽器

 

配置檔案:

setings={
'template_path':'templates',#配置模板路徑
'static_path':'static',     #配置靜態檔案存放的路徑
'static_url_prefix':'/zhanggen/', #在模板中引用靜態檔案路徑時使用的別名 注意是模板引用時的別名
"xsrf_cookies": True,               #使用xsrf認證
 'cookie_secret' :'xsseffekrjewkhwy'#cokies加密時使用的鹽
}
application=tornado.web.Application([
                        (r'/login/',LoginHandler) ,#引數1 路由系統
                        (r'/index/',IndexHandler) ,#引數1 路由系統

                                     ],
                        **setings                  #引數2 配置檔案
                            )
View Code

 

 

2、路由系統

2.1、動態路由(url傳引數)

app=tornado.web.Application(
    [
        (r'^/index/$',MainHandler),
        (r'^/index/(\d+)$',MainHandler), #url傳參
    ]
)
View Code

2.2、域名匹配 

#支援域名匹配  www.zhanggen.com:8888/index/333333
app.add_handlers('www.zhanggen.com',[

        (r'^/index/$', MainHandler),
        (r'^/index/(\d+)$', MainHandler),
])
View Code

2.3、反向生成url

app.add_handlers('www.zhanggen.com',[

        (r'^/index/$', MainHandler,{},"name1"), #反向生成url
        (r'^/index/(\d+)$', MainHandler,{},"name2"),
])
路由
class MainHandler(tornado.web.RequestHandler):
    def get(self,*args,**kwargs):
        url1=self.application.reverse_url('name1')
        url2 = self.application.reverse_url('name2', 666)
        print(url1,url2)
        self.write('hello word')
檢視

 

 

3、檢視

tornado的檢視才有CBV模式,url匹配成功之後先  檢視執行順序為 initialize 、prepare、get/post/put/delete、finish;

import tornado.ioloop
import tornado.web
class MainHandler(tornado.web.RequestHandler):
    def initialize(self): #1
        print()

    def prepare(self):
        pass

    def get(self,*args,**kwargs):
        self.write('hello word')

    def post(self, *args, **kwargs):
        pass

    def finish(self, chunk=None):
        pass
        super(self,MainHandler).finish()
View Code

 

3.1、請求相關

self.get_body_argument('user') :獲取POST請求攜帶的引數

self.get_body_arguments('user_list') :獲取POST請求引數列表(如chebox標籤和select多選)

self.request.body.decode('utf-8'):獲取json資料

self.get_query_argument('user') :獲取GET請求攜帶的引數

self.get_query_arguments('user_list') :獲取GET請求引數列表(如chebox標籤和select多選)

self.get_argument('user') :獲取GET和POST請求攜帶的引數

self.get_arguments('user_list'):獲取GET和POST請求引數列表(如chebox標籤和select多選)

 

注:以上取值方式如果取不到值就會報錯,可以設定取不到值就取None;(例如 self.get_argument('user',None))

 

3.2、響應相關

self.write() :響應字串

self.render():響應頁面

self.redirect():頁面跳轉

 

4、模板語言

tornado的模板語言和Python語法一致

123
View Code

4.1、登入頁面

#準備安裝Tornado: pip install tornado

import tornado.ioloop
import tornado.web

class LoginHandler(tornado.web.RequestHandler): #注意繼承RequestHandler 而不是redirectHandler
    def get(self):
        self.render('login.html')

setings={
'template_path':'templates',#配置模板路徑
'static_path':'static',     #配置靜態檔案存放的路徑
'static_url_prefix':'/zhanggen/' #在模板中引用靜態檔案路徑時使用的別名 注意是模板引用時的別名
}
application=tornado.web.Application([
                        (r'/login/',LoginHandler) #引數1 路由系統

                                     ],
                        **setings                  #引數2 配置檔案
                            )


if __name__ == '__main__':
    application.listen(8888)                  #建立1個socket物件
    tornado.ioloop.IOLoop.instance().start()  #conn,addr=socket.accept()進入監聽狀態
View Code
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="/zhanggen/dist/css/bootstrap.css">
    <title>Title</title>
</head>
<body>
<div class="container">
    <div class="row">
        <div class="col-md-5 col-md-offset-3">
            <form method="post" >
                  <div class="form-group">
                    <label for="exampleInputEmail1">使用者名稱</label>
                    <input type="email" class="form-control" id="exampleInputEmail1" placeholder="使用者名稱">
                  </div>
                  <div class="form-group">
                    <label for="exampleInputPassword1">密碼</label>
                    <input type="password" class="form-control" id="exampleInputPassword1" placeholder="密碼">
                  </div>
                  <button type="submit" class="btn btn-default">提交</button>
            </form>
        </div>
    </div>
</div>
</body>
</html>
模板語言

 

4.2、引入靜態檔案

<link rel="stylesheet" href="/zhanggen/coment.css">
通過別名引入靜態檔案
<link rel="stylesheet" href='{{static_url("dist/css/bootstrap.css") }}'>
static_url()方式引入靜態檔案

 

通過static_url()方法引入靜態檔案的好處: 

1、使用static_url()可以不用考慮靜態檔案修改之後造成引用失效的情況;

2、還會生成靜態檔案url會有一個v=...的引數,這是tornado根據靜態檔案MD5之後的值,如果後臺的靜態檔案修改,這個值就會變化,前端就會重新向後臺請求靜態檔案,保證頁面實時更新,不引用瀏覽器快取;

 

4.3、上下文物件

如果模板語言中聲明瞭變數,上下文物件必須對應傳值,如果沒有就設定為空,否則會報錯;

self.render('login.html',**{'erro_msg':'' }) #模板中聲明瞭變數,檢視必須傳值,如果沒有就設定為空;
View Code

 

5、xsrf_tocken認證

setings={
'template_path':'templates',#配置模板路徑
'static_path':'static',     #配置靜態檔案存放的路徑
'static_url_prefix':'/zhanggen/', #在模板中引用靜態檔案路徑時使用的別名 注意是模板引用時的別名
"xsrf_cookies": True,           #使用xsrf認證
}
配置檔案setings={"xsrf_cookies": True, }
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href='{{static_url("dist/css/bootstrap.css") }}'>
    <title>Title</title>
</head>
<body>
<div class="container">
    <div class="row">
        <div class="col-md-5 col-md-offset-3">
            <form method="post" >
                {%raw xsrf_form_html() %}
                  <div class="form-group">
                    <input type="text" class="form-control" placeholder="使用者名稱" name="user">
                  </div>
                  <div class="form-group">
                    <input type="password" class="form-control" placeholder="密碼" name="pwd">
                  </div>
                  <button type="submit" class="btn btn-default">提交</button>
            </form>
        </div>
    </div>
</div>
</body>
</html>
模板語言 {%raw xsrf_form_html() %}

 

6、cokies

Tornado不自帶session,但是包含cookies;

6.1、cookies

設定cokies

  user=self.get_cookie('username')
        if user:
            v=time.time()+10
            self.set_cookie('username', user, expires=v)
set_cookie('key',value , expires=過期時間)

獲取cokies

self.get_cookie('username')
self.get_cookie('username')

 

設定在使用者不斷重新整理頁面的情況,cookies不過期;

import tornado.ioloop
import tornado.web
import time
class SeedListHandler(tornado.web.RequestHandler):
    def initialize(self):
        user=self.get_cookie('username')
        if user:
            v=time.time()+10
            self.set_cookie('username', user, expires=v)
構造initialize方法

 

6.2、Tornado加密cokies

配置加密規則使用的字串

setings={
        'template_path':'templates',
        'static_path': 'static',
        'static_url_prefix':'/zhanggen/', #配置檔案別名必須以/開頭以/結尾
        'cookie_secret':'sssseertdfcvcvd'#配置加密cookie使用得加密字串

    }
setings

 

設定加密的cokies

self.set_secure_cookie('username',user,expires=v)
self.set_secure_cookie('key',value,expires=過期時間)

 

獲取加密的cokies

self.get_secure_cookie('username')
get_secure_cookie('key')

 

設定在使用者不斷重新整理頁面的情況,SecureCookies不過期;

import tornado.ioloop
import tornado.web
import time
class SeedListHandler(tornado.web.RequestHandler):
    def initialize(self):
        user=self.get_secure_cookie('username')
        if user:
            v=time.time()+10
            self.set_secure_cookie('username', user, expires=v)  #設定加密cookies
構造initialize方法

 

6.3、@authenticated 裝飾器

執行 self.curent_user,有值就登入使用者,無就去執行get_curent_user方法,get_curent_user沒有返回使用者資訊,會記錄當前url更加配置檔案跳轉到登入頁面;

 

配置認證失敗跳轉的url

setings={
        'template_path':'templates',
        'static_path': 'static',
        'static_url_prefix':'/zhanggen/', #配置檔案別名必須以/開頭以/結尾
        'cookie_secret':'sssseertdfcvcvd',#配置加密cookie使用得加密字串
        'login_url':'/login/'              #@authenticated 驗證失敗跳轉的url
    }
setings

檢視

import tornado.ioloop
import tornado.web
import time
from tornado.web import authenticated
class SeedListHandler(tornado.web.RequestHandler):
    def initialize(self):
        user=self.get_secure_cookie('username')
        if user:
            v=time.time()+10
            self.set_secure_cookie('username', user, expires=v)  #設定加密cookies

    def get_current_user(self):
        return self.get_secure_cookie('username')

    @authenticated #執行 self.curent_user,有值就登入使用者,無就去執行get_curent_user方法
    def get(self, *args, **kwargs):
        self.write('種子列表')
檢視
if user == 'zhanggen' and pwd=='123.com':
            v = time.time() + 10
            self.set_secure_cookie('username',user,expires=v)
            net_url=self.get_query_argument ('next',None)
            if not net_url:
                net_url='/index/'
            self.redirect(net_url)
            return
獲取即將跳轉的url

 

 

 

三、Tornado特色功能

Tornado有2大特色:原生支援WebSocket協議、非同步非阻塞的Web框架

 

1、WebSocket協議

HTTP和WebSocket協議都是基於TCP協議的,不同於HTTP協議的是WebSocket和服務端建立是長連線且連線成功之後,會建立一個全雙工通道,這時服務端可以向客戶端推送訊息,客戶端也可以向服務端推送訊息,其本質是保持TCP連線,在瀏覽器和服務端通過Socket進行通訊,由於WebSocket協議建立的是雙向全雙工通道,所以客戶端(瀏覽器)和服務端(Web框架)雙方都要支援WebSocket協議,Tornado原生支援這種協議;

 

1.0、WebSocket 和HTTP輪詢、長輪詢、長連線的區別?

HTTP輪詢:

每間隔1段時間 向服務端傳送http請求;

優點:後端程式編寫比較容易。
缺點:請求中有大半是無用,浪費頻寬和伺服器資源,有資料延遲。
例項:適於小型應用。

 

HTTP長輪詢:

每間隔1段時間 向服務端傳送http請求,伺服器接收到請求之後hold住本次連線1段時間,客戶端進入pending狀態;

如果在hold期間服務端有新訊息:會立即響應給客戶端;

如果沒有新訊息:超過hold時間,服務端會放開客戶端;

一直迴圈往復;

 

優點:在無訊息的情況下不會頻繁的請求。
缺點:伺服器hold連線會消耗資源
例項:WebQQ、WEB微信、Hi網頁版、Facebook IM。

 

HTTP長連線:

客戶端就傳送1個長連線的請求,伺服器端就能源源不斷地往客戶端輸入資料。

優點:訊息即時到達,客戶端無需重複傳送請求。
缺點:伺服器維護一個長連線會增加開銷。

 

WebSocket 協議:

服務端和客戶端連線建立全雙工通道一直不斷開;

優點:實現了實時通訊

缺點:舊版本瀏覽器不支援WebSocket協議,相容性不強;(這也行也是騰訊的WEB微信、WEBQQ不使用該協議的原因吧?)

 

 

1.1、實現WebSocket

實現WebScoket協議,需要遵循2項規則 建立WebSocket連線、服務端對封包和解包

 

a、建立連線

步驟1:客戶端向server端傳送請求中,請求資訊中攜帶Sec-WebSocket-Key: jnqJRYC7EgcTK8OCkVnu9w==\r\n;

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
    <div>
        <input type="text" id="txt"/>
        <input type="button" id="btn" value="提交" onclick="sendMsg();"/>
        <input type="button" id="close" value="關閉連線" onclick="closeConn();"/>
    </div>
    <div id="content"></div>

<script type="text/javascript">
    var socket = new WebSocket("ws://127.0.0.1:8002");

    socket.onopen = function () {
        /* 與伺服器端連線成功後,自動執行 */

        var newTag = document.createElement('div');
        newTag.innerHTML = "【連線成功】";
        document.getElementById('content').appendChild(newTag);
    };

    socket.onmessage = function (event) {
        /* 伺服器端向客戶端傳送資料時,自動執行 */
        var response = event.data;
        var newTag = document.createElement('div');
        newTag.innerHTML = response;
        document.getElementById('content').appendChild(newTag);
    };

    socket.onclose = function (event) {
        /* 伺服器端主動斷開連線時,自動執行 */
        var newTag = document.createElement('div');
        newTag.innerHTML = "【關閉連線】";
        document.getElementById('content').appendChild(newTag);
    };

    function sendMsg() {
        var txt = document.getElementById('txt');
        socket.send(txt.value);
        txt.value = "";
    }
    function closeConn() {
        socket.close();
        var newTag = document.createElement('div');
        newTag.innerHTML = "【關閉連線】";
        document.getElementById('content').appendChild(newTag);
    }

</script>
</body>
</html>
JavaScript客戶端

 

步驟2:服務端接收到客戶端請求,獲取請求頭,從中獲取Sec-WebSocket-Key;

 

步驟3:獲取到的Sec-WebSocket-Key對應的字元和magic_string進行拼接; 

magic_string = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'  #固定且全球唯一
value = headers['Sec-WebSocket-Key'] + magic_string

 

步驟4:設定響應頭,步驟3拼接完成之後的結果進行 base64加密;

ac = base64.b64encode(hashlib.sha1(value.encode('utf-8')).digest())
GET / HTTP/1.1\r\n

Host: 127.0.0.1:8002\r\n

Connection: Upgrade\r\n

Pragma: no-cache\r\n

Cache-Control: no-cache\r\n

Upgrade: websocket\r\n

Origin: http://localhost:63342\r\n

Sec-WebSocket-Version: 13\r\n

User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36\r\n

Accept-Encoding: gzip, deflate, br\r\n

Accept-Language: zh-CN,zh;q=0.8\r\n

Cookie: csrftoken=Om7ZrGEiMyYdx3F6xJmD5ycSWllhDc1D7SXRZKBoj7geGrQ3uwCHkCDdEJRWN1Zg; key="2|1:0|10:1513731498|3:key|12:emhhbmdnZW4=|664ad11ac6e040938f32893d7515f0680b171c39d0f99b918c3366a397f9331c"\r\n

Sec-WebSocket-Key: jnqJRYC7EgcTK8OCkVnu9w==\r\n


Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits\r\n\r\n'
WebSocket響應頭格式

 

b、資料傳輸(解包、封包

客戶端和服務端傳輸資料時,需要對資料進行【封包】和【解包】。客戶端的JavaScript類庫已經封裝【封包】和【解包】過程,但Socket服務端需要手動實現。

 

步驟1:Socket服務端接收客戶端傳送的資料,並對其解包;

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
    <div>
        <input type="text" id="txt"/>
        <input type="button" id="btn" value="提交" onclick="sendMsg();"/>
        <input type="button" id="close" value="關閉連線" onclick="closeConn();"/>
    </div>
    <div id="content"></div>

<script type="text/javascript">
    var socket = new WebSocket("ws://127.0.0.1:8002");

    socket.onopen = function () {
        /* 與伺服器端連線成功後,自動執行 */

        var newTag = document.createElement('div');
        newTag.innerHTML = "【連線成功】";
        document.getElementById('content').appendChild(newTag);
    };

    socket.onmessage = function (event) {
        /* 伺服器端向客戶端傳送資料時,自動執行 */
        var response = event.data;
        var newTag = document.createElement('div');
        newTag.innerHTML = response;
        document.getElementById('content').appendChild(newTag);
    };

    socket.onclose = function (event) {
        /* 伺服器端主動斷開連線時,自動執行 */
        var newTag = document.createElement('div');
        newTag.innerHTML = "【關閉連線】";
        document.getElementById('content').appendChild(newTag);
    };

    function sendMsg() {
        var txt = document.getElementById('txt');
        socket.send(txt.value);
        txt.value = "";
    }
    function closeConn() {
        socket.close();
        var newTag = document.createElement('div');
        newTag.innerHTML = "【關閉連線】";
        document.getElementById('content').appendChild(newTag);
    }

</script>
</body>
</html>
JavaScript類庫已經封裝【封包】和【解包】過程

 

conn, address = sock.accept()
    data = conn.recv(1024)
    headers = get_headers(data)
    response_tpl = "HTTP/1.1 101 Switching Protocols\r\n" \
                   "Upgrade:websocket\r\n" \
                   "Connection:Upgrade\r\n" \
                   "Sec-WebSocket-Accept:%s\r\n" \
                   "WebSocket-Location:ws://%s%s\r\n\r\n"

    value = headers['Sec-WebSocket-Key'] + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
    ac = base64.b64encode(hashlib.sha1(value.encode('utf-8')).digest())
    response_str = response_tpl % (ac.decode('utf-8'), headers['Host'], headers['url'])
    conn.send(bytes(response_str, encoding='utf-8'))
Socket解包+迴應完成握手

 

 

 步驟2:Socket服務端對傳送給服務端的資料進行封包;

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import socket
import base64
import hashlib


def get_headers(data):
    """
    將請求頭格式化成字典
    :param data:
    :return:
    """
    header_dict = {}
    data = str(data, encoding='utf-8')

    header, body = data.split('\r\n\r\n', 1)
    header_list = header.split('\r\n')
    for i in range(0, len(header_list)):
        if i == 0:
            if len(header_list[i].split(' ')) == 3:
                header_dict['method'], header_dict['url'], header_dict['protocol'] = header_list[i].split(' ')
        else:
            k, v = header_list[i].split(':', 1)
            header_dict[k] = v.strip()
    return header_dict


def send_msg(conn, msg_bytes):
    """
    WebSocket服務端向客戶端傳送訊息
    :param conn: 客戶端連線到伺服器端的socket物件,即: conn,address = socket.accept()
    :param msg_bytes: 向客戶端傳送的位元組
    :return:
    """
    import struct

    token = b"\x81"
    length = len(msg_bytes)
    if length < 126:
        token += struct.pack("B", length)
    elif length <= 0xFFFF:
        token += struct.pack("!BH", 126, length)
    else:
        token += struct.pack("!BQ", 127, length)

    msg = token + msg_bytes
    conn.send(msg)
    return True


def run():
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.bind(('127.0.0.1', 8002))
    sock.listen(5)

    conn, address = sock.accept()
    data = conn.recv(1024)
    headers = get_headers(data)
    response_tpl = "HTTP/1.1 101 Switching Protocols\r\n" \
                   "Upgrade:websocket\r\n" \
                   "Connection:Upgrade\r\n" \
                   "Sec-WebSocket-Accept:%s\r\n" \
                   "WebSocket-Location:ws://%s%s\r\n\r\n"

    value = headers['Sec-WebSocket-Key'] + '258EAFA5-E914-47