PythonWEB框架之Tornado

前言

Tornado(龍捲風)和Django同樣是Python中比較主流的web框架,javascript

知識鋪墊:css

什麼是別人所說的web server /web服務器?html

全部Python寫的web服務器本質是就是1個符合wsgi協議標準的 socket(例如:Python的wsgiref模塊就能夠實現1個web服務)前端

web server負責 監聽在某1個端口、接收客戶端的鏈接,分割http的請求頭和請求體..等底層工做java

最後封裝好請求頭和響應頭的信息,傳給web框架node

讓咱們在使用web框架開發web應用程序的時候更加快捷和方便,就能夠 request.xx 、render, HttpResponse,redirectpython

 

什麼是別人說的web 應用/web框架?mysql

web框架負責web應用邏輯處理的部分,一般作了一些代碼封裝,能夠更加人性性化得讓咱們使用(Django/Flashk。。。)jquery

 

web server 和 web 框架如何結合?git

若是想要讓你的web程序運行起來,2者缺一不可,若是你使用的web框架不包含 web server就須要搭配第3方的模塊,例如Django在默認狀況下socket藉助的是wsgiref模塊

可是有的web框架包含了web server(tornado)

 

 

 

以Django爲表明的python web 框架搭配的web server通常爲gunicorn/uwsgi...這些都是基於多進程和多線程工做的,這種工做模式的缺陷是服務器每接受1個請求都會啓動1個線程/進程進行處理,若是併發量過大就須要產生大量的線程/進程,服務器不能無限制得開線程和進程,因此知足不了用戶量大、高併發的應用場景

 

Tornado 和如今的主流 Web 服務器框架也有着明顯的區別:

Tornado是服務器和web框架的結合體,自帶web server,而且在框架內部也能夠實現了異步、非阻塞、且對WebSocket協議自然支持;

 

1、Tornado框架的基本組成

Tonado由 web server(基於epoll機制的IO多路複用性IO的socket)、路由系統、視圖、模板語言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功能對比

 

2、Tornado自帶功能

一、Tornado執行流程

若是不明白Tornado的IO多路複用機制,做爲1個使用者來講將是一件很失敗的事情;

Tornado是基於epoll實現的;

import tornado.web
import tornado.ioloop

class IndexHandler(tornado.web.RequestHandler):
    def get(self):
        self.write('hello Martin!')

if __name__ == '__main__':
    app=tornado.web.Application([(r"/",IndexHandler)])
    app.listen(8000)                        #監聽可是不綁定
    tornado.ioloop.IOLoop.current().start()#對epoll機制的封裝,實例化1個epoll實例,將socket交給epoll進行監聽管理

 

第一步:tornado.ioloop.IOLoop.current().start()執行,實例化1個epoll容器,將socket0交給epoll進行循環監聽

第二步:客戶端A訪問 /index  -->  http://127.0.0.1:8888/index/對服務器進行鏈接

第三步:因爲客戶端A鏈接了Tornado開啓的socket,全部epoll循環發現了有socket0可讀,就把客戶A鏈接socket0的socket添加到epoll容器進行循環監聽

第四步:若是循環監聽發現有客戶socket有可讀的操做,就響應客戶端(走路由--》視圖--》模板渲染....)

PS:

Tornado經過1個事件循環監聽,監聽到哪一個socket能夠操做,Tornado就操做哪一個!只用了1個線程就可對多個請求進行處理;

可是Tornado的單線程有個致命缺陷,若是咱們在響應客戶端的過程當中(路由、視圖、查庫、模板渲染..)出現了long IO,即便另外一個客戶端socket可操做,也必須排隊等待.......;

因而這就爲何咱們要在視圖中作異步的緣由....

 

 

 

 

配置文件:

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.一、動態路由(url傳參數)

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

2.二、域名匹配 

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

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

2.三、反向生成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')
視圖

 

 

三、視圖

tornado的視圖纔有CBV模式,url匹配成功以後先  視圖執行順序爲 initialize 、prepare、get/post/put/delete(視圖)、finish

必定要注意這3個鉤子方法:

#!/bin/env python
# -*- coding: UTF-8 -*-

"""
Copyright (c) 2016 SensorsData, Inc. All Rights Reserved
@author padme(jinsilan@sensorsdata.cn)
@brief

封裝些基本的方法 還有logger
mysql> desc user_info;
+----------+--------------+------+-----+---------+-------+
| Field    | Type         | Null | Key | Default | Extra |
+----------+--------------+------+-----+---------+-------+
| name     | varchar(100) | NO   | PRI | NULL    |       |
| cname    | varchar(100) | NO   |     | NULL    |       |
| mail     | varchar(100) | NO   |     | NULL    |       |
| password | varchar(128) | YES  |     | NULL    |       |
| salt     | varchar(20)  | YES  |     | NULL    |       |
| role     | varchar(20)  | YES  |     | NULL    |       |
| comment  | text         | YES  |     | NULL    |       |
+----------+--------------+------+-----+---------+-------+
7 rows in set (0.00 sec)
"""
import copy
import datetime
import hashlib
import json
import logging
import pprint
import pymysql
import os
import random
import time
import threading
import tornado.web
import tornado.escape
import sys
import ldap

sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../')))
import conf
from data import group_cache, cache

