PythonWEB框架之Tornado
Posted ExplorerMan
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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、Admin tornado功能概览: socket:有(异步非阻塞、支持WebScoket) 路由系统:有 视图函数:有 静态文件:有 ORM操作:无 模板语言:有 simple_tag:有,uimethod,uimodule cokies:有 session:无 csrf:有 xss:有 其他:无
二、Tornado自带功能
1、Tornado执行流程
#准备安装Tornado: pip install tornado import tornado.ioloop import tornado.web class MainHandler(tornado.web.RequestHandler): #注意继承RequestHandler 而不是redirectHandler def get(self): self.write(\'hellow ,world\') application=tornado.web.Application([ (r\'/index/\',MainHandler) #路由 ]) if __name__ == \'__main__\': application.listen(8888) #创建1个socket对象 tornado.ioloop.IOLoop.instance().start() #conn,addr=socket.accept()进入监听状态
第一步:执行脚本,监听 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): #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==\\r\\n;
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title></title> </head> <body> <div> <input type="text" id="txt"/> <input type="button" id="btn" value="提交" onclick="sendMsg();"/> <input type="button" id="close" value="关闭连接" onclick="closeConn();"/> </div> <div id="content"></div> <script type="text/javascript"> var socket = new WebSocket("ws://127.0.0.1:8002"); socket.onopen = function () { /* 与服务器端连接成功后,自动执行 */ var newTag = document.createElement(\'div\'); newTag.innerHTML = "【连接成功】"; document.getElementById(\'content\').appendChild(newTag); }; socket.onmessage = function (event) { /* 服务器端向客户端发送数据时,自动执行 */ var response = event.data; var newTag = document.createElement(\'div\'); newTag.innerHTML = response; document.getElementById(\'content\').appendChild(newTag); }; socket.onclose = function (event) { /* 服务器端主动断开连接时,自动执行 */ var newTag = document.createElement(\'div\'); newTag.innerHTML = "【关闭连接】"; document.getElementById(\'content\').appendChild(newTag); }; function sendMsg() { var txt = document.getElementById(\'txt\'); socket.send(txt.value); txt.value = ""; } function closeConn() { socket.close(); var newTag = document.createElement(\'div\'); newTag.innerHTML = "【关闭连接】"; document.getElementById(\'content\').appendChild(newTag); } </script> </body> </html>
步骤2:服务端接收到客户端请求,获取请求头,从中获取Sec-WebSocket-Key;
步骤3:获取到的Sec-WebSocket-Key对应的字符和magic_string进行拼接;
magic_string = \'258EAFA5-E914-47DA-95CA-C5AB0DC85B11\' #固定且全球唯一 value = headers[\'Sec-WebSocket-Key\'] + magic_string
步骤4:设置响应头,步骤3拼接完成之后的结果进行 base64加密;
ac = base64.b64encode(hashlib.sha1(value.encode(\'utf-8\')).digest())
GET / HTTP/1.1\\r\\n Host: 127.0.0.1:8002\\r\\n Connection: Upgrade\\r\\n Pragma: no-cache\\r\\n Cache-Control: no-cache\\r\\n Upgrade: websocket\\r\\n Origin: http://localhost:63342\\r\\n Sec-WebSocket-Version: 13\\r\\n User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36\\r\\n Accept-Encoding: gzip, deflate, br\\r\\n Accept-Language: zh-CN,zh;q=0.8\\r\\n Cookie: csrftoken=Om7ZrGEiMyYdx3F6xJmD5ycSWllhDc1D7SXRZKBoj7geGrQ3uwCHkCDdEJRWN1Zg; key="2|1:0|10:1513731498|3:key|12:emhhbmdnZW4=|664ad11ac6e040938f32893d7515f0680b171c39d0f99b918c3366a397f9331c"\\r\\n Sec-WebSocket-Key: jnqJRYC7EgcTK8OCkVnu9w==\\r\\n Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits\\r\\n\\r\\n\'
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\\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 hashlib def get_headers(data): """ 将请求头格式化成字典 :param data: :return: """ header_dict = {} data = str(data, encoding=\'utf-8\') header, body = data.split(\'\\r\\n\\r\\n\', 1) header_list = header.split(\'\\r\\n\') for i in range(0, len(header_list)): if i == 0: if len(header_list[i].split(\' \')) == 3: header_dict[\'method\'], header_dict[\'url\'], header_dict[\'protocol\'] = header_list[i].split(\' \') else: k, v = header_list[i].split(\':\', 1) header_dict[k] = v.strip() return header_dict def send_msg(conn, msg_bytes): """ WebSocket服务端向客户端发送消息 :param conn: 客户端连接到服务器端的socket对象,即: conn,address = socket.accept() :param msg_bytes: 向客户端发送的字节 :return: """ import struct token = b"\\x81" length = len(msg_bytes) if length < 126: token += struct.pack("B", length) elif length <= 0xFFFF: token += struct.pack("!BH", 126, length) else: token += struct.pack("!BQ", 127, length) msg = token + msg_bytes conn.send(msg) return True def run(): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind((\'127.0.0.1\', 8002)) sock.listen(5) conn, address = sock.accept() data = conn.recv(1024) headers = get_headers(data) response_tpl = "HTTP/1.1 101 Switching Protocols\\r\\n" \\ "Upgrade:websocket\\r\\n" \\ "Connection:Upgrade\\r\\n" \\ "Sec-WebSocket-Accept:%s\\r\\n" \\ "WebSocket-Location:ws://%s%s\\r\\n\\r\\n" value = headers[\'Sec-WebSocket-Key\'] + \'258EAFA5-E914-47DA-95CA-C5AB0DC85B11\' ac = base64.b64encode(hashlib.sha1(value.encode(\'utf-8\')).digest()) response_str = response_tpl % (ac.decode(\'utf-8\'), headers[\'Host\'], headers[\'url\']) conn.send(bytes(response_str, encoding=\'utf-8\')) while True: try: info = conn.recv(8096) except Exception as e: info = None if not info: break payload_len = info[1] & 127 if payload_len == 126: extend_payload_len = info[2:4] mask = info[4:8] decoded = info[8:] elif payload_len == 127: extend_payload_len = info[2:10] mask = info[10:14] decoded = info[14:] else: extend_payload_len = None mask = info[2:6] decoded = info[6:] bytes_list = bytearray() for i in range(len(decoded)): chunk = decoded[i] ^ mask[i % 4] bytes_list.append(chunk) body = str(bytes_list, encoding=\'utf-8\') send_msg(conn, body.encode(\'utf-8\')) sock.close() if __name__ == \'__main__\': run()
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(\'\\r\\n\\r\\n\', 1) header_list = header.split(\'\\r\\n\') for i in range(0, len(header_list)): if i == 0: if len(header_list[i].split(\' \')) == 3: header_dict[\'method\'], header_dict[\'url\'], header_dict[\'protocol\'] = header_list[i].split(\' \') else: k, v = header_list[i].split(\':\', 1) header_dict[k] = v.strip() return header_dict def send_msg(conn, msg_bytes): """ WebSocket服务端向客户端发送消息 :param conn: 客户端连接到服务器端的socket对象,即: conn,address = socket.accept() :param msg_bytes: 向客户端发送的字节 :return: """ import struct token = b"\\x81" length = len(msg_bytes) if length < 126: token += struct.pack("B", length) elif length <= 0xFFFF: token += struct.pack("!BH", 126, length) else: token += struct.pack("!BQ", 127, length) msg = token + msg_bytes conn.send(msg) return True def run(): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind((\'127.0.0.1\', 8002)) sock.listen(5) conn, address = sock.accept() data = conn.recv(1024) headers = get_headers(data) response_tpl = "HTTP/1.1 101 Switching Protocols\\r\\n" \\ "Upgrade:websocket\\r\\n" \\ "Connection:Upgrade\\r\\n" \\ "Sec-WebSocket-Accept:%s\\r\\n" \\ "WebSocket-Location:ws://%s%s\\r\\n\\r\\n" value = headers[\'Sec-WebSocket-Key\'] + \'258EAFA5-E914-47DA-95CA-C5AB0DC85B11\' ac = base64.b64encode(hashlib.sha1(value.encode(\'utf-8\')).digest()) response_str = response_tpl % (ac.decode(\'utf-8\'), headers[\'Host\'], headers[\'url\']) conn.send(bytes(response_str, encoding=\'utf-8\')) while True: try: info = conn.recv(8096) except Exception as e: info = None if not info: break payload_len = info[1] & 127 if payload_len == 126: extend_payload_len = info[2:4] mask = info[4:8] decoded = info[8:] elif payload_len == 127: extend_payload_len = info[2:10] mask = info[10:14] decoded = info[14:] else: extend_payload_len = None mask = info[2:6] decoded = info[6:] bytes_list = bytearray() for i in range(len(decoded)): chunk = decoded[i] ^ mask[i % 4] bytes_list.append(chunk) body = str(bytes_list, encoding=\'utf-8\') send_msg(conn, body.encode(\'utf-8\')) sock.close() if __name__ == \'__main__\': run()
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 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()
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 Z以上是关于PythonWEB框架之Tornado的主要内容,如果未能解决你的问题,请参考以下文章