Tornado简介
Tornado安装
pip install tornado
Tornado框架的基本组成
由一个简单的例子开始
import tornado.ioloop import tornado.web class MainHandler(tornado.web.RequestHandler): def get(self): self.write("Hello, world") def make_app(): return tornado.web.Application([ (r"/", MainHandler), ]) if __name__ == "__main__": app = make_app() app.listen(8888) tornado.ioloop.IOLoop.current().start()
执行python hello.py
,打开浏览器访问http://localhost:8888/就可以看到服务器的正常输出Hello, world
。
一个tornado web服务器通常由四大组件组成。
- ioloop实例,它是全局的tornado事件循环,是服务器的引擎核心,示例中
tornado.ioloop.IOLoop.current()
就是默认的tornado ioloop实例。 - app实例,它代表着一个完成的后端app,它会挂接一个服务端套接字端口对外提供服务。一个ioloop实例里面可以有多个app实例,示例中只有1个,实际上可以允许多个,不过一般几乎不会使用多个。
- handler类,它代表着业务逻辑,我们进行服务端开发时就是编写一堆一堆的handler用来服务客户端请求。
- 路由表,它将指定的url规则和handler挂接起来,形成一个路由映射表。当请求到来时,根据请求的访问url查询路由映射表来找到相应的业务handler。
这四大组件的关系是,一个ioloop包含多个app(管理多个服务端口),一个app包含一个路由表,一个路由表包含多个handler。ioloop是服务的引擎核心,它是发动机,负责接收和响应客户端请求,负责驱动业务handler的运行,负责服务器内部定时任务的执行。
当一个请求到来时,ioloop读取这个请求解包成一个http请求对象,找到该套接字上对应app的路由表,通过请求对象的url查询路由表中挂接的handler,然后执行handler。handler方法执行后一般会返回一个对象,ioloop负责将对象包装成http响应对象序列化发送给客户端。
Tornado的异步非阻塞
Web框架分阻塞式和异步非阻塞2种。
大多数的Web框架(Django、Flask、Bottle)都是阻塞式的,体现在1个请求到达服务端如果服务端未处理完该请求,后续请求一直等待;解决方案可以是开启多线程/多进程,多个线程提高并发等。
异步非阻塞就是在服务端结合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监听列表中剔除;
class MainHandler(tornado.web.RequestHandler):
def get(self):
self.render("index.html", list_info=[11, 22, 33], key1='value1')
class AsyncHandler(tornado.web.RequestHandler): def get(self): import time time.sleep(20) self.write('666') application = tornado.web.Application([ (r"/index", MainHandler), (r"/async", AsyncHandler), ], **settings)
测试上面的代码,不难发现:先打开async页面,没有任何返回,一直显示在加载,此时访问index页面,发现index页面同时也在加载出不来。当async执行完,index页面也很快就返回了。
import tornado.ioloop import tornado.web from tornado.web import UIModule from tornado import gen from tornado.concurrent import Future import time class MainHandler(tornado.web.RequestHandler): def get(self): self.render("index.html", list_info=[11, 22, 33], key1='value1') class AsyncHandler(tornado.web.RequestHandler): @gen.coroutine def get(self): future = Future() tornado.ioloop.IOLoop.current().add_timeout(time.time() + 20, self.doing) yield future def doing(self, *args, **kwargs): self.write('async') self.finish() settings = { 'template_path': 'tmp', 'static_path': 'statics' } application = tornado.web.Application([ (r"/index", MainHandler), (r"/async", AsyncHandler), ], **settings) if __name__ == "__main__": application.listen(8888) tornado.ioloop.IOLoop.instance().start()
此时再执行上面的操作,就会发现当async阻塞等待返回的时候,其他网页是可以继续访问的。装饰器 + Future 实现了Tornado的异步非阻塞。
当发送GET请求时,由于方法被@gen.coroutine装饰且yield一个 Future对象,那么Tornado会等待,等待用户向future对象中放置数据或者发送信号,如果获取到数据或信号之后,就开始执行doing方法。异步非阻塞体现在当在Tornaod等待用户向future对象中放置数据时,还可以处理其他请求。
注意:在等待用户向future对象中放置数据或信号时,此连接是不断开的。
关于协程和asyncio
我们知道在程序在执行IO密集型任务的时候,程序会因为等待IO而阻塞,而协程作为一种用户态的轻量级线程,可以帮我们解决这个问题。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存,在调度回来的时候,恢复先前保存的寄存器上下文和栈。因此协程能保留上一次调用时的状态,即所有局部状态的一个特定组合。说白了就是,当协程遇到IO操作而阻塞时,立即切换到别的任务,如果操作完成则进行回调返回执行结果,提高了效率,同时这样也可以充分利用 CPU 和其他资源,这就是异步协程的优势,并且协程本质上是个单进程,相对于多进程来说,无需进程间上下文切换的开销,无需原子操作锁定及同步的开销,编程模型也非常简单。
在python2以及python3.3时代,使用协程还得基于greenlet或者gevent,greenlet机制的主要思想是:生成器函数或者协程函数中的yield语句挂起函数的执行,直到稍后使用next()或send()操作进行恢复为止。可以使用一个调度器循环在一组生成器函数之间协作多个任务,它的缺点是必须通过安装三方库进行使用,使用时由于封装特性导致性能有一定的流失。
终于在python3.4中,迎来了python的原生协程关键字:Async和Await,它们的底层基于生成器函数,使得协程的实现更加方便。Async 用来声明一个函数为异步函数,异步函数的特点是能在函数执行过程中挂起,去执行其他异步函数,等到挂起条件(假设挂起条件是sleep(5))消失后,也就是5秒到了再回来执行。Await 用来用来声明程序挂起,比如异步程序执行到某一步时需要等待的时间很长,就将此挂起,去执行其他的异步程序。
import time import asyncio async def job(t): # 使用 async 关键字将一个函数定义为协程 await asyncio.sleep(t) # 等待 t 秒, 期间切换执行其他任务 print('用了%s秒' % t) async def main(loop): # 使用 async 关键字将一个函数定义为协程 tasks = [loop.create_task(job(t)) for t in range(1,3)] # 创建任务, 不立即执行 await asyncio.wait(tasks) # 执行并等待所有任务完成 start = time.time() loop = asyncio.get_event_loop() # 建立 loop loop.run_until_complete(main(loop)) # 执行 loop loop.close() # 关闭 loop print(time.time()-start)
输出
用了1秒
用了2秒
2.0187768936157227
从运行结果可以看出,我们没有等待 job 1 执行结束再开始执行 job 2,而是 job 1 触发 await 的时候切换到了 job 2 。 这时 job 1 和 job 2 同时在执行 await asyncio.sleep(t),所以最终程序的执行时间取决于执行时间最长的那个 job,也就是 job 2 的执行时间:2 秒
import tornado.web from tornado import gen class IndexHandler(tornado.web.RequestHandler): def get(self): self.write('index') async def doing(): await gen.sleep(10) # here are doing some things return 'Non-Blocking' class NonBlockingHandler(tornado.web.RequestHandler): async def get(self): result = await doing() self.write(result) application = tornado.web.Application([ (r"/", IndexHandler), (r"/nonblocking", NonBlockingHandler), ]) if __name__ == "__main__": application.listen(8888) tornado.ioloop.IOLoop.instance().start()