PythonWEB框架之Tornado
前言
Tornado(龙卷风)和Django一样是Python中比较主流的web框架,Tornado 和现在的主流 Web 服务器框架也有着明显的区别:Tornado自带socket,并且实现了异步非阻塞并对WebSocket协议天然支持;
一、Tornado框架的基本组成
Tonado由 路由系统、视图、模板语言4大部分组成,如果习惯了使用Django你会感觉它功能单薄,但是只有这样才能足够轻量,如果用到什么功能就自己去GitHub上找现成的插件,或者自实现;以下将对这些基本组件进行逐一介绍。
Django功能概览:socket:有 中间件:无(使用Python的wsgiref模块)路由系统:有视图函数:有ORM操作:有模板语言:有simple_tag:有cokies:有session:有csrf:有xss:有其他:缓存、信号、Form组件、ModelFormm、Admintornado功能概览:socket:有(异步非阻塞、支持WebScoket)路由系统:有视图函数:有静态文件:有ORM操作:无模板语言:有simple_tag:有,uimethod,uimodulecokies:有session:无csrf:有xss:有其他:无
二、Tornado自带功能
1、Tornado执行流程
#准备安装Tornado: pip install tornadoimport tornado.ioloop import tornado.webclass MainHandler(tornado.web.RequestHandler): #注意继承RequestHandler 而不是redirectHandlerdef get(self):self.write('hellow ,world')application=tornado.web.Application([(r'/index/',MainHandler) #路由])if __name__ == '__main__':application.listen(8888) #创建1个socket对象tornado.ioloop.IOLoop.instance().start() #conn,addr=socket.accept()进入监听状态
第一步:执行脚本,监听 8888 端口
第二步:浏览器客户端访问 /index --> http://127.0.0.1:8888/index/
第三步:服务器接受请求,并交由对应的类处理该请求
第四步:类接受到请求之后,根据请求方式(post / get / delete ...)的不同调用并执行相应的方法
第五步:方法返回值的字符串内容发送浏览器
配置文件:
setings={ 'template_path':'templates',#配置模板路径 'static_path':'static', #配置静态文件存放的路径 'static_url_prefix':'/zhanggen/', #在模板中引用静态文件路径时使用的别名 注意是模板引用时的别名 "xsrf_cookies": True, #使用xsrf认证'cookie_secret' :'xsseffekrjewkhwy'#cokies加密时使用的盐 } application=tornado.web.Application([(r'/login/',LoginHandler) ,#参数1 路由系统(r'/index/',IndexHandler) ,#参数1 路由系统],**setings #参数2 配置文件)
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;
import tornado.ioloop import tornado.web class MainHandler(tornado.web.RequestHandler):def initialize(self): #1print()def prepare(self):passdef get(self,*args,**kwargs):self.write('hello word')def post(self, *args, **kwargs):passdef finish(self, chunk=None):passsuper(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 tornadoimport tornado.ioloop import tornado.webclass LoginHandler(tornado.web.RequestHandler): #注意继承RequestHandler 而不是redirectHandlerdef 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()+10self.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()+10self.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()+10self.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()+10self.set_secure_cookie('username', user, expires=v) #设置加密cookiesdef 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() + 10self.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==\r\n;
<!DOCTYPE html> <html> <head lang="en"><meta charset="UTF-8"><title></title> </head> <body><div><input type="text" id="txt"/><input type="button" id="btn" value="提交" οnclick="sendMsg();"/><input type="button" id="close" value="关闭连接" οnclick="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\r\nHost: 127.0.0.1:8002\r\nConnection: Upgrade\r\nPragma: no-cache\r\nCache-Control: no-cache\r\nUpgrade: websocket\r\nOrigin: http://localhost:63342\r\nSec-WebSocket-Version: 13\r\nUser-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36\r\nAccept-Encoding: gzip, deflate, br\r\nAccept-Language: zh-CN,zh;q=0.8\r\nCookie: csrftoken=Om7ZrGEiMyYdx3F6xJmD5ycSWllhDc1D7SXRZKBoj7geGrQ3uwCHkCDdEJRWN1Zg; key="2|1:0|10:1513731498|3:key|12:emhhbmdnZW4=|664ad11ac6e040938f32893d7515f0680b171c39d0f99b918c3366a397f9331c"\r\nSec-WebSocket-Key: jnqJRYC7EgcTK8OCkVnu9w==\r\nSec-WebSocket-Extensions: permessage-deflate; client_max_window_bits\r\n\r\n'
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="提交" οnclick="sendMsg();"/><input type="button" id="close" value="关闭连接" οnclick="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\r\n" \"Upgrade:websocket\r\n" \"Connection:Upgrade\r\n" \"Sec-WebSocket-Accept:%s\r\n" \"WebSocket-Location:ws://%s%s\r\n\r\n"value = headers['Sec-WebSocket-Key'] + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'ac = base64.b64encode(hashlib.sha1(value.encode('utf-8')).digest())response_str = response_tpl % (ac.decode('utf-8'), headers['Host'], headers['url'])conn.send(bytes(response_str, encoding='utf-8'))
步骤2:Socket服务端对发送给服务端的数据进行封包;
#!/usr/bin/env python # -*- coding:utf-8 -*- import socket import base64 import hashlibdef get_headers(data):"""将请求头格式化成字典:param data::return:"""header_dict = {}data = str(data, encoding='utf-8')header, body = data.split('\r\n\r\n', 1)header_list = header.split('\r\n')for i in range(0, len(header_list)):if i == 0:if len(header_list[i].split(' ')) == 3:header_dict['method'], header_dict['url'], header_dict['protocol'] = header_list[i].split(' ')else:k, v = header_list[i].split(':', 1)header_dict[k] = v.strip()return header_dictdef send_msg(conn, msg_bytes):"""WebSocket服务端向客户端发送消息:param conn: 客户端连接到服务器端的socket对象,即: conn,address = socket.accept():param msg_bytes: 向客户端发送的字节:return:"""import structtoken = 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_bytesconn.send(msg)return Truedef run():sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)sock.bind(('127.0.0.1', 8002))sock.listen(5)conn, address = sock.accept()data = conn.recv(1024)headers = get_headers(data)response_tpl = "HTTP/1.1 101 Switching Protocols\r\n" \"Upgrade:websocket\r\n" \"Connection:Upgrade\r\n" \"Sec-WebSocket-Accept:%s\r\n" \"WebSocket-Location:ws://%s%s\r\n\r\n"value = headers['Sec-WebSocket-Key'] + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'ac = base64.b64encode(hashlib.sha1(value.encode('utf-8')).digest())response_str = response_tpl % (ac.decode('utf-8'), headers['Host'], headers['url'])conn.send(bytes(response_str, encoding='utf-8'))while True:try:info = conn.recv(8096)except Exception as e:info = Noneif not info:breakpayload_len = info[1] & 127if 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 = Nonemask = 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="提交" οnclick="sendMsg();"/><input type="button" id="close" value="关闭连接" οnclick="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 hashlibdef get_headers(data):"""将请求头格式化成字典:param data::return:"""header_dict = {}data = str(data, encoding='utf-8')header, body = data.split('\r\n\r\n', 1)header_list = header.split('\r\n')for i in range(0, len(header_list)):if i == 0:if len(header_list[i].split(' ')) == 3:header_dict['method'], header_dict['url'], header_dict['protocol'] = header_list[i].split(' ')else:k, v = header_list[i].split(':', 1)header_dict[k] = v.strip()return header_dictdef send_msg(conn, msg_bytes):"""WebSocket服务端向客户端发送消息:param conn: 客户端连接到服务器端的socket对象,即: conn,address = socket.accept():param msg_bytes: 向客户端发送的字节:return:"""import structtoken = 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_bytesconn.send(msg)return Truedef run():sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)sock.bind(('127.0.0.1', 8002))sock.listen(5)conn, address = sock.accept()data = conn.recv(1024)headers = get_headers(data)response_tpl = "HTTP/1.1 101 Switching Protocols\r\n" \"Upgrade:websocket\r\n" \"Connection:Upgrade\r\n" \"Sec-WebSocket-Accept:%s\r\n" \"WebSocket-Location:ws://%s%s\r\n\r\n"value = headers['Sec-WebSocket-Key'] + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'ac = base64.b64encode(hashlib.sha1(value.encode('utf-8')).digest())response_str = response_tpl % (ac.decode('utf-8'), headers['Host'], headers['url'])conn.send(bytes(response_str, encoding='utf-8'))while True:try:info = conn.recv(8096)except Exception as e:info = Noneif not info:breakpayload_len = info[1] & 127if 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 = Nonemask = 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个请求到达服务端如果服务端未处理完该请求,后续请求一直等待;
解决方案:
开启多线程/多进程:多个线程提高并发;
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)
异步非阻塞就是在服务端结合IO多路复用select/poll/epoll模板,做到1个线程在遇到IO操作的情况下,还可以做一些其他的任务;Tornado默认是阻塞的同时也支持异步非阻塞功能;
Tornado异步非阻塞=IO多路复用(循环检查socket是否发生变化)+携程(哪个有变化?就切换到那个socket!)
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 timeclass 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()
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 httpclientclass AsyncHandler(RequestHandler):@gen.coroutinedef 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#installationpip3 install Tornado-MySQL"""import tornado.web from tornado import genimport tornado_mysql from tornado_mysql import poolsPOOL = 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.coroutinedef 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 666def __setitem__(self, key, value):passdef __delitem__(self, key):passobj=Foo() print(obj['name']) #Python的[]语法,会自动执行对象的__getitem__方法;obj['name']=888 #会自动执行对象的__setitem__方法del obj['name'] #会自动执行对象的__delitem__方法class Yuchao(object):def __init__(self,num):self.num=numdef __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=handlerrandom_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]=valuedef __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对象,方便获取cokiesdef 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']=userself.redirect('/index/')returnself.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/')returnuserlist = ['张根', '于超', '李兆宇']userdict = {'name': '张根', 'gender': 'man', 'age': 18}print(contatiner)self.render('index.html', **{'userlist': userlist, 'userdict': userdict})
1.1、分布式存储session信息
多个鸡蛋不能放在1个篮子,可是如何放在多个篮子里呢?通过什么机制判断哪个鸡蛋应该放在哪个篮子里呢?就就需要一致性hash算法了;
一致性hash算法逻辑:
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',]
Python3一致性hash模块
# -*- coding: utf-8 -*- """hash_ring~~~~~~~~~~~~~~Implements consistent hashing that can be used whenthe number of server nodes can increase or decrease (like in memcached).Consistent hashing is a scheme that provides a hash table functionalityin a way that the adding or removing of one slotdoes 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=38148Example 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 bisectif sys.version_info >= (2, 5):import hashlibmd5_constructor = hashlib.md5 else:import md5md5_constructor = md5.newclass 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 defaultweight is that all nodes are equal."""self.ring = dict()self._sorted_keys = []self.nodes = nodesif not weights:weights = {}self.weights = weightsself._generate_circle()def _generate_circle(self):"""Generates the circle."""total_weight = 0for node in self.nodes:total_weight += self.weights.get(node, 1)for node in self.nodes:weight = 1if 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] = nodeself._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 Nonereturn 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 returnedalong with it's position in the ring.If the hash ring is empty, (`None`, `None`) is returned."""if not self.ring:return Nonekey = self.gen_key(string_key)nodes = self._sorted_keyspos = bisect(nodes, key)if pos == len(nodes):return 0else:return posdef 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 ringstarting 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, Nonereturned_values = set()def distinct_filter(value):if str(value) not in returned_values:returned_values.add(str(value))return valuepos = self.get_node_pos(string_key)for key in self._sorted_keys[pos:]:val = distinct_filter(self.ring[key])if val:yield valfor i, key in enumerate(self._sorted_keys):if i < pos:val = distinct_filter(self.ring[key])if val:yield valdef 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 HashRingredis_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
转载于:https://www.cnblogs.com/ExMan/p/9825648.html
PythonWEB框架之Tornado相关推荐
- pythonweb框架_浅谈python web三大框架
Django请求回应机制 Django(ORM.URL分发系统.MTV ) Django是一个开放源代码的Web应用框架,由Python写成.采用了MVC的框架模式,即模型M,视图V和控制器C.它最初 ...
- 最快的Pythonweb框架之一:FastAPI 初探
最快的Pythonweb框架之一:FastAPI 初探 希望和大家一起来学习FastAPI 异步 web框架,记录一下,当做笔记. 文档资源: 官网:https://fastapi.tiangolo. ...
- python web框架之Tornado
说Tornado之前分享几个前端不错的网站: -- Bootstraphttp://www.bootcss.com/-- Font Awesomehttp://fontawesome.io/-- bx ...
- Web框架之Tornado
https://www.cnblogs.com/wupeiqi/articles/5702910.html https://www.cnblogs.com/wupeiqi/p/4375610.html ...
- Python-web框架 fastapi
目录 简介 性能 api 文档 类型检查 蓝图APIRouter 自定义的tags与responses 注册 APIRouter 简介 官网教程(中文版):FastAPI python语言下的Web框 ...
- python 框架tornado_PythonWEB框架之Tornado
前言 Tornado(龙卷风)和Django一样是Python中比较主流的web框架, 知识铺垫: 什么是别人所说的web server /web服务器? 所有Python写的web服务器本质是就是1 ...
- Python Web 框架:Tornado
版权声明:如需转载本文章,请保留出处! https://blog.csdn.net/xc_zhou/article/details/80637714 1.Tornado Tornado:python编 ...
- web框架 之 Tornado
初识 Tornado : tornado web server 是使用python编写出来的一个轻量级.高可伸缩性和非阻塞IO的Web服务器软件,其特点是采用epoll非阻塞IO,相应快速,可处理数千 ...
- pythonweb框架使用教程_Django视频教程 - 基于Python的Web框架(全13集)
Django是由Python驱动的开源模型-视图-控制器(MVC)风格的Web应用程序框架,使用Django可以在即可分钟内快速开发一个高品质易维护数据库驱动的应用程序.下面是一大坨关于Django应 ...
- pythonweb框架django_Django企业开发实战 高效Python Web框架指南
第一部分初入江湖 章需求2 1.1需求文档3 1.2需求评审/分析4 1.2.1博客需求评审5 1.2.2评审之后6 1.3功能分析6 1.3.1需求列表6 1.3.2功能点梳理7 1.3.3模块划分 ...
最新文章
- redis命令操作(1)
- 本人开源项目 Lu-Rpc
- node-mysql中的连接池代码学习
- 前端学习(2814):小程序学习之建立第一个项目
- 解读 | 滴滴主题研究计划:机器学习专题+
- java最大最小距离算法缺点_java算法(蓝桥杯)- 算法提高 题目1 最大最小值
- 22.docker wait
- 【购买ipad 2021记录:Apple官网线上下单,直营店线下取货】
- 有关cdsn论坛被封后如何解封
- 如何复制PDF文件中的文本和图片?
- 【安装教程】python3.6安装Tensorflow-GPU路上的那些坑(WIN10)
- mysql中的mysql数据库不见了
- 网易校招测试岗位2018
- 手机投屏不是全屏怎么办_一招搞定手机投屏不是全屏问题,手机投屏自适应全屏...
- 小程序源码:经典语录大全微信小程序下载多种分类语录
- MATLAB算法实战应用案例精讲-【智能优化算法】黑寡妇算法-BWO(附matlab代码)
- HDMI热插拔原理及信号解析
- 从软件工程师到创业二十年从业经历感悟
- 【VOLTE案例解析】南京移动拨打10086回落到2G,拨打其他电话正常使用VOLTE
- 简单分析市场上从不会缺席的场外交易模式
热门文章
- 举世闻名的 SQL 注入是什么?这个漫画告诉你!
- 20 道 Redis 面试题,面试官能问的都被我找到了
- 最实用也最容易被遗忘的 Linux 命令行使用技巧
- 从0开始学习 GitHub 系列之「05.Git 进阶」
- linux+shell+整数计算,Shell expr命令进行整数计算的实现
- C#-WinForm-ListView-表格式展示数据、如何将数据库中的数据展示到ListView中、如何对选中的项进行修改...
- WebService学习之三:spring+cxf整合
- jquery中的ajax方法详解
- WITH (NOLOCK)提高查询效率
- SqlServer事务回滚失败