RESERVED_USERS = {
    "monitor": {"name": "monitor", "password": "968df05ea257081d6d7831a3fc4c4145", "role":
        "super", "cname": "monitor郵件組", "mail": "monitor@sensorsdata.cn"},
    "sale": {"name": "sale", "password": "128ea23fa279cf2d1fa26a1522cc2a53", "role":
        "normal", "cname": "sale", "mail": "sale@sensorsdata.cn"},
    "ztxadmin": {"name": "ztxadmin", "password": "6934fd6089194c9f9ec0e1b011045abf", "role":
        "admin", "cname": "張天曉admin", "mail": "zhangtianxiao@sensorsdata.cn"},
    "jenkins": {"name": "jenkins", "password": "przs7j0ubzvvgu9ofw48a55n813edxzk",
                "role": "normal", "cname": "jenkins專用", "mail": "jinsilan@sensorsdata.cn"}
}

RESERVED_USER_TOKENS = {
    "968df05ea257081d6d7831a3fc4c4145": {"name": "monitor", "role": "super",
                                         "cname": "monitor郵件組"},
    "128ea23fa279cf2d1fa26a1522cc2a53": {"name": "sale", "role": "normal", "cname": "sale"},
    "6934fd6089194c9f9ec0e1b011045abf": {"name": "ztxadmin", "role": "admin", "cname": "張天曉admin"},
    "przs7j0ubzvvgu9ofw48a55n813edxzk": {"name": "jenkins", "role": "normal", "cname": "jenkins專用"},
}

counter_map = {}
snapshots = []
lock = threading.Lock()
uptime = time.time() * 1000


class DatetimeSerializer(json.JSONEncoder):
    """
    實現 date 和 datetime 類型的 JSON 序列化,以符合 SensorsAnalytics 的要求。
    """

    def default(self, obj):
        if isinstance(obj, datetime.datetime):
            head_fmt = "%Y-%m-%d %H:%M:%S"
            return "{main_part}.{ms_part}".format(
                main_part=obj.strftime(head_fmt),
                ms_part=int(obj.microsecond / 1000))
        elif isinstance(obj, datetime.date):
            fmt = '%Y-%m-%d'
            return obj.strftime(fmt)
        return json.JSONEncoder.default(self, obj)


# 最多保留100個snapshot
MAX_SNAPSHOT_NUM = 100
MIN_SNAPSHOT_INTERVAL = 60 * 1000


class MysqlCursorWrapper():
    def __init__(self, mysql_conf):
        self.mysql_con = pymysql.connect(**mysql_conf)
        self.cursor = self.mysql_con.cursor()

    def __enter__(self):
        return self.cursor

    def __exit__(self, *exc_info):
        self.cursor.close()
        self.mysql_con.close()


class BaseHandler(tornado.web.RequestHandler):
    '''
    1. 自帶counter
    2. 自帶logger
    3. 自帶mysql cursor(短時間就每一個查詢發起一個鏈接吧 反正目前qps不高 搞個鏈接池也總會超時的
    '''

    def send_error(self, status_code=500, **kwargs):#對tornado 報錯進行了二次封裝
        # self.set_status(status_code)
        self.logger.warn('send error: %d[%s]' % (status_code, kwargs.get('reason', 'unknown reason')))
        return super().send_error(status_code, **kwargs)

    def get_json_body(self):#獲取 json json.loads
        self.logger.debug(self.request.body)
        data = False
        try:
            data = json.loads(self.request.body.decode('utf8'))
            self.logger.debug(data)
        except:
            self.send_error(400, reason='Invalid json data')
        return data

    def redirect_login(self, error, url=None):#跳轉的方法
        if not url:
            url = self.request.uri
        self.redirect('/login?next=%s&error=%s' % (tornado.escape.url_escape(url), tornado.escape.url_escape(error)))

    def check_admin(self):#檢測是不是admin角色
        if self.role != 'admin':
            self.send_error(401)
            raise Exception('required login')

    def clear_auth(self):     #清除cookie 的auth鍵中的 權限角色
        self.clear_cookie('auth')
        self.role = 'normal'
        self.ldapPass = False
        self.ldapRole = 'nobody'

    def initialize(self, url):#tornado自帶的方法 初始化 把用戶角色還
        self.logger = logging
        self.tag = url.lstrip('/').replace('([0-9]+)', 'id').replace('/', '_').replace('(.*)', 'name')
        print(self.tag)
        self.user = '未登陸'
        self.role = 'normal'
        self.ldapPass = False
        self.ldapRole = 'nobody'

    def get_gitlab_role(self, user): #獲取用戶在 ldap中的角色
        return group_cache.get_gitlab_role(user)

    def prepare_ldap(self, user, pwd):#使用用戶提交的 用戶名、密碼去ldap服務器 認證
        return group_cache.ldap_simple_authenticate(user, pwd)

    def prepare(self): #tornado自帶的鉤子順序 initialize 、prepare、get/post/put/delete、on_finish;
        k = "%s_num" % self.tag
        counter_map[k] = counter_map.get(k, 0) + 1
        self.start_time = time.time() * 1000.0
        auth_str = self.get_secure_cookie("auth")
        if auth_str:
            auth = json.loads(auth_str.decode('utf8'))
            self.user = auth['u']
            self.role = auth['r']

        elif self.get_argument('token', None):#從get請求的url參數中獲取token
            token = self.get_argument('token')
            if token in RESERVED_USER_TOKENS:
                user_result = RESERVED_USER_TOKENS[token]
                if user_result:
                    self.user = user_result['name']
                    self.role = user_result['role']

                # 適配銷售建立接口
                if self.user == 'sale':
                    self.user = self.get_argument('user')
                    if not cache.global_user_infos:
                        cache.update_global_user_infos(self)
                    if not self.user in cache.global_user_infos:
                        self.user = "未登陸"

        if not self.request.path.startswith('/login'):
            logging.getLogger('auth').info('%s %s %s %s\n%s' \
                                           % (self.user, self.role, self.request.method, self.request.uri,
                                              self.request.body))

    def on_finish(self):
        k = "%s_succeed_num" % self.tag
        counter_map[k] = counter_map.get(k, 0) + 1
        k = "%s_interval" % self.tag
        interval = time.time() * 1000.0 - self.start_time
        counter_map[k] = counter_map.get(k, 0) + interval
        if random.randint(0, 10) != 0:
            return
        self.add_snapshot()

    def add_snapshot(self):
        with lock:
            s = copy.deepcopy(counter_map)
            s['time'] = time.time() * 1000.0
            snapshots.append(s)
            while len(snapshots) > MAX_SNAPSHOT_NUM:
                snapshots.pop(0)

    def get_mysql_cursor(self, mysql_conf=conf.mysql_conf):
        return MysqlCursorWrapper(mysql_conf)

    def query_args(self, sql, args, mysql_conf=conf.mysql_conf):
        '''返回a list of dict'''
        self.logger.debug('query mysql: %s;args: %s' % (sql, args))
        ret = []
        with self.get_mysql_cursor(mysql_conf) as cursor:
            cursor.execute(sql, args)
            columns = [x[0] for x in cursor.description]
            for row in cursor.fetchall():
                d = dict(zip(columns, row))
                ret.append(d)
            cursor.execute('commit')
        self.logger.debug('ret %d lines. top 1:\n%s' % (len(ret), pprint.pformat(ret[:1], width=200)))
        return ret

    def update_args(self, sql, args, mysql_conf=conf.mysql_conf):
        '''返回id'''
        self.logger.debug('update mysql: %s;args: %s' % (sql, args))
        with self.get_mysql_cursor(mysql_conf) as cursor:
            cursor.execute(sql, args)
            lastrow = cursor.lastrowid
            cursor.execute('commit')
            return lastrow

    def update(self, sql, mysql_conf=conf.mysql_conf):
        return self.update_args(sql, None, mysql_conf)

    def query(self, sql, mysql_conf=conf.mysql_conf):
        return self.query_args(sql, None, mysql_conf)

    def to_json(self, d):
        return json.dumps(d, cls=DatetimeSerializer)

    def render(self, name, **args):
        args['current_url'] = self.request.path
        args['current_user'] = self.user
        args['current_role'] = self.role
        return super().render(name, **args)

    def check_auth(self, customer_id, customer_info=None):
        '''
        檢查權限:super有全部權限;其餘只能看本身的客戶(經過user_id銷售和customer_success來標記)
        '''
        if not customer_info:
            sql = 'select * from customer_info where visible = true AND customer_id = "%s"' % customer_id
            customer_info = self.query(sql)[0]
        # members = [x for x in customer_info['members'].split(',') if x]
        if self.role == 'super' or self.check_customer_member(customer_info):
            return True
        self.logger.warn('bad auth: %s[%s] cannot see %s[%s/%s]' \
                         % (self.user, self.role, customer_id, customer_info['user_id'],
                            customer_info['customer_success']))
        return False

    def check_customer_member(self, customer_info):
        if not customer_info['members']:
            members = []
        elif type(customer_info['members']) == str:
            members = [x for x in customer_info['members'].split(',') if x]
        else:
            members = customer_info['members']
        return customer_info['user_id'] == self.user or customer_info[
                                                            'customer_success'] == self.user or self.user in members


