咱們用兩個函數來模擬兩個客戶端請求,並依次進行處理:javascript
# coding:utf-8 def req_a(): """模擬請求a""" print '開始處理請求req_a' print '完成處理請求req_a' def req_b(): """模擬請求b""" print '開始處理請求req_b' print '完成處理請求req_b' def main(): """模擬tornado框架,處理兩個請求""" req_a() req_b() if __name__ == "__main__": main()
執行結果:php
開始處理請求req_a
完成處理請求req_a
開始處理請求req_b
完成處理請求req_b
同步是循序漸進的依次執行,始終按照同一個步調執行,上一個步驟未執行完不會執行下一步。html
想想,若是在處理請求req_a時須要執行一個耗時的工做(如IO),其執行過程如何?前端
# coding:utf-8 import time def long_io(): """模擬耗時IO操做""" print "開始執行IO操做" time.sleep(5) print "完成IO操做" return "io result" def req_a(): print "開始處理請求req_a" ret = long_io() print "ret: %s" % ret print "完成處理請求req_a" def req_b(): print "開始處理請求req_b" print "完成處理請求req_b" def main(): req_a() req_b() if __name__=="__main__": main()
執行過程:java
開始處理請求req_a
開始執行IO操做
完成IO操做
完成處理請求req_a
開始處理請求req_b
完成處理請求req_b
在上面的測試中,咱們看到耗時的操做會將代碼執行阻塞住,即req_a未處理完req_b是沒法執行的。python
對於耗時的過程,咱們將其交給別人(如其另一個線程)去執行,而咱們繼續往下處理,當別人執行完耗時操做後再將結果反饋給咱們,這就是咱們所說的異步。jquery
咱們用容易理解的線程機制來實現異步。web
# coding:utf-8 import time import thread def long_io(callback): """將耗時的操做交給另外一線程來處理""" def fun(cb): # 回調函數做爲參數 """耗時操做""" print "開始執行IO操做" time.sleep(5) print "完成IO操做,並執行回調函數" cb("io result") # 執行回調函數 thread.start_new_thread(fun, (callback,)) # 開啓線程執行耗時操做 def on_finish(ret): """回調函數""" print "開始執行回調函數on_finish" print "ret: %s" % ret print "完成執行回調函數on_finish" def req_a(): print "開始處理請求req_a" long_io(on_finish) print "離開處理請求req_a" def req_b(): print "開始處理請求req_b" time.sleep(2) # 添加此句來突出顯示程序執行的過程 print "完成處理請求req_b" def main(): req_a() req_b() while 1: # 添加此句防止程序退出,保證線程能夠執行完 pass if __name__ == '__main__': main()
執行過程:數據庫
開始處理請求req_a
離開處理請求req_a
開始處理請求req_b
開始執行IO操做
完成處理請求req_b
完成IO操做,並執行回調函數
開始執行回調函數on_finish
ret: io result
完成執行回調函數on_finish
異步的特色是程序存在多個步調,即本屬於同一個過程的代碼可能在不一樣的步調上同時執行。編程
在使用回調函數寫異步程序時,需將本屬於一個執行邏輯(處理請求a)的代碼拆分紅兩個函數req_a和on_finish,這與同步程序的寫法相差很大。而同步程序更便於理解業務邏輯,因此咱們可否用同步代碼的寫法來編寫異步程序?
# coding:utf-8 import time import thread gen = None # 全局生成器,供long_io使用 def long_io(): def fun(): print "開始執行IO操做" global gen time.sleep(5) try: print "完成IO操做,並send結果喚醒掛起程序繼續執行" gen.send("io result") # 使用send返回結果並喚醒程序繼續執行 except StopIteration: # 捕獲生成器完成迭代,防止程序退出 pass thread.start_new_thread(fun, ()) def req_a(): print "開始處理請求req_a" ret = yield long_io() print "ret: %s" % ret print "完成處理請求req_a" def req_b(): print "開始處理請求req_b" time.sleep(2) print "完成處理請求req_b" def main(): global gen gen = req_a() gen.next() # 開啓生成器req_a的執行 req_b() while 1: pass if __name__ == '__main__': main()
執行過程:
開始處理請求req_a
開始處理請求req_b
開始執行IO操做
完成處理請求req_b
完成IO操做,並send結果喚醒掛起程序繼續執行
ret: io result
完成處理請求req_a
咱們在上面編寫出的版本雖然req_a的編寫方式很相似與同步代碼,可是在main中調用req_a的時候卻不能將其簡單的視爲普通函數,而是須要做爲生成器對待。
如今,咱們試圖嘗試修改,讓req_a與main的編寫都相似與同步代碼。
# coding:utf-8 import time import thread gen = None # 全局生成器,供long_io使用 def gen_coroutine(f): def wrapper(*args, **kwargs): global gen gen = f() gen.next() return wrapper def long_io(): def fun(): print "開始執行IO操做" global gen time.sleep(5) try: print "完成IO操做,並send結果喚醒掛起程序繼續執行" gen.send("io result") # 使用send返回結果並喚醒程序繼續執行 except StopIteration: # 捕獲生成器完成迭代,防止程序退出 pass thread.start_new_thread(fun, ()) @gen_coroutine def req_a(): print "開始處理請求req_a" ret = yield long_io() print "ret: %s" % ret print "完成處理請求req_a" def req_b(): print "開始處理請求req_b" time.sleep(2) print "完成處理請求req_b" def main(): req_a() req_b() while 1: pass if __name__ == '__main__': main()
執行過程:
開始處理請求req_a
開始處理請求req_b
開始執行IO操做
完成處理請求req_b
完成IO操做,並send結果喚醒掛起程序繼續執行
ret: io result
完成處理請求req_a
剛剛完成的版本依然不理想,由於存在一個全局變量gen來供long_io使用。咱們如今再次改寫程序,消除全局變量gen。
# coding:utf-8 import time import thread def gen_coroutine(f): def wrapper(*args, **kwargs): gen_f = f() # gen_f爲生成器req_a r = gen_f.next() # r爲生成器long_io def fun(g): ret = g.next() # 執行生成器long_io try: gen_f.send(ret) # 將結果返回給req_a並使其繼續執行 except StopIteration: pass thread.start_new_thread(fun, (r,)) return wrapper def long_io(): print "開始執行IO操做" time.sleep(5) print "完成IO操做,yield回操做結果" yield "io result" @gen_coroutine def req_a(): print "開始處理請求req_a" ret = yield long_io() print "ret: %s" % ret print "完成處理請求req_a" def req_b(): print "開始處理請求req_b" time.sleep(2) print "完成處理請求req_b" def main(): req_a() req_b() while 1: pass if __name__ == '__main__': main()
執行過程:
開始處理請求req_a
開始處理請求req_b
開始執行IO操做
完成處理請求req_b
完成IO操做,yield回操做結果
ret: io result
完成處理請求req_a
這個最終版本就是理解Tornado異步編程原理的最簡易模型,可是,Tornado實現異步的機制不是線程,而是epoll,即將異步過程交給epoll執行並進行監視回調。
須要注意的一點是,咱們實現的版本嚴格意義上來講不能算是協程,由於兩個程序的掛起與喚醒是在兩個線程上實現的,而Tornado利用epoll來實現異步,程序的掛起與喚醒始終在一個線程上,由Tornado本身來調度,屬於真正意義上的協程。雖如此,並不妨礙咱們理解Tornado異步編程的原理。
由於epoll主要是用來解決網絡IO的併發問題,因此Tornado的異步編程也主要體如今網絡IO的異步上,即異步Web請求。
Tornado提供了一個異步Web請求客戶端tornado.httpclient.AsyncHTTPClient用來進行異步Web請求。
用於執行一個web請求request,並異步返回一個tornado.httpclient.HTTPResponse響應。
request能夠是一個url,也能夠是一個tornado.httpclient.HTTPRequest對象。若是是url,fetch會本身構造一個HTTPRequest對象。
HTTP請求類,HTTPRequest的構造函數能夠接收衆多構造參數,最經常使用的以下:
HTTP響應類,其經常使用屬性以下:
新浪IP地址庫
接口說明
1.請求接口(GET):
http://int.dpool.sina.com.cn/iplookup/iplookup.php?format=json&ip=[ip地址字串]
2.響應信息:
(json格式的)國家 、省(自治區或直轄市)、市(縣)、運營商
3.返回數據格式:
{"ret":1,"start":-1,"end":-1,"country":"\u4e2d\u56fd","province":"\u5317\u4eac","city":"\u5317\u4eac","district":"","isp":"","type":"","desc":""}
class IndexHandler(tornado.web.RequestHandler): @tornado.web.asynchronous # 不關閉鏈接,也不發送響應 def get(self): http = tornado.httpclient.AsyncHTTPClient() http.fetch("http://int.dpool.sina.com.cn/iplookup/iplookup.php?format=json&ip=14.130.112.24", callback=self.on_response) def on_response(self, response): if response.error: self.send_error(500) else: data = json.loads(response.body) if 1 == data["ret"]: self.write(u"國家:%s 省份: %s 城市: %s" % (data["country"], data["province"], data["city"])) else: self.write("查詢IP信息錯誤") self.finish() # 發送響應信息,結束請求處理
此裝飾器用於回調形式的異步方法,而且應該僅用於HTTP的方法上(如get、post等)。
此裝飾器不會讓被裝飾的方法變爲異步,而只是告訴框架被裝飾的方法是異步的,當方法返回時響應還沒有完成。只有在request handler調用了finish方法後,纔會結束本次請求處理,發送響應。
不帶此裝飾器的請求在get、post等方法返回時自動完成結束請求處理。
在上一節中咱們本身封裝的裝飾器get_coroutine在Tornado中對應的是tornado.gen.coroutine。
class IndexHandler(tornado.web.RequestHandler): @tornado.gen.coroutine def get(self): http = tornado.httpclient.AsyncHTTPClient() response = yield http.fetch("http://int.dpool.sina.com.cn/iplookup/iplookup.php?format=json&ip=14.130.112.24") if response.error: self.send_error(500) else: data = json.loads(response.body) if 1 == data["ret"]: self.write(u"國家:%s 省份: %s 城市: %s" % (data["country"], data["province"], data["city"])) else: self.write("查詢IP信息錯誤")
也能夠將異步Web請求單獨出來:
class IndexHandler(tornado.web.RequestHandler): @tornado.gen.coroutine def get(self): rep = yield self.get_ip_info("14.130.112.24") if 1 == rep["ret"]: self.write(u"國家:%s 省份: %s 城市: %s" % (rep["country"], rep["province"], rep["city"])) else: self.write("查詢IP信息錯誤") @tornado.gen.coroutine def get_ip_info(self, ip): http = tornado.httpclient.AsyncHTTPClient() response = yield http.fetch("http://int.dpool.sina.com.cn/iplookup/iplookup.php?format=json&ip=" + ip) if response.error: rep = {"ret:0"} else: rep = json.loads(response.body) raise tornado.gen.Return(rep) # 此處須要注意
代碼中咱們須要注意的地方是get_ip_info返回值的方式,在python 2中,使用了yield的生成器可使用不返回任何值的return,但不能return value,所以Tornado爲咱們封裝了用於在生成器中返回值的特殊異常tornado.gen.Return,並用raise來返回此返回值。
Tornado能夠同時執行多個異步,併發的異步可使用列表或字典,以下:
class IndexHandler(tornado.web.RequestHandler): @tornado.gen.coroutine def get(self): ips = ["14.130.112.24", "15.130.112.24", "16.130.112.24", "17.130.112.24"] rep1, rep2 = yield [self.get_ip_info(ips[0]), self.get_ip_info(ips[1])] rep34_dict = yield dict(rep3=self.get_ip_info(ips[2]), rep4=self.get_ip_info(ips[3])) self.write_response(ips[0], rep1) self.write_response(ips[1], rep2) self.write_response(ips[2], rep34_dict['rep3']) self.write_response(ips[3], rep34_dict['rep4']) def write_response(self, ip, response): self.write(ip) self.write(":<br/>") if 1 == response["ret"]: self.write(u"國家:%s 省份: %s 城市: %s<br/>" % (response["country"], response["province"], response["city"])) else: self.write("查詢IP信息錯誤<br/>") @tornado.gen.coroutine def get_ip_info(self, ip): http = tornado.httpclient.AsyncHTTPClient() response = yield http.fetch("http://int.dpool.sina.com.cn/iplookup/iplookup.php?format=json&ip=" + ip) if response.error: rep = {"ret:1"} else: rep = json.loads(response.body) raise tornado.gen.Return(rep)
網站基本都會有數據庫操做,而Tornado是單線程的,這意味着若是數據庫查詢返回過慢,整個服務器響應會被堵塞。
數據庫查詢,實質上也是遠程的網絡調用;理想狀況下,是將這些操做也封裝成爲異步的;但Tornado對此並無提供任何支持。
這是Tornado的設計,而不是缺陷。
一個系統,要知足高流量;是必須解決數據庫查詢速度問題的!
數據庫若存在查詢性能問題,整個系統不管如何優化,數據庫都會是瓶頸,拖慢整個系統!
異步並不能從本質上提到系統的性能;它僅僅是避免多餘的網絡響應等待,以及切換線程的CPU耗費。
若是數據庫查詢響應太慢,須要解決的是數據庫的性能問題;而不是調用數據庫的前端Web應用。
對於實時返回的數據查詢,理想狀況下須要確保全部數據都在內存中,數據庫硬盤IO應該爲0;這樣的查詢才能足夠快;而若是數據庫查詢足夠快,那麼前端web應用也就無將數據查詢封裝爲異步的必要。
就算是使用協程,異步程序對於同步程序始終仍是會提升複雜性;須要衡量的是處理這些額外複雜性是否值得。
若是後端有查詢實在是太慢,沒法繞過,Tornaod的建議是將這些查詢在後端封裝獨立封裝成爲HTTP接口,而後使用Tornado內置的異步HTTP客戶端進行調用。
WebSocket是HTML5規範中新提出的客戶端-服務器通信協議,協議自己使用新的ws://URL格式。
WebSocket 是獨立的、建立在 TCP 上的協議,和 HTTP 的惟一關聯是使用 HTTP 協議的101狀態碼進行協議切換,使用的 TCP 端口是80,能夠用於繞過大多數防火牆的限制。
WebSocket 使得客戶端和服務器之間的數據交換變得更加簡單,容許服務端直接向客戶端推送數據而不須要客戶端進行請求,二者之間能夠建立持久性的鏈接,並容許數據進行雙向傳送。
目前常見的瀏覽器如 Chrome、IE、Firefox、Safari、Opera 等都支持 WebSocket,同時須要服務端程序支持 WebSocket。
Tornado提供支持WebSocket的模塊是tornado.websocket,其中提供了一個WebSocketHandler類用來處理通信。
當一個WebSocket鏈接創建後被調用。
當客戶端發送消息message過來時被調用,注意此方法必須被重寫。
當WebSocket鏈接關閉後被調用。
向客戶端發送消息messagea,message能夠是字符串或字典(字典會被轉爲json字符串)。若binary爲False,則message以utf8編碼發送;二進制模式(binary=True)時,可發送任何字節碼。
關閉WebSocket鏈接。
判斷源origin,對於符合條件(返回判斷結果爲True)的請求源origin容許其鏈接,不然返回403。能夠重寫此方法來解決WebSocket的跨域請求(如始終return True)。
在前端JS中使用WebSocket與服務器通信的經常使用方法以下:
var ws = new WebSocket("ws://127.0.0.1:8888/websocket"); // 新建一個ws鏈接 ws.onopen = function() { // 鏈接創建好後的回調 ws.send("Hello, world"); // 向創建的鏈接發送消息 }; ws.onmessage = function (evt) { // 收到服務器發送的消息後執行的回調 alert(evt.data); // 接收的消息內容在事件參數evt的data屬性中 };
# coding:utf-8 import tornado.web import tornado.ioloop import tornado.httpserver import tornado.options import os import datetime from tornado.web import RequestHandler from tornado.options import define, options from tornado.websocket import WebSocketHandler define("port", default=8000, type=int) class IndexHandler(RequestHandler): def get(self): self.render("index.html") class ChatHandler(WebSocketHandler): users = set() # 用來存放在線用戶的容器 def open(self): self.users.add(self) # 創建鏈接後添加用戶到容器中 for u in self.users: # 向已在線用戶發送消息 u.write_message(u"[%s]-[%s]-進入聊天室" % (self.request.remote_ip, datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))) def on_message(self, message): for u in self.users: # 向在線用戶廣播消息 u.write_message(u"[%s]-[%s]-說:%s" % (self.request.remote_ip, datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), message)) def on_close(self): self.users.remove(self) # 用戶關閉鏈接後從容器中移除用戶 for u in self.users: u.write_message(u"[%s]-[%s]-離開聊天室" % (self.request.remote_ip, datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))) def check_origin(self, origin): return True # 容許WebSocket的跨域請求 if __name__ == '__main__': tornado.options.parse_command_line() app = tornado.web.Application([ (r"/", IndexHandler), (r"/chat", ChatHandler), ], static_path = os.path.join(os.path.dirname(__file__), "static"), template_path = os.path.join(os.path.dirname(__file__), "template"), debug = True ) http_server = tornado.httpserver.HTTPServer(app) http_server.listen(options.port) tornado.ioloop.IOLoop.current().start()
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>聊天室</title> </head> <body> <div id="contents" style="height:500px;overflow:auto;"></div> <div> <textarea id="msg"></textarea> <a href="javascript:;" onclick="sendMsg()">發送</a> </div> <script src="{{static_url('js/jquery.min.js')}}"></script> <script type="text/javascript"> var ws = new WebSocket("ws://192.168.114.177:8000/chat"); ws.onmessage = function(e) { $("#contents").append("<p>" + e.data + "</p>"); } function sendMsg() { var msg = $("#msg").val(); ws.send(msg); $("#msg").val(""); } </script> </body> </html>
請解釋清同步、異步、yield、協程幾個概念和Tornado實現異步的原理。
練習使用Tornado異步Web客戶端。
練習使用WebSocket。
修改WebSocket案例中的在線聊天代碼,將獲取到的用戶IP利用異步客戶端查詢歸屬地,並將消息顯示格式爲
[城市網友]-[IP]-[時間]:消息
如
[北京網友]-[14.130.112.24]-[2016-08-29 00:00:00]: 你好,Python
注意,局域網的內網IP沒有歸屬地,只需寫出代碼便可,不用測試。