Tornado(龍捲風)和Django同樣是Python中比較主流的web框架,Tornado 和如今的主流 Web 服務器框架也有着明顯的區別:Tornado自帶socket,而且實現了異步非阻塞並對WebSocket協議自然支持;javascript
Tonado由 路由系統、視圖、模板語言4大部分組成,若是習慣了使用Django你會感受它功能單薄,可是隻有這樣才能足夠輕量,若是用到什麼功能就本身去GitHub上找現成的插件,或者自實現;如下將對這些基本組件進行逐一介紹。css
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執行流程html
#準備安裝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/java
第三步:服務器接受請求,並交由對應的類處理該請求node
第四步:類接受到請求以後,根據請求方式(post / get / delete ...)的不一樣調用並執行相應的方法python
第五步:方法返回值的字符串內容發送瀏覽器mysql
配置文件:jquery
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 配置文件 )
二、路由系統git
2.一、動態路由(url傳參數)
app=tornado.web.Application( [ (r'^/index/$',MainHandler), (r'^/index/(\d+)$',MainHandler), #url傳參 ] )
2.二、域名匹配
#支持域名匹配 www.zhanggen.com:8888/index/333333 app.add_handlers('www.zhanggen.com',[ (r'^/index/$', MainHandler), (r'^/index/(\d+)$', MainHandler), ])
2.三、反向生成url
app.add_handlers('www.zhanggen.com',[ (r'^/index/$', MainHandler,{},"name1"), #反向生成url (r'^/index/(\d+)$', MainHandler,{},"name2"), ])
class MainHandler(tornado.web.RequestHandler): def get(self,*args,**kwargs): url1=self.application.reverse_url('name1') url2 = self.application.reverse_url('name2', 666) print(url1,url2) self.write('hello word')
三、視圖
tornado的視圖纔有CBV模式,url匹配成功以後先 視圖執行順序爲 initialize 、prepare、get/post/put/delete、finish;
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.一、請求相關
self.get_body_argument('user') :獲取POST請求攜帶的參數
self.get_body_arguments('user_list') :獲取POST請求參數列表(如chebox標籤和select多選)
self.request.body.decode('utf-8'):獲取json數據
self.get_query_argument('user') :獲取GET請求攜帶的參數
self.get_query_arguments('user_list') :獲取GET請求參數列表(如chebox標籤和select多選)
self.get_argument('user') :獲取GET和POST請求攜帶的參數
self.get_arguments('user_list'):獲取GET和POST請求參數列表(如chebox標籤和select多選)
注:以上取值方式若是取不到值就會報錯,能夠設置取不到值就取None;(例如 self.get_argument('user',None))
3.二、響應相關
self.write() :響應字符串
self.render():響應頁面
self.redirect():頁面跳轉
四、模板語言
tornado的模板語言和Python語法一致
4.一、登陸頁面
#準備安裝Tornado: pip install tornado import tornado.ioloop import tornado.web class LoginHandler(tornado.web.RequestHandler): #注意繼承RequestHandler 而不是redirectHandler def get(self): self.render('login.html') setings={ 'template_path':'templates',#配置模板路徑 'static_path':'static', #配置靜態文件存放的路徑 'static_url_prefix':'/zhanggen/' #在模板中引用靜態文件路徑時使用的別名 注意是模板引用時的別名 } application=tornado.web.Application([ (r'/login/',LoginHandler) #參數1 路由系統 ], **setings #參數2 配置文件 ) if __name__ == '__main__': application.listen(8888) #建立1個socket對象 tornado.ioloop.IOLoop.instance().start() #conn,addr=socket.accept()進入監聽狀態
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <link rel="stylesheet" href="/zhanggen/dist/css/bootstrap.css"> <title>Title</title> </head> <body> <div class="container"> <div class="row"> <div class="col-md-5 col-md-offset-3"> <form method="post" > <div class="form-group"> <label for="exampleInputEmail1">用戶名</label> <input type="email" class="form-control" id="exampleInputEmail1" placeholder="用戶名"> </div> <div class="form-group"> <label for="exampleInputPassword1">密碼</label> <input type="password" class="form-control" id="exampleInputPassword1" placeholder="密碼"> </div> <button type="submit" class="btn btn-default">提交</button> </form> </div> </div> </div> </body> </html>
4.二、引入靜態文件
<link rel="stylesheet" href="/zhanggen/coment.css">
<link rel="stylesheet" href='{{static_url("dist/css/bootstrap.css") }}'>
經過static_url()方法引入靜態文件的好處:
一、使用static_url()能夠不用考慮靜態文件修改以後形成引用失效的狀況;
二、還會生成靜態文件url會有一個v=...的參數,這是tornado根據靜態文件MD5以後的值,若是後臺的靜態文件修改,這個值就會變化,前端就會從新向後臺請求靜態文件,保證頁面實時更新,不引用瀏覽器緩存;
4.三、上下文對象
若是模板語言中聲明瞭變量,上下文對象必須對應傳值,若是沒有就設置爲空,不然會報錯;
self.render('login.html',**{'erro_msg':'' }) #模板中聲明瞭變量,視圖必須傳值,若是沒有就設置爲空;
五、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>
六、cokies
Tornado不自帶session,可是包含cookies;
6.一、cookies
設置cokies
user=self.get_cookie('username') if user: v=time.time()+10 self.set_cookie('username', user, expires=v)
獲取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.二、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.三、@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有2大特點:原生支持WebSocket協議、異步非阻塞的Web框架
一、WebSocket協議
HTTP和WebSocket協議都是基於TCP協議的,不一樣於HTTP協議的是WebSocket和服務端創建是長鏈接且鏈接成功以後,會建立一個全雙工通道,這時服務端能夠向客戶端推送消息,客戶端也能夠向服務端推送消息,其本質是保持TCP鏈接,在瀏覽器和服務端經過Socket進行通訊,因爲WebSocket協議創建的是雙向全雙工通道,因此客戶端(瀏覽器)和服務端(Web框架)雙方都要支持WebSocket協議,Tornado原生支持這種協議;
1.0、WebSocket 和HTTP輪詢、長輪詢、長鏈接的區別?
HTTP輪詢:
每間隔1段時間 向服務端發送http請求;
優勢:後端程序編寫比較容易。
缺點:請求中有大半是無用,浪費帶寬和服務器資源,有數據延遲。
實例:適於小型應用。
HTTP長輪詢:
每間隔1段時間 向服務端發送http請求,服務器接收到請求以後hold住本次鏈接1段時間,客戶端進入pending狀態;
若是在hold期間服務端有新消息:會當即響應給客戶端;
若是沒有新消息:超過hold時間,服務端會放開客戶端;
一直循環往復;
優勢:在無消息的狀況下不會頻繁的請求。
缺點:服務器hold鏈接會消耗資源
實例:WebQQ、WEB微信、Hi網頁版、Facebook IM。
HTTP長鏈接:
客戶端就發送1個長鏈接的請求,服務器端就能源源不斷地往客戶端輸入數據。
優勢:消息即時到達,客戶端無需重複發送請求。
缺點:服務器維護一個長鏈接會增長開銷。
WebSocket 協議:
服務端和客戶端鏈接創建全雙工通道一直不斷開;
優勢:實現了實時通信
缺點:舊版本瀏覽器不支持WebSocket協議,兼容性不強;(這也行也是騰訊的WEB微信、WEBQQ不使用該協議的緣由吧?)
1.一、實現WebSocket
實現WebScoket協議,須要遵循2項規則 建立WebSocket鏈接、服務端對封包和解包
a、創建鏈接
步驟1:客戶端向server端發送請求中,請求信息中攜帶Sec-WebSocket-Key: jnqJRYC7EgcTK8OCkVnu9w==\r\n;
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title></title> </head> <body> <div> <input type="text" id="txt"/> <input type="button" id="btn" value="提交" onclick="sendMsg();"/> <input type="button" id="close" value="關閉鏈接" onclick="closeConn();"/> </div> <div id="content"></div> <script type="text/javascript"> var socket = new WebSocket("ws://127.0.0.1:8002"); socket.onopen = function () { /* 與服務器端鏈接成功後,自動執行 */ var newTag = document.createElement('div'); newTag.innerHTML = "【鏈接成功】"; document.getElementById('content').appendChild(newTag); }; socket.onmessage = function (event) { /* 服務器端向客戶端發送數據時,自動執行 */ var response = event.data; var newTag = document.createElement('div'); newTag.innerHTML = response; document.getElementById('content').appendChild(newTag); }; socket.onclose = function (event) { /* 服務器端主動斷開鏈接時,自動執行 */ var newTag = document.createElement('div'); newTag.innerHTML = "【關閉鏈接】"; document.getElementById('content').appendChild(newTag); }; function sendMsg() { var txt = document.getElementById('txt'); socket.send(txt.value); txt.value = ""; } function closeConn() { socket.close(); var newTag = document.createElement('div'); newTag.innerHTML = "【關閉鏈接】"; document.getElementById('content').appendChild(newTag); } </script> </body> </html>
步驟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.二、基於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()
二、異步非阻塞介紹
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.二、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的異步非阻塞,本質上是請求到達視圖 一、先yield 1個Future對象 二、 IO多路複用模塊把該socket添加到監聽列表循環監聽起來;三、 循環監聽過程當中哪1個socket發生變化有response,執行 Future.set_result(response),請求至此返回結束,不然socket鏈接一直不斷開,IO多路複用模塊一直循環監聽socket是否發生變化?;
當發送GET請求時,因爲方法被@gen.coroutine裝飾且yield 一個 Future對象,那麼Tornado會等待,等待用戶向future對象中放置數據或者發送信號,若是獲取到數據或信號以後,就開始執行doing方法。
異步非阻塞體如今當在Tornaod等待用戶向future對象中放置數據時,還能夠處理其餘請求。
注意:在等待用戶向future對象中放置數據或信號時,此鏈接是不斷開的。
#!/usr/bin/env python # -*- coding:utf-8 -*- import tornado.ioloop import time import tornado.web import tornado.websocket from tornado import gen #導入 from tornado.concurrent import Future import time class IndexHadlar(tornado.web.RequestHandler): @gen.coroutine #coroutine(攜程裝飾器) def get(self): print('請求開始') future=Future() tornado.ioloop.IOLoop.current().add_timeout(time.time()+10,self.doing) yield future #yield 1個future對象,IO以後自動切換到doing方法執行; def doing(self): self.write('請求完成') self.finish() #關閉鏈接 application=tornado.web.Application([ (r'/index/',IndexHadlar) ]) if __name__ == '__main__': # 單進程模式 application.listen(8888) tornado.ioloop.IOLoop.instance().start()
2.三、Tornado httpclient類庫
若是服務端接受到客戶端的請求,須要去其餘API獲取數據,再響應給客戶端,這就涉及到了IO操做,Tornado提供了httpclient類庫用於發送Http請求,其配合Tornado的異步非阻塞使用。
#!/usr/bin/env python # -*- coding:utf-8 -*- import tornado.web from tornado.web import RequestHandler from tornado import gen from tornado import httpclient class AsyncHandler(RequestHandler): @gen.coroutine def get(self): print('收到報警') http=httpclient.AsyncHTTPClient() yield http.fetch('https://github.com',self.done) def done(self,respose,*args,**kwargs): print(respose) self.write('推送成功') self.finish() application = tornado.web.Application([ (r"/zhanggen/", AsyncHandler), ]) if __name__ == '__main__': application.listen(8888) tornado.ioloop.IOLoop.instance().start()
2.三、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()
三、使用 Tornado異步非阻塞功能小結:
一、視圖之上加@gen.coroutine裝飾器
二、yield Future()
三、Future對象的set_result()執行請求會當即返回;
一、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.一、分佈式存儲session信息
多個雞蛋不能放在1個籃子,但是如何放在多個籃子裏呢?經過什麼機制判斷哪一個雞蛋應該放在哪一個籃子裏呢?就就須要一致性hash算法了;
一致性hash算法邏輯:
0、定義一個socket地址列表 ['192.168.1.1:6379','192.168.1.2:6379','192.168.1.3:6379']
一、每次鏈接數據庫的請求過來都設置1個惟一的隨機字符串,而後根據ASCII表把該字符串轉換成對應的數字 N; asdsdffrdf ==> 1234
二、數字N和socket地址列表的長度求餘(N%len(socket地址列表)),獲得socket地址列表中的index,進而根據索引獲取socket地址列表中的socket;
三、即便取餘也沒法保證平均,若是增長權重呢?多出現幾回,增長出現機率; v=['192.168.1.1:6379','192.168.1.2:6379','192.168.1.3:6379','192.168.1.1:6379','192.168.1.1:6379',]
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)
二、自定義Form組件
Form組件2大功能:自動生成html標籤 +對用戶數據進行驗證
待續。。。。
三、自定義中間件
tornado在執行視圖以前會先執行initialize prepare方法,完成響應以後會執行finish方法,利用這個特性就能夠作一個相似Django中間件的功能;
import tornado.ioloop import tornado.web class MiddleWare1(object): def process_request(self,request): #request 是RequestHandler的實例 print('訪問前通過中間件ware1') def process_response(self,request): print('訪問結束通過中間件ware1') class BaseMiddleWare(object): middleware = [MiddleWare1(),] class MiddleRequestHandler(BaseMiddleWare,tornado.web.RequestHandler): def prepare(self): #從新父類的 prepare方法(默認是pass) for middleware in self.middleware: middleware.process_request(self) def finish(self, chunk=None): #重寫父類finish方法 for middleware in self.middleware: middleware.process_response(self) super(MiddleRequestHandler,self).finish() #注意最後須要執行父類RequestHandler的finish方法才能結束; def get(self, *args, **kwargs): self.write('hhhhhhhh') def post(self, *args, **kwargs): print(self.request) self.write('post') application = tornado.web.Application([ (r'/index/',MiddleRequestHandler), ] ) if __name__ == '__main__': application.listen(8888) tornado.ioloop.IOLoop.instance().start() # 注:在tornado中要實現中間件的方式,經過prepare和finish這兩種方法
銀角大王博客:
http://www.cnblogs.com/wupeiqi/articles/5341480.html
http://www.cnblogs.com/wupeiqi/p/5938916.html(自定義Form組件)
http://www.cnblogs.com/wupeiqi/articles/5702910.html