def redirect_if_not_login(func):
    '''跳轉到登陸頁面'''

    def _decorator(self, *args, **kwargs):
        if self.user == '未登陸':
            if not self.ldapPass:
                self.logger.error('not login!')
                return self.redirect_login('請先登陸才能夠看相關內容')

        return func(self, *args, **kwargs)

    return _decorator


def error_if_not_login(func):
    '''檢查是否登陸 若是沒有則返回401'''

    def _decorator(self, *args, **kwargs):
        if self.user == '未登陸':
            if not self.ldapPass:
                return self.send_error(401)
        return func(self, *args, **kwargs)

    return _decorator


def error_if_not_admin(func):
    '''檢查是否admin 若是沒有則返回401'''

    def _decorator(self, *args, **kwargs):
        if self.role != 'admin' and self.role != 'super':
            if self.ldapRole != 'admin' and self.ldapRole != 'super':
                return self.send_error(401)

        return func(self, *args, **kwargs)

    return _decorator


def error_if_not_super(func):
    '''檢查是否super 若是沒有則返回401'''

    def _decorator(self, *args, **kwargs):
        if self.role != 'super':
            if self.ldapRole != 'super':
                return self.send_error(401)
        return func(self, *args, **kwargs)

    return _decorator


class StatusHandler(BaseHandler):
    def get(self):
        self.add_snapshot()
        first = {}
        with lock:
            if not snapshots:
                second = {}
            else:
                second = snapshots[-1]
                for x in reversed(snapshots[:-1]):
                    if second['time'] - x['time'] > MIN_SNAPSHOT_INTERVAL:
                        first = x
        self.logger.debug('first=%s second=%s' % (first, second))
        if first and second:
            interval = (second['time'] - first['time']) / 1000.0
        else:
            interval = 0
        if 'time' in first:
            first_date = datetime.datetime.fromtimestamp(first['time'] / 1000.0).strftime('%Y-%m-%d %H:%M:%S')
        else:
            first_date = 'unknown'
        if 'time' in second:
            second_date = datetime.datetime.fromtimestamp(second['time'] / 1000.0).strftime('%Y-%m-%d %H:%M:%S')
        else:
            second_date = 'unknown'
        tags = [x[:-4] for x in second if x.endswith('_num') and not x.endswith('_succeed_num')]
        tags.remove(self.tag)
        self.logger.debug('tags=%s' % tags)
        ret = {'from': first_date, 'to': second_date}
        for t in tags:
            args = {}
            for (prefix, v) in [('first', first), ('second', second)]:
                for (suffix, alias) in [('num', 'n'), ('succeed_num', 's'), ('interval', 'i')]:
                    args['%s_%s' % (prefix, alias)] = v.get(suffix, 0)
            if args['first_n']:
                ret[t] = {
                    'query': args['second_n'] - args['first_n'],
                    'success': args['second_s'] - args['first_s'],
                    'query_per_minutes': (args['second_n'] - args['first_n']) * 60 / interval,
                    'success_rate': (args['second_s'] - args['first_s']) / (args['second_n'] - args['first_n']) if
                    args['second_n'] > args['first_n'] else '-',
                    'avg_interval': (args['second_i'] - args['first_i']) / (args['second_s'] - args['first_s']) if
                    args['second_s'] > args['first_s'] else '-',
                }
            else:
                ret[t] = {
                    'query': args['second_n'],
                    'success': args['second_s'],
                    'success_rate': args['second_s'] / args['second_n'] if args['second_n'] != 0 else '-',
                    'query_per_minutes': '-',
                    'avg_interval': args['second_i'] / args['second_s'] if args['second_s'] != 0 else '-',
                }
        self.write(self.to_json(ret))


