前言
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:有
其他:无
二、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 配置文件 )
2、路由系统
2.1、动态路由(url传参数)
app=tornado.web.Application( [ (r'^/index/$',MainHandler), (r'^/index/(d+)$',MainHandler), #url传参 ] )
2.2、域名匹配
#支持域名匹配 www.zhanggen.com:8888/index/333333 app.add_handlers('www.zhanggen.com',[ (r'^/index/$', MainHandler), (r'^/index/(d+)$', MainHandler), ])
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', '/'))
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()
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语法一致
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()进入监听状态
<!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()方法引入静态文件的好处:
1、使用static_url()可以不用考虑静态文件修改之后造成引用失效的情况;
2、还会生成静态文件url会有一个v=...的参数,这是tornado根据静态文件MD5之后的值,如果后台的静态文件修改,这个值就会变化,前端就会重新向后台请求静态文件,保证页面实时更新,不引用浏览器缓存;
4.3、上下文对象
如果模板语言中声明了变量,上下文对象必须对应传值,如果没有就设置为空,否则会报错;
self.render('login.html',**{'erro_msg':'' }) #模板中声明了变量,视图必须传值,如果没有就设置为空;
5、xsrf_tocken认证
setings={ 'template_path':'templates',#配置模板路径 'static_path':'static', #配置静态文件存放的路径 'static_url_prefix':'/zhanggen/', #在模板中引用静态文件路径时使用的别名 注意是模板引用时的别名 "xsrf_cookies": True, #使用xsrf认证 }
<!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>
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)
获取cokies
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)
6.2、Tornado加密cokies
配置加密规则使用的字符串
setings={ 'template_path':'templates', 'static_path': 'static', 'static_url_prefix':'/zhanggen/', #配置文件别名必须以/开头以/结尾 'cookie_secret':'sssseertdfcvcvd'#配置加密cookie使用得加密字符串 }
设置加密的cokies
self.set_secure_cookie('username',user,expires=v)
获取加密的cokies
self.get_secure_cookie('username')
设置在用户不断刷新页面的情况,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
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 }
视图
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
三、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>
步骤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
'
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>
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'))
步骤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()
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>
<div style="border: 1px solid #dddddd;margin: 10px;"> <div>游客{{uid}}</div> <div style="margin-left: 20px;">{{message}}</div> </div>
视图
#!/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()
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()
缺点:浪费系统资源
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异步非阻塞的效果。
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()
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 需要对每个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()
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)
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>
<!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>
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]
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})
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模块
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)
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