Tornado——异步,websocket
模拟tornado两个socket请求
同步执行
按部就班的依次执行,知道上一个步骤执行完才执行下一步。
# coding:utf-8 import time def long_io(): # 长io操作 print("开始执行IO操作") time.sleep(2) print("完成IO操作") def req_a(): # 模拟请求a print('开始处理请求req_a') long_io() # 执行一个长io操作 print('完成处理请求req_a') def req_b(): # 模拟请求b print('开始处理请求req_b') print('完成处理请求req_b') def main(): # 模拟tornado框架,处理两个请求 req_a() req_b() if __name__ == "__main__": main()
结果:
开始处理请求req_a
开始执行IO操作
完成IO操作
完成处理请求req_a
开始处理请求req_b
完成处理请求req_b
异步执行
对于耗时的过程,我们将其交给别人(如其另外一个线程)去执行,而我们继续往下处理,当别人执行完耗时操作后再将结果反馈给我们,这就是我们所说的异步。
1)引入线程和回调函数
# coding:utf-8 import time,threading # 引入多线程 thread_list = [] def long_io(callback): # 长io操作 def fun(cb): print("开始执行IO操作") time.sleep(5) print("完成IO操作") cb("IO操作结束") # 当线程结束时,执行回调函数 threading.Thread(target=fun,args=(callback,)).start() # 将长的io操作交个另一个线程来处理,此时long_io执行完毕(python多线程不用join) def callback(ret): print(ret) def req_a(): # 模拟请求a print('开始处理请求req_a') long_io(callback) # 执行一个长io操作 print('完成处理请求req_a') def req_b(): # 模拟请求b print('开始处理请求req_b') print('完成处理请求req_b') def main(): # 模拟tornado框架,处理两个请求 req_a() req_b() if __name__ == "__main__": main()
结果:
开始处理请求req_a
开始执行IO操作
完成处理请求req_a
开始处理请求req_b
完成处理请求req_b
完成IO操作
IO操作结束
2)引入协程
yield关键字:将函数/循环...变成生成器,使用__next__执行到下一个yield关键字的位置,使用send赋值并唤醒。
正常版本:
# coding:utf-8 import time,threading # 引入多线程 thread_list = [] def long_io(): # 长io操作 def fun(): print("开始执行IO操作") time.sleep(5) print("完成IO操作") try: gen.send("============ IO操作结束 ============") # 使用send返回结果并唤醒程序继续执行 except: # 捕获生成器完成迭代的异常,防止程序退出 pass threading.Thread(target=fun).start() # 将长的io操作交个另一个线程来处理,python多进程不用join def req_a(): # 模拟请求a print('开始处理请求req_a') ret = yield long_io() # 执行一个长io操作,接收结果 print(ret) print('完成处理请求req_a') def req_b(): # 模拟请求b print('开始处理请求req_b') print('完成处理请求req_b') def main(): # 模拟tornado框架,处理两个请求 global gen gen = req_a() # 初始化生成器 gen.__next__() # 执行到第一个yield位置 req_b() if __name__ == "__main__": main()
结果:
开始处理请求req_a
开始执行IO操作
开始处理请求req_b
完成处理请求req_b
完成IO操作
============ IO操作结束 ============
完成处理请求req_a
装饰器版本:
# coding:utf-8 import time,threading # 引入多线程 thread_list = [] def long_io(): # 长io操作 def fun(): print("开始执行IO操作") time.sleep(5) print("完成IO操作") try: gen.send("============ IO操作结束 ============") # 使用send返回结果并唤醒程序继续执行 except: # 捕获生成器完成迭代的异常,防止程序退出 pass threading.Thread(target=fun).start() # 将长的io操作交个另一个线程来处理,python多进程不用join def gen_coroutine(f): def inner(*args,**kwargs): global gen gen = f() # 执行req_a,初始化生成器 gen.__next__() # 执行到第一个yield位置 return inner @gen_coroutine def req_a(): # 模拟请求a print('开始处理请求req_a') ret = yield long_io() # 执行一个长io操作,接收结果 print(ret) print('完成处理请求req_a') def req_b(): # 模拟请求b print('开始处理请求req_b') print('完成处理请求req_b') def main(): # 模拟tornado框架,处理两个请求 req_a() # 下面不修改 req_b() if __name__ == "__main__": main()
结果:
开始处理请求req_a
开始执行IO操作
开始处理请求req_b
完成处理请求req_b
完成IO操作
============ IO操作结束 ============
完成处理请求req_a
这个版本就是理解Tornado异步编程原理的最简易模型,但是,Tornado实现异步的机制不是线程,而是ioloop,即将异步过程交给ioloop执行并进行监视回调。
需要注意的一点是,我们实现的版本严格意义上来说不能算是协程,因为两个程序的挂起与唤醒是在两个线程上实现的,而Tornado利用ioloop来实现异步,程序的挂起与唤醒始终在一个线程上,由Tornado自己来调度,属于真正意义上的协程。虽如此,并不妨碍我们理解Tornado异步编程的原理。
Tornado的异步
因为epoll主要是用来解决网络IO的并发问题,所以Tornado的异步编程也主要体现在网络IO的异步上,即异步Web请求。
将每个建立起来的socket送入epoll池中监听,当有请求来了的时候,epoll会捕捉信号并将生成器,断点...全部打包给IOLoop,让IOLoop来进行调度。
经过路由映射交给视图函数处理,当遇到堵塞的时候会将生成器,断点...全部打包还给IOLoop,等待下一次调度。
tornado.httpclient.AsyncHTTPClient
Tornado提供了一个异步Web请求客户端tornado.httpclient.AsyncHTTPClient用来进行异步Web请求。
fetch(request, callback=None)
用于执行一个web请求request,并异步返回一个tornado.httpclient.HTTPResponse响应。
request可以是一个url,也可以是一个tornado.httpclient.HTTPRequest对象。如果是url,fetch会自己构造一个HTTPRequest对象。
HTTPRequest
HTTP请求类,HTTPRequest的构造函数可以接收众多构造参数,最常用的如下:
- url (string) – 要访问的url,此参数必传,除此之外均为可选参数
- method (string) – HTTP访问方式,如“GET”或“POST”,默认为GET方式
- headers (HTTPHeaders or dict) – 附加的HTTP协议头
- body – HTTP请求的请求体
HTTPResponse
HTTP响应类,其常用属性如下:
- code: HTTP状态码,如 200 或 404
- reason: 状态码描述信息
- body: 响应体字符串
- error: 异常(可有可无)
装饰器(tornado.web.asynchronous)
此装饰器用于回调形式的异步方法,并且应该仅用于HTTP的方法上(如get、post等)。
此装饰器不会让被装饰的方法变为异步,而只是告诉框架被装饰的方法是异步的,当方法返回时响应尚未完成。只有在request handler调用了finish方法后,才会结束本次请求处理,发送响应。
不带此装饰器的请求在get、post等方法返回时自动完成结束请求处理。
callback异步:
# coding:utf-8 import tornado.web import tornado.ioloop import tornado.httpserver import tornado.options import json from tornado.web import url,RequestHandler from tornado.options import define,options from tornado.httpclient import AsyncHTTPClient # 引入异步Web请求客户端 tornado.options.define("port",default=8001,type=int,help="给个端口号呗") class IndexHandler(RequestHandler): @tornado.web.asynchronous # 不关闭连接,也不发送响应(因为在get,post方法执行后会执行on_finish方法,会关闭管道) def get(self): http = AsyncHTTPClient() # 实例化异步客户端 http.fetch("http://int.dpool.sina.com.cn/iplookup/iplookup.php?format=json&ip=14.130.112.24",callback=self.on_response) # 发送一个http请求 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() # 发送响应信息,结束请求处理 if __name__ == "__main__": tornado.options.parse_command_line() app = tornado.web.Application( [ (r"/",IndexHandler), ], debug = True ) http_server = tornado.httpserver.HTTPServer(app) # 创建httpserver实例 http_server.listen(options.port) tornado.ioloop.IOLoop.current().start()
结果:
国家:中国 省份: 北京 城市: 北京
协程异步:
# coding:utf-8 import tornado.web import tornado.ioloop import tornado.httpserver import tornado.options import json from tornado.web import url,RequestHandler from tornado.options import define,options from tornado.httpclient import AsyncHTTPClient # 引入异步Web请求客户端 import tornado.gen tornado.options.define("port",default=8001,type=int,help="给个端口号呗") class IndexHandler(RequestHandler): class IndexHandler(tornado.web.RequestHandler): @tornado.gen.coroutine def get(self): http = 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信息错误") if __name__ == "__main__": tornado.options.parse_command_line() app = tornado.web.Application( [ (r"/",IndexHandler), ], debug = True ) http_server = tornado.httpserver.HTTPServer(app) # 创建httpserver实例 http_server.listen(options.port) tornado.ioloop.IOLoop.current().start()
也可以将异步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) return(rep) # 此处需要注意,python2中使用raise tornado.gen.Return
代码中我们需要注意的地方是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
WebSocket是HTML5规范中新提出的客户端-服务器通讯协议,协议本身使用新的ws://URL格式。
WebSocket 是独立的、创建在 TCP 上的协议,和 HTTP 的唯一关联是使用 HTTP 协议的101状态码进行协议切换,使用的 TCP 端口是80,可以用于绕过大多数防火墙的限制。
WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端直接向客户端推送数据而不需要客户端进行请求,两者之间可以创建持久性的连接,并允许数据进行双向传送。
目前常见的浏览器如 Chrome、IE、Firefox、Safari、Opera 等都支持 WebSocket,同时需要服务端程序支持 WebSocket。
1. Tornado的WebSocket模块
Tornado提供支持WebSocket的模块是tornado.websocket,其中提供了一个WebSocketHandler类用来处理通讯。
WebSocketHandler.open()
当一个WebSocket连接建立后被调用。
WebSocketHandler.on_message(message)
当客户端发送消息message过来时被调用,注意此方法必须被重写。
WebSocketHandler.on_close()
当WebSocket连接关闭后被调用。
WebSocketHandler.write_message(message, binary=False)
向客户端发送消息messagea,message可以是字符串或字典(字典会被转为json字符串)。若binary为False,则message以utf8编码发送;二进制模式(binary=True)时,可发送任何字节码。
WebSocketHandler.close()
关闭WebSocket连接。
WebSocketHandler.check_origin(origin)
判断源origin,对于符合条件(返回判断结果为True)的请求源origin允许其连接,否则返回403。可以重写此方法来解决WebSocket的跨域请求(如始终return True)。
2. 前端JavaScript编写
在前端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属性中 };
在线聊天室的小Demo
1 # coding:utf-8 2 3 import tornado.web 4 import tornado.ioloop 5 import tornado.httpserver 6 import tornado.options 7 import os 8 import datetime 9 10 from tornado.web import RequestHandler 11 from tornado.options import define, options 12 from tornado.websocket import WebSocketHandler 13 14 define("port", default=8000, type=int) 15 16 class IndexHandler(RequestHandler): 17 def get(self): 18 self.render("index.html") 19 20 class ChatHandler(WebSocketHandler): 21 22 users = set() # 用来存放在线用户的容器 23 24 def open(self): 25 self.users.add(self) # 建立连接后添加用户到容器中 26 for u in self.users: # 向已在线用户发送消息 27 u.write_message(u"[%s]-[%s]-进入聊天室" % (self.request.remote_ip, datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))) 28 29 def on_message(self, message): 30 for u in self.users: # 向在线用户广播消息 31 u.write_message(u"[%s]-[%s]-说:%s" % (self.request.remote_ip, datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), message)) 32 33 def on_close(self): 34 self.users.remove(self) # 用户关闭连接后从容器中移除用户 35 for u in self.users: 36 u.write_message(u"[%s]-[%s]-离开聊天室" % (self.request.remote_ip, datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))) 37 38 def check_origin(self, origin): 39 return True # 允许WebSocket的跨域请求 40 41 if __name__ == '__main__': 42 tornado.options.parse_command_line() 43 app = tornado.web.Application([ 44 (r"/", IndexHandler), 45 (r"/chat", ChatHandler), 46 ], 47 static_path = os.path.join(os.path.dirname(__file__), "static"), 48 template_path = os.path.join(os.path.dirname(__file__), "template"), 49 debug = True 50 ) 51 http_server = tornado.httpserver.HTTPServer(app) 52 http_server.listen(options.port) 53 tornado.ioloop.IOLoop.current().start()
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="utf-8"> 5 <title>聊天室</title> 6 </head> 7 <body> 8 <div id="contents" style="height:500px;overflow:auto;"></div> 9 <div> 10 <textarea id="msg"></textarea> 11 <a href="javascript:;" onclick="sendMsg()">发送</a> 12 </div> 13 <script src="{{static_url('js/jquery.min.js')}}"></script> 14 <script type="text/javascript"> 15 var ws = new WebSocket("ws://192.168.114.177:8000/chat"); 16 ws.onmessage = function(e) { 17 $("#contents").append("<p>" + e.data + "</p>"); 18 } 19 function sendMsg() { 20 var msg = $("#msg").val(); 21 ws.send(msg); 22 $("#msg").val(""); 23 } 24 </script> 25 </body> 26 </html>