class HomeHandler(BaseHandler): #首頁視圖
    def get(self):
        self.render('home.html')


class LoginHandler(BaseHandler): #登陸頁面的視圖
    def get(self):
        self.clear_auth()
        param = {'error': self.get_argument('error', None)}
        self.render('login.html', **param)

    def post(self):

        username = self.get_argument("username", "")
        password = self.get_argument("password", "")
        remember = self.get_argument('remember', '')

        user_result = None
        if username in RESERVED_USERS:
            user_result = RESERVED_USERS[username]

        # 保留用戶
        if user_result:
            token = self.get_argument('token', None)
            if token == user_result['password']:
                auth = {'u': username, 'r': user_result['role'], 'd': datetime.datetime.now().strftime('%Y-%m-%d')}#cookie中的認證信息
                if remember:
                    self.set_secure_cookie('auth', json.dumps(auth), expires_days=conf.cookie_expire_day)
                else:
                    self.set_secure_cookie('auth', json.dumps(auth))
                self.redirect(self.get_argument('next', '/'))
                return
            else:
                self.logger.warn('invalid password, given %s result %s' % (token, user_result['password']))
                self.redirect_login('密碼錯誤', self.get_argument('next', '/'))
                return

        # 查ldap
        self.ldapPass, self.ldapRole = self.prepare_ldap(username, password)
        if not self.ldapPass:  # 沒查到
            if self.ldapRole != "locked":
                self.logger.warn('user %s wrong password', username)
                self.redirect_login('用戶密碼輸入錯誤', self.get_argument('next', '/'))
            else:
                self.logger.warn('user %s has been locked', username)
                self.redirect_login('ldap用戶被鎖定,請聯繫管理員解鎖', self.get_argument('next', '/'))
            return

        role = self.ldapRole  #查到了ldap的角色

        if role != "nobody": #若是 不是匿名角色 #開始寫cokie了
            auth = {'u': username, 'r': role, 'd': datetime.datetime.now().strftime('%Y-%m-%d')}
            if remember:
                self.set_secure_cookie('auth', json.dumps(auth), expires_days=conf.cookie_expire_day)
            else:
                self.set_secure_cookie('auth', json.dumps(auth))
            self.redirect(self.get_argument('next', '/'))

            return

        self.logger.warn('user %s not in ldap or not in group', username)
        self.redirect_login('暫不支持你所在的郵件組', self.get_argument('next', '/'))
base_handler.py

 

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.一、請求相關

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.二、響應相關

self.write() :響應字符串

self.render():響應頁面

self.redirect():頁面跳轉

 

四、模板語言

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

View Code

4.一、登陸頁面

#準備安裝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.二、引入靜態文件

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

 

經過static_url()方法引入靜態文件的好處: 

一、使用static_url()能夠不用考慮靜態文件修改以後形成引用失效的狀況;

二、還會生成靜態文件url會有一個v=...的參數,這是tornado根據靜態文件MD5以後的值,若是後臺的靜態文件修改,這個值就會變化,前端就會從新向後臺請求靜態文件,保證頁面實時更新,不引用瀏覽器緩存;

 

4.三、上下文對象

若是模板語言中聲明瞭變量,上下文對象必須對應傳值,若是沒有就設置爲空,不然會報錯;

self.render('login.html',**{'erro_msg':'' }) #模板中聲明瞭變量,視圖必須傳值,若是沒有就設置爲空;
View Code

 

五、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() %}

 

六、cokies

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

6.一、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.二、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.三、@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

 

 

 

3、Tornado特點功能

Tornado有2大特點:原生支持WebSocket協議、異步非阻塞的Web框架

 

一、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.一、實現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-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'))

    while True:
        try:
            info = conn.recv(8096)
        except Exception as e:
            info = None
        if not info:
            break
        payload_len = info[1] & 127
        if payload_len == 126:
            extend_payload_len = info[2:4]
            mask = info[4:8]
            decoded = info[8:]
        elif payload_len == 127:
            extend_payload_len = info[2:10]
            mask = info[10:14]
            decoded = info[14:]
        else:
            extend_payload_len = None
            mask = info[2:6]
            decoded = info[6:]

        bytes_list = bytearray()
        for i in range(len(decoded)):
            chunk = decoded[i] ^ mask[i % 4]
            bytes_list.append(chunk)
        body = str(bytes_list, encoding='utf-8')
        send_msg(conn, body.encode('utf-8'))

    sock.close()


