• PythonWEB框架之Tornado


    前言

    Tornado(龙卷风)和Django一样是Python中比较主流的web框架,

    知识铺垫:

    什么是别人所说的web server /web服务器?

    所有Python写的web服务器本质是就是1个符合wsgi协议标准的 socket(例如:Python的wsgiref模块就可以实现1个web服务)

    web server负责 监听在某1个端口、接收客户端的连接,分割http的请求头和请求体..等底层工作

    最后封装好请求头和响应头的信息,传给web框架

    让我们在使用web框架开发web应用程序的时候更加快捷和方便,就可以 request.xx 、render, HttpResponse,redirect

    什么是别人说的web 应用/web框架?

    web框架负责web应用逻辑处理的部分,通常做了一些代码封装,可以更加人性性化得让我们使用(Django/Flashk。。。)

    web server 和 web 框架如何结合?

    如果想要让你的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协议天然支持;

    一、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功能对比

     

    二、Tornado自带功能

    1、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、路由系统

    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

    一定要注意这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
    %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:
    %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.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语法一致

    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== ;

    <!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
    
    
    Host: 127.0.0.1:8002
    
    
    Connection: Upgrade
    
    
    Pragma: no-cache
    
    
    Cache-Control: no-cache
    
    
    Upgrade: websocket
    
    
    Origin: http://localhost:63342
    
    
    Sec-WebSocket-Version: 13
    
    
    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
    
    
    Accept-Encoding: gzip, deflate, br
    
    
    Accept-Language: zh-CN,zh;q=0.8
    
    
    Cookie: csrftoken=Om7ZrGEiMyYdx3F6xJmD5ycSWllhDc1D7SXRZKBoj7geGrQ3uwCHkCDdEJRWN1Zg; key="2|1:0|10:1513731498|3:key|12:emhhbmdnZW4=|664ad11ac6e040938f32893d7515f0680b171c39d0f99b918c3366a397f9331c"
    
    
    Sec-WebSocket-Key: jnqJRYC7EgcTK8OCkVnu9w==
    
    
    
    Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
    
    '
    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
    " 
                       "Upgrade:websocket
    " 
                       "Connection:Upgrade
    " 
                       "Sec-WebSocket-Accept:%s
    " 
                       "WebSocket-Location:ws://%s%s
    
    "
    
        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('
    
    ', 1)
        header_list = header.split('
    ')
        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
    " 
                       "Upgrade:websocket
    " 
                       "Connection:Upgrade
    " 
                       "Sec-WebSocket-Accept:%s
    " 
                       "WebSocket-Location:ws://%s%s
    
    "
    
        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.2、基于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('
    
    ', 1)
        header_list = header.split('
    ')
        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
    " 
                       "Upgrade:websocket
    " 
                       "Connection:Upgrade
    " 
                       "Sec-WebSocket-Accept:%s
    " 
                       "WebSocket-Location:ws://%s%s
    
    "
    
        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

    2、异步非阻塞介绍

    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.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的异步非阻塞,本质上是请求到达视图 1、先yield 1个Future对象  2、 IO多路复用模块把该socket添加到监听列表循环监听起来;3、 循环监听过程中哪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.3、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.3、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

    3、使用 Tornado异步非阻塞功能小结:

    1、视图之上加@gen.coroutine装饰器

    2、yield Future()

    3、Future对象的set_result()执行请求会立即返回;

     

    四、Tornado功能扩展

    1、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.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、每次连接数据库的请求过来,获取当前用户生成1个唯一的随机字符串,然后根据ASCII表把该字符串转换成对应的数字 N;asdsdffrdf ==> 1234
    2、数字N和socket地址列表的长度求余(N%len(socket地址列表)),得到socket地址列表中的index,进而根据索引获取socket地址列表中的socket;
    3、即使取余也无法保证平均,如果增加权重呢?多出现几次,增加出现机率; 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生成的唯一的随机字符串过来,反解步骤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

    2、自定义Form组件

    Form组件2大功能:自动生成html标签 +对用户数据进行验证

    待续。。。。

    3、自定义中间件

     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

  • 相关阅读:
    庆祝一下开通了第一条博客!
    查看树莓派温度
    Ubuntu Server for Raspberry Pi部署Jenkins
    acme.sh部署RSA、ECC双证书(使用阿里云API)
    Ubuntu Server 20.04换用阿里源
    ESP32使用NTP同步时间
    C语言的可变参数函数
    一种Θ(1)的计算32位整数二进制中1的个数的方法
    CentOS 8安装Docker
    CentOS 8搭建LNMP + WordPress(三)
  • 原文地址:https://www.cnblogs.com/sss4/p/8057865.html
Copyright © 2020-2023  润新知