if __name__ == '__main__':
    run()
View Code

 

WebSocket協議參考博客:http://www.cnblogs.com/wupeiqi/p/6558766.html

 

 

1.二、基於Tornado實現Web聊天室

Tornado是一個支持WebSocket的優秀框架,固然Tornado內部封裝功能更加完整,如下是基於Tornado實現的聊天室示例:

 

模板語言

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Python聊天室</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="container" style="border: 1px solid #dddddd;margin: 20px;min-height: 500px;">

    </div>

    <script src="/static/jquery-3.2.1.min.js"></script>
    <script type="text/javascript">
        $(function () {
            wsUpdater.start();
        });

        var wsUpdater = {
            socket: null,
            uid: null,
            start: function() {
                var url = "ws://127.0.0.1:8009/chat";
                wsUpdater.socket = new WebSocket(url);
                wsUpdater.socket.onmessage = function(event) {
                    console.log(event);
                    if(wsUpdater.uid){
                        wsUpdater.showMessage(event.data);
                    }else{
                        wsUpdater.uid = event.data;
                    }
                }
            },
            showMessage: function(content) {
                $('#container').append(content);
            }
        };

        function sendMsg() {
            var msg = {
                uid: wsUpdater.uid,
                message: $("#txt").val()
            };
            wsUpdater.socket.send(JSON.stringify(msg));
        }

</script>

</body>
</html>
index.html
<div style="border: 1px solid #dddddd;margin: 10px;">
    <div>遊客{{uid}}</div>
    <div style="margin-left: 20px;">{{message}}</div>
</div>
message.html

 

視圖

#!/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-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'))

    while True:
        try:
            info = conn.recv(8096)
        except Exception as e:
            info = None
        if not info:
            break
        payload_len = info[1] & 127
        if payload_len == 126:
            extend_payload_len = info[2:4]
            mask = info[4:8]
            decoded = info[8:]
        elif payload_len == 127:
            extend_payload_len = info[2:10]
            mask = info[10:14]
            decoded = info[14:]
        else:
            extend_payload_len = None
            mask = info[2:6]
            decoded = info[6:]

        bytes_list = bytearray()
        for i in range(len(decoded)):
            chunk = decoded[i] ^ mask[i % 4]
            bytes_list.append(chunk)
        body = str(bytes_list, encoding='utf-8')
        send_msg(conn, body.encode('utf-8'))

    sock.close()


if __name__ == '__main__':
    run()
Views

 

 

二、異步非阻塞介紹

Web框架分阻塞式和異步非阻塞2種;

 

2.1.阻塞式IO(Django、Flask、Bottle)

大多數的Web框架都是阻塞式的,體如今第1個請求到達服務端,在服務端未處理完第1個請求期間,後面鏈接過來的第2個、第3個、第4個、第5個、第6個、第多少個請求都得一直排隊等待;

解決方案:

開啓多線程/多進程:在1個請求過程當中開啓N個線程幹活,以此來加速當前請求處理的過程,提升服務器能夠處理多個請求的能力(大併發能力);

import tornado.ioloop
import time
import tornado.web
import tornado.websocket
from tornado.httpserver import HTTPServer
class IndexHadlar(tornado.web.RequestHandler):
    def get(self):
        print('請求開始')
        time.sleep(10)
        self.write('hello,world ')
        print("請求結束")
application=tornado.web.Application([
    (r'/index/',IndexHadlar)
])


if __name__ == '__main__':
    # 單線程模式
    # application.listen(8888)
    # tornado.ioloop.IOLoop.instance().start()
    # 多線程模式
    server=HTTPServer(application)
    server.bind(8888)
    server.start(3) #開啓4個進程
    tornado.ioloop.IOLoop.instance().start()
Tornado多進程模式(僅支持Linux平臺)

缺點:浪費系統資源

 

 

 2.二、Tornado異步非阻塞(Tornado/NodeJS)

服務器(遵循wsgi標準的soket)作到異步非阻塞(在客戶端socket鏈接Server端Socket時)

服務器結合操做系統的IO多路復型IO(select/poll/epoll)作到N個客戶端Socket鏈接到服務端Socket時,把N個socket所有循環監聽起來,哪一個socket可讀、可寫就切換到哪一個socket?

這個是Tornado自帶的功能,能夠作到N個客戶端Soket(請求)鏈接進來Server端的socket能夠全接收進來。

 

在Web框架作到異步非阻塞(web框架的代碼)

注意:若是隻是在客戶端socket鏈接Server端socket階段作到了異步非阻塞,可是在接收完了客戶端socket,開始執行web框架裏的代碼階段遇到了long IO,因爲tornado的單線程,它仍然會被執行代碼階段的IO阻塞住

 

@tornado.web.gen.coroutine使用協程裝飾器

yield future()對象

來保證處理當前客戶請求階段,tornado單線程不會被阻塞。

 

 

1.客戶端發送請求若是請求內容不涉及IO操做(鏈接數據、還得去其餘網站獲取內容)服務端直接響應客戶端;

 

2.若是請求內容涉及IO操做,服務端把本次鏈接的socket信息添加到socket監聽列表中監聽起來;

而後去鏈接其它socket(數據庫、其它站點)因爲是不阻塞的因此服務端把此次發送socket信息也監聽起來;(一直循環監聽,直到socket監聽列表中的socket發生變化)

 

3.把socket所有監聽以後,就能夠去繼續接收其它請求了,若是檢測到socket監聽列表中的socket有變化(有數據返回),找到對應socket響應數據,並從socket監聽列表中剔除;

 

小結:

Tornado的異步非阻塞,本質上是請求到達視圖 一、先yield 1個Future對象  二、 IO多路複用模塊把該socket添加到監聽列表循環監聽起來;三、 循環監聽過程當中哪1個socket發生變化有response,執行 Future.set_result(response),請求至此返回結束,不然socket鏈接一直不斷開,IO多路複用模塊一直循環監聽socket是否發生變化?;

 

當發送GET請求時,因爲方法被@gen.coroutine裝飾且yield 一個 Future對象,那麼Tornado會等待,等待用戶向future對象中放置數據或者發送信號,若是獲取到數據或信號以後,就開始執行doing方法。

異步非阻塞體如今當在Tornaod等待用戶向future對象中放置數據時,還能夠處理其餘請求。

注意:在等待用戶向future對象中放置數據或信號時,此鏈接是不斷開的。

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import tornado.ioloop
import time
import tornado.web
import tornado.websocket
from tornado import gen             #導入
from tornado.concurrent import Future
import time

class IndexHadlar(tornado.web.RequestHandler):
    @gen.coroutine #coroutine(攜程裝飾器)
    def get(self):
        print('請求開始')
        future=Future()
        tornado.ioloop.IOLoop.current().add_timeout(time.time()+10,self.doing)
        yield future #yield 1個future對象,IO以後自動切換到doing方法執行;

    def doing(self):
        self.write('請求完成')
        self.finish()           #關閉鏈接


application=tornado.web.Application([
    (r'/index/',IndexHadlar)
])


if __name__ == '__main__':
    # 單進程模式
    application.listen(8888)
    tornado.ioloop.IOLoop.instance().start()
Tornado異步非阻塞模式

 

注意:代碼運行以後,在瀏覽器打開對個窗口測試,模擬多個用戶請求。才能看到tornado異步非阻塞的效果。

 

2.三、Tornado httpclient類庫

若是服務端接受到客戶端的請求,須要去其餘API獲取數據,再響應給客戶端,這就涉及到了IO操做,Tornado提供了httpclient類庫用於發送Http請求,其配合Tornado的異步非阻塞使用。

#!/usr/bin/env python
# -*- coding:utf-8 -*-

import tornado.web
from tornado.web import RequestHandler
from tornado import gen
from tornado import httpclient


class AsyncHandler(RequestHandler):
    @gen.coroutine
    def get(self):
        print('收到報警')
        http=httpclient.AsyncHTTPClient()
        yield http.fetch('https://github.com',self.done)

    def done(self,respose,*args,**kwargs):
        print(respose)
        self.write('推送成功')
        self.finish()


application = tornado.web.Application([
    (r"/zhanggen/", AsyncHandler),
])

if __name__ == '__main__':
    application.listen(8888)
    tornado.ioloop.IOLoop.instance().start()
httpclient模塊

 

 

2.三、Tornado-MySQL類庫

若是服務端接收到客戶端請求,須要鏈接數據庫再把查詢的結果響應客戶端,這個過程當中鏈接數據、發送查詢SQL、接收數據庫返回結果 都會遇到IO阻塞、耗時的問題,因此Tornado提供了Tornado-MySQL模塊(對PyMySQL進行二次封裝),讓咱們在使用數據庫的時候也能夠作到異步非阻塞。

# yield cur.execute("SELECT name,email FROM web_models_userprofile where name=%s", (user,))
方式1

 

方式1 須要對每一個IO操做分別yeild,操做起來比較繁瑣,因此能夠經過task的方式把IO操做封裝到函數中統一進行異步處理(不管什麼方式本質都會yelid 1個Future對象);

#!/usr/bin/env python
# -*- coding:utf-8 -*-
"""
須要先安裝支持異步操做Mysql的類庫:
    Tornado-MySQL: https://github.com/PyMySQL/Tornado-MySQL#installation

    pip3 install Tornado-MySQL

"""

import tornado.web
from tornado import gen

import tornado_mysql
from tornado_mysql import pools

POOL = pools.Pool(
    dict(host='127.0.0.1', port=3306, user='root', passwd='123', db='cmdb'),
    max_idle_connections=1,
    max_recycle_sec=3)


@gen.coroutine
def get_user_by_conn_pool(user):
    cur = yield POOL.execute("SELECT SLEEP(%s)", (user,))
    row = cur.fetchone()
    raise gen.Return(row)


@gen.coroutine
def get_user(user):
    conn = yield tornado_mysql.connect(host='127.0.0.1', port=3306, user='root', passwd='123', db='cmdb',
                                       charset='utf8')
    cur = conn.cursor()
    # yield cur.execute("SELECT name,email FROM web_models_userprofile where name=%s", (user,))
    yield cur.execute("select sleep(10)")
    row = cur.fetchone()
    cur.close()
    conn.close()
    raise gen.Return(row)


class LoginHandler(tornado.web.RequestHandler):
    def get(self, *args, **kwargs):
        self.render('login.html')

    @gen.coroutine
    def post(self, *args, **kwargs):
        user = self.get_argument('user')
        data = yield gen.Task(get_user, user)  #把函數添加任務
        if data:
            print(data)
            self.redirect('http://www.oldboyedu.com')
        else:
            self.render('login.html')


application = tornado.web.Application([
    (r"/login", LoginHandler),
])

if __name__ == "__main__":
    application.listen(8888)
    tornado.ioloop.IOLoop.instance().start()
方式2

 

 

三、使用 Tornado異步非阻塞功能小結:

一、視圖之上加@gen.coroutine裝飾器

二、yield Future()

三、Future對象的set_result()執行請求會當即返回;

 

 

 

 

4、Tornado功能擴展

一、session

Tornado原生不帶session,因此須要自定製session框架;

自定製session知識儲備

a、python的 __getitem__、__setitem__,__delitem__內置方法

class Foo(object):
    def __getitem__(self, item):
        return 666
    def __setitem__(self, key, value):
        pass
    def __delitem__(self, key):
        pass

obj=Foo()
print(obj['name'])   #Python的[]語法,會自動執行對象的__getitem__方法;

obj['name']=888      #會自動執行對象的__setitem__方法

del obj['name']     #會自動執行對象的__delitem__方法

class Yuchao(object):
    def __init__(self,num):
        self.num=num
    def __add__(self, other):
        return self.num+other.num

'''
python 內置的方法
__new__
__init__
__add__
__getitem__
__setitem__
__delitem__
__call__

'''
a=Yuchao('5')
b=Yuchao('5')

print(a+b)
View Code

 

b、Tornado在請求處理以前先執行initialize方法;

模板語言

<!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>
                  <p>{{msg}}</p>
            </form>
        </div>
    </div>
</div>
</body>
</html>
login.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h2>首頁</h2>
<h1>循環列表</h1>
<ul>
    {% for item in userlist %}
    <li>{{item}} </li>
    {% end %}
    <!--注意不是Django裏面的enfor直接end if 也是end-->
</ul>
<h1>列表索引取值</h1>
{{userlist[1]}}

<h1>循環字典</h1>
<ul>
    {% for item in userdict.items() %}
    <li>{{item}} </li>
    {% end %}
    <!--注意不是Django裏面的enfor直接end if 也是end-->
</ul>

<h1>字典索引取值</h1>
{{userdict['name']}}
{{userdict.get('age')}}

</body>
</html>
index.html

 

c、自定製session

from hashlib import sha1
import os, time
create_session_id = lambda: sha1(bytes('%s%s' % (os.urandom(16), time.time()), encoding='utf-8')).hexdigest()

contatiner={}

class Zg(object):
    def __init__(self,handler ):
        self.handler=handler
        random_str=self.handler.get_cookie('MySessionId') #獲取用戶cokies中的隨機字符串
        if not random_str: #若是沒有隨機字符串,則建立1個;
            random_str=create_session_id()
            contatiner[random_str]={}
        else:              #若是有檢查是不是僞造的隨機字符串?
            if random_str not in contatiner:
                random_str = create_session_id()#僞造的從新生產一個
                contatiner[random_str] = {}
        self.random_str=random_str              #最後生成隨機字符串
        self.handler.set_cookie('MySessionId',random_str,max_age=10) #把隨機字符串,寫到用戶cokies中;

    def __getitem__(self, item):

        return contatiner[self.random_str].get(item)

    def __setitem__(self, key, value):
        contatiner[self.random_str][key]=value
    def __delitem__(self, key):
        if contatiner[self.random_str][key]:
            del contatiner[self.random_str][key]
session
class LoginHandler(tornado.web.RequestHandler):
    def initialize(self):
        self.session=Zg(self) #sel是Handler對象,方便獲取cokies
    def get(self):
        self.render('login.html',**{'msg':''})
    def post(self):
        user = self.get_argument('user')
        pwd = self.get_argument('pwd')
        if user == 'zhanggen' and pwd == '123.com':
            self.session['user_info']=user
            self.redirect('/index/')
            return
        self.render('login.html', **{'msg': '用戶名/密碼錯誤'})

class IndexHandler(tornado.web.RequestHandler):
    def initialize(self):
        self.session = Zg(self)
    def get(self):
        username = self.session['user_info']
        if not username:
            self.redirect('/login/')
            return
        userlist = ['張根', '於超', '李兆宇']
        userdict = {'name': '張根', 'gender': 'man', 'age': 18}
        print(contatiner)
        self.render('index.html', **{'userlist': userlist, 'userdict': userdict})
應用session

 

1.一、分佈式存儲session信息

 

N個雞蛋不能放在1個籃子,若是想要把N個雞蛋放在N個籃子裏,須要解決如下2個問題;

問題1:經過什麼機制判斷哪1個雞蛋應該放在哪1個籃子裏?

問題2:想要吃吃某1個雞蛋時 要已O1的時間複雜度,把它快速取出來;

就就須要一致性hash算法了; 

 

 一致性hash算法邏輯:

r=a-n*[a//n]  #主要運用了1個取模運算(%) a是被除數  n是除數 
0=12-4*(12//4)
0、定義一個socket地址列表 ['192.168.1.1:6379','192.168.1.2:6379','192.168.1.3:6379']
一、每次鏈接數據庫的請求過來,獲取當前用戶生成1個惟一的隨機字符串,而後根據ASCII表把該字符串轉換成對應的數字 N;asdsdffrdf ==> 1234
二、數字N和socket地址列表的長度求餘(N%len(socket地址列表)),獲得socket地址列表中的index,進而根據索引獲取socket地址列表中的socket;
三、即便取餘也沒法保證平均,若是增長權重呢?多出現幾回,增長出現機率; v=['192.168.1.1:6379','192.168.1.2:6379','192.168.1.3:6379','192.168.1.1:6379','192.168.1.1:6379',]
4.若是想要獲取放進去的session信息就拿着那1個步驟1生成的惟一的隨機字符串過來,反解步驟一、2便可

 

 Python3一致性hash模塊

# -*- coding: utf-8 -*-
"""
    hash_ring
    ~~~~~~~~~~~~~~
    Implements consistent hashing that can be used when
    the number of server nodes can increase or decrease (like in memcached).

    Consistent hashing is a scheme that provides a hash table functionality
    in a way that the adding or removing of one slot
    does not significantly change the mapping of keys to slots.

    More information about consistent hashing can be read in these articles:

        "Web Caching with Consistent Hashing":
            http://www8.org/w8-papers/2a-webserver/caching/paper2.html

        "Consistent hashing and random trees:
        Distributed caching protocols for relieving hot spots on the World Wide Web (1997)":
            http://citeseerx.ist.psu.edu/legacymapper?did=38148


    Example of usage::

        memcache_servers = ['192.168.0.246:11212',
                            '192.168.0.247:11212',
                            '192.168.0.249:11212']

        ring = HashRing(memcache_servers)
        server = ring.get_node('my_key')

    :copyright: 2008 by Amir Salihefendic.
    :license: BSD
"""

import math
import sys
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=None, weights=None):
        """`nodes` is a list of objects that have a proper __str__ representation.
        `weights` is dictionary that sets weights to the nodes.  The default
        weight is that all nodes are equal.
        """
        self.ring = dict()
        self._sorted_keys = []

        self.nodes = nodes

        if not weights:
            weights = {}
        self.weights = weights

        self._generate_circle()

    def _generate_circle(self):
        """Generates the circle.
        """
        total_weight = 0
        for node in self.nodes:
            total_weight += self.weights.get(node, 1)

        for node in self.nodes:
            weight = 1

            if node in self.weights:
                weight = self.weights.get(node)

            factor = math.floor((40*len(self.nodes)*weight) / total_weight)

            for j in range(0, int(factor)):
                b_key = self._hash_digest( '%s-%s' % (node, j) )

                for i in range(0, 3):
                    key = self._hash_val(b_key, lambda x: x+i*4)
                    self.ring[key] = node
                    self._sorted_keys.append(key)

        self._sorted_keys.sort()

    def get_node(self, string_key):
        """Given a string key a corresponding node in the hash ring is returned.

        If the hash ring is empty, `None` is returned.
        """
        pos = self.get_node_pos(string_key)
        if pos is None:
            return None
        return self.ring[ self._sorted_keys[pos] ]

    def get_node_pos(self, string_key):
        """Given a string key a corresponding node in the hash ring is returned
        along with it's position in the ring.

        If the hash ring is empty, (`None`, `None`) is returned.
        """
        if not self.ring:
            return None

        key = self.gen_key(string_key)

        nodes = self._sorted_keys
        pos = bisect(nodes, key)

        if pos == len(nodes):
            return 0
        else:
            return pos

    def iterate_nodes(self, string_key, distinct=True):
        """Given a string key it returns the nodes as a generator that can hold the key.

        The generator iterates one time through the ring
        starting at the correct position.

        if `distinct` is set, then the nodes returned will be unique,
        i.e. no virtual copies will be returned.
        """
        if not self.ring:
            yield None, None

        returned_values = set()
        def distinct_filter(value):
            if str(value) not in returned_values:
                returned_values.add(str(value))
                return value

        pos = self.get_node_pos(string_key)
        for key in self._sorted_keys[pos:]:
            val = distinct_filter(self.ring[key])
            if val:
                yield val

        for i, key in enumerate(self._sorted_keys):
            if i < pos:
                val = distinct_filter(self.ring[key])
                if val:
                    yield val

    def gen_key(self, key):
        """Given a string key it returns a long value,
        this long value represents a place on the hash ring.

        md5 is currently used because it mixes well.
        """
        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.encode('utf-8'))
        # return map(ord, m.digest())
        return list(m.digest())
hash_ring.py

 

使用一致性hash模塊

from hash_ring import HashRing

redis_server=['192.168.1.1:6379','192.168.1.2:6379','192.168.1.3:6379']
weights={
    '192.168.1.1:6379':1,
    '192.168.1.2:6379':1,
    '192.168.1.3:6379':1,
}

ring=HashRing(redis_server,weights)

ret=ring.get_node('隨機字符串')#獲取隨機得 socket地址

print(ret)
View Code

 

 

二、自定義Form組件

Form組件2大功能:自動生成html標籤 +對用戶數據進行驗證

 

待續。。。。

 

 

三、自定義中間件

 tornado在執行視圖以前會先執行initialize  prepare方法,完成響應以後會執行finish方法,利用這個特性就能夠作一個相似Django中間件的功能;

import tornado.ioloop
import tornado.web
class MiddleWare1(object):
   def process_request(self,request):
       #request 是RequestHandler的實例
       print('訪問前通過中間件ware1')
   def process_response(self,request):
       print('訪問結束通過中間件ware1')


class BaseMiddleWare(object):
   middleware = [MiddleWare1(),]



class MiddleRequestHandler(BaseMiddleWare,tornado.web.RequestHandler):

   def prepare(self):                           #從新父類的 prepare方法(默認是pass)
       for middleware in self.middleware:
           middleware.process_request(self)

   def finish(self, chunk=None):               #重寫父類finish方法
       for middleware in self.middleware:
           middleware.process_response(self)
       super(MiddleRequestHandler,self).finish()  #注意最後須要執行父類RequestHandler的finish方法才能結束;

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

   def post(self, *args, **kwargs):
       print(self.request)
       self.write('post')


application = tornado.web.Application([
       (r'/index/',MiddleRequestHandler),
   ]
)

if __name__ == '__main__':
   application.listen(8888)
   tornado.ioloop.IOLoop.instance().start()
# 注:在tornado中要實現中間件的方式,經過prepare和finish這兩種方法
自定義中間件

 

 

 

 

 

 

銀角大王博客:

http://www.cnblogs.com/wupeiqi/articles/5341480.html

http://www.cnblogs.com/wupeiqi/p/5938916.html(自定義Form組件)

http://www.cnblogs.com/wupeiqi/articles/5702910.html

相關文章
相關標籤/搜索