Tornado 是一个Python web框架和异步网络库,起初由 FriendFeed(后该公司被facebook收购,目前tornado由facebook开发维护)开发.由于非阻塞的特性,他在处理Http长连接、websocket等保持连接时间较长的请求时,并发能力很强.
Tornado是非阻塞式服务器,是实时Web服务的一个理想框架,大体上可以被分为4个主要的部分:
- web框架(RequestHandler,Application)
- HTTP的客户端和web服务端实现 (AsyncHTTPClient and HTTPServer)
- 异步网络库 (IOLoop and IOStream)
- 协程库 (tornado.gen) 允许异步代码写的更直接而不用链式回调的方式
web框架 (包括创建web应用的 RequestHandler 类,还有很多其他支持的类).
tornado.web.Application类的实例化:
传递给Application类init方法的最重要的参数是handlers(是一个元组组成的列表,其中每个元组的第一个元素是一个用于匹配的正则表达式,第二个元素是一个RequestHanlder类).它告诉Tornado应该用哪个类来响应请求(如果一个正则表达式包含一个捕获分组(即,正则表达式中的部分被括号括起来),匹配的内容将作为相应HTTP请求的参数传到RequestHandler对象中).settings参数则是对这个app的一些设置.
一旦Application对象被创建,我们可以将其传递给Tornado的HTTPServer对象,然后使用我们在命令行指定的端口进行监听(通过options对象取出.)最后,在程序准备好接收HTTP请求后,我们创建一个Tornado的IOLoop的实例.
Application对象是负责全局配置的,包括映射请求转发给处理程序的路由表.static,template的内容.Application实例中的所有属性,BaseHandler中使用Application实例中的属性,以便所有的Handler都可以使用其属性.
Tornado的请求处理函数类.当处理一个请求时,Tornado将这个类实例化,并调用与HTTP请求方法所对应的方法(get,post方法)
tornado.web.RequestHandler的生命周期是initialize() -> prepare() -> get()/post() -> on_finish()
initialize() 在构造函数后调用,一般用于定义参数,不可异步
prepare() 在具体的get()/post()/put()/delete()等执行前调用,一般用于加载登录信息,过滤请求等,可异步
get()/post() 处理请求
on_finish() 请求后的清理,保存缓存,session等,该方法不能传递任何数据到客户端,所以不能操作cookie,可异步
getcurrentuser() 第一次使用实例中的currentuser参数且为None时会调用该方法,因为不能异步,所以需要通过异步来获取的,请写在prepare()中,blogxtg便是在prepare()中异步从redis读取.
RequestHandler类有一系列有用的内建方法,包括get_argument
RequestHandler的另一个有用的方法是write,它以一个字符串作为函数的参数,并将其写入到HTTP响应中
Tornado默认实现了几个常用的处理器
1 2 3 4
| ErrorHandler :生成指定状态码的错误响应. RedirectHandler :重定向请求. StaticFileHandler :处理静态文件请求. FallbackHandler :使可以在Tornado中混合使用其他HTTP服务器.
|
其它的Web 模块
escape - XHTML, JSON, URL 的编码/解码方法
database - 对 MySQLdb 的简单封装,使其更容易使用
template - 基于 Python 的 web 模板系统
httpclient - 非阻塞式 HTTP 客户端,它被设计用来和 web 及 httpserver 协同工作
auth - 第三方认证的实现(包括 Google OpenID/OAuth、Facebook Platform、Yahoo BBAuth、FriendFeed OpenID/OAuth、Twitter OAuth)
locale - 针对本地化和翻译的支持
options - 命令行和配置文件解析工具,针对服务器环境做了优化
Tornado可以解析URLencoded和multipart结构的POST请求
Tornado模板是被Python表达式和控制语句标记的简单文本文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| {{wood}} {% for book in books %} <li>{{ book }}</li> {% end %} }} self.render('index.html') {% extend base.html%} {% block body %}重写body block
|
如果你想编写一个可扩展的社交应用、实时分析引擎,或RESTful API,那么简单而强大的Python,以及Tornado正是为你准备的!
提供静态文件
1 2
| <link rel="stylesheet" href="{{ static_url("style.css") }}">
|
模板扩展(模块)
大多数站点希望复用像header、footer和布局网格这样的内容.在这一章中,我们将看到如何使用扩展Tornado模板或UI模块完成这一工作.一个拥有样式、布局和header/footer细节的主模版,以及一个处理页面的轻量级的子模板.
tornado通过extends和block语句支持模板继承,这就让你拥有了编写能够在合适的地方复用的流体模板的控制权和灵活性.
1 2 3 4 5 6 7
| {% extends "main.html" %}
|
UI模块是封装模板中包含的标记、样式以及行为的可复用组件(使模板部分模块化)
1.它所定义的元素通常用于多个模板交叉复用或在同一个模板中重复使用
2.模块本身是一个继承自Tornado的UIModule类的简单Python类,并定义了一个render方法.
当一个模板使用 module Foo(…) 标签引用一个模块时,Tornado的模板引擎调用模块的render方法,然后返回一个字符串来替换模板中的模块标签.
UI模块也可以在渲染后的页面中嵌入自己的JavaScript和CSS文件,或指定额外包含的JavaScript或CSS文件.
你可以定义可选的embedded_javascript、embedded_css、javascript_files和css_files方法来实现这一方法
1
| {% module xsrf_form_html() %}跨网站攻击
|
3.一个非常有用的做法是让模块指向一个模板文件而不是在模块类中直接渲染字符串
Tornado默认会自动转义(escape)模板中的内容,把标签转换为相应的HTML实体,而不会被作为一个HTML元素解释(以免被浏览器执行).防止你的访客进行恶意攻击的.
举个例子,如果Burt想在footer中使用模板变量设置email联系链接,他将不会得到期望的HTML链接.考虑下面的模板片段:
1 2 3 4 5 6
| {% set mailLink = "<a href="mailto:contact@burtsbooks.com">Contact Us</a>" %} {{ mailLink }}' # 它会在页面源代码中渲染成如下代码:
<a href="mailto:contact@burtsbooks.com">Contact Us</a> # 此时自动转义被运行了,很明显,这无法让人们联系上Burt.
|
为了处理这种情况,你可以禁用自动转义,一种方法是在Application构造函数中传递autoescape=None,另一种方法是在每页的基础上修改自动转义行为,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| {% autoescape None %} {{ mailLink }} {% raw mailLink %} {% block footer %} <p> For more information about our selection, hours or events, please email us at <a href="mailto:contact@burtsbooks.com">contact@burtsbooks.com</a>. </p> <p class="small"> Follow us on Facebook at {% raw linkify("https://fb.me/burtsbooks", extra_params='ref=website') %}. {% apply linkify or 自定义方法%} Application中get_template_namespace </p> {% end %}
|
异步网络库 (IOLoop and IOStream), 为HTTP组件提供构建模块,也可以用来实现其他协议.
一般的web应用是阻塞性质的,也就是说当一个请求被处理时,这个进程就会被挂起直至请求完成.这意味着应用程序被有效的锁定直至处理结束,很明显这在可扩展性上出现了问题.
不过,Tornado给了我们更好的方法来处理这种情况.应用程序在等待第一个处理完成的过程中,让I/O循环打开以便服务于其他客户端,直到处理完成时启动一个请求并给予反馈,而不再是等待请求完成的过程中挂起进程.
同时实时web功能需要为每个用户提供一个多数时间被闲置的长连接, 在传统的同步web服务器中,这意味着要为每个用户提供一个线程, 当然每个线程的开销都是很昂贵的.
为了尽量减少并发连接造成的开销,Tornado使用了一种单线程事件循环的方式. 这就意味着所有的应用代码都应该是异步非阻塞的, 因为在同一时间只有一个操作是有效的

一般的异步回调
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| import time import threading def long_io(callback): """将耗时的操作交给另一线程来处理""" def fun(cb): """耗时操作""" print( "开始执行IO操作") time.sleep(5) print("完成IO操作,并执行回调函数") cb("io result") threading._start_new_thread(fun, (callback,)) def on_finish(ret): """回调函数""" print( "开始执行回调函数on_finish") print( "ret: %s" % ret) print( "完成执行回调函数on_finish") def req_a(): """将req_q逻辑拆分了""" 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()
|
yield控制进程的暂停与send又恢复
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| import time import threading gen = None def long_io(): def fun(): print("开始执行IO操作") global gen time.sleep(5) try: print("完成IO操作,并send结果唤醒挂起程序继续执行") gen.send("io result") except StopIteration: pass threading._start_new_thread(fun, ()) def req_a(): """将req_q逻辑组织在一起""" 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() next(gen) req_b() while 1: pass if __name__ == '__main__': main()
|
改进yield
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| import time import threading gen = None def gen_coroutine(f): def wrapper(*args, **kwargs): global gen gen = f() next(gen) return wrapper def long_io(): def fun(): print("开始执行IO操作") global gen time.sleep(5) try: print("完成IO操作,并send结果唤醒挂起程序继续执行") gen.send("io result") except StopIteration: pass threading._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()
|
理解Tornado异步编程原理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| import time import threading def gen_coroutine(f): def wrapper(*args, **kwargs): gen_f = f() r = next(gen_f) def fun(g): ret = next(g) try: gen_f.send(ret) except StopIteration: pass threading._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()
|
但tornado是单线程的,实现异步的机制不是线程,而是epoll,即将异步过程交给epoll执行并进行监视回调
Tornado默认在函数处理返回时关闭客户端的连接.在通常情况下,这正是你想要的.但是当我们处理一个需要回调函数的异步请求时,我们需要连接保持开启状态直到回调函数执行完毕.
tornado.web.asynchronous 装饰器可以用来代替异步处理. 当使用这个装饰器的时候, 响应不会自动发送; 而请求将一直保持开放直到callback调用 RequestHandler.finish
1 2 3 4 5 6 7 8 9 10 11 12 13
| class MainHandler(tornado.web.RequestHandler): @tornado.web.asynchronous def get(self): http = tornado.httpclient.AsyncHTTPClient() http.fetch("http://friendfeed-api.com/v2/feed/bret", callback=self.on_response) def on_response(self, response): if response.error: raise tornado.web.HTTPError(500) json = tornado.escape.json_decode(response.body) self.write("Fetched " + str(len(json["entries"])) + " entries " "from the FriendFeed API") self.finish()
|
记住当你使用@tornado.web.asynchonous装饰器时,Tornado永远不会自己关闭连接.
你必须在你的RequestHandler对象中调用finish方法来显式地告诉Tornado关闭连接.(否则,请求将可能挂起,浏览器可能不会显示我们已经发送给客户端的数据.)
目前非阻塞IO的框架在近几年的web技术中特别火,比如node.js,而tornado依靠底层基于epoll(Linux)或者kqueue(BSD和MAC OSX)的IOLoop实现非阻塞IO,而且经过FriendFeed的实践,已经证明他是绝对优秀可靠的非阻塞IO框架,再加上他协程特性,让基于他的异步代码可以像阻塞多线程框架的同步代码一样易读易维护.
协程库 (tornado.gen) 允许异步代码写的像同步代码一样直观,而不用链式回调的方式.
tornado.gen模块,可以提供一个更整洁的方式来执行异步请求.
tornado.gen.Task对象的一个实例,将我们想要的调用和传给该调用函数的参数传递给那个函数.
这里,yield的使用返回程序对Tornado的控制,允许在HTTP请求进行中执行其他任务.
当HTTP请求完成时,RequestHandler方法在其停止的地方恢复.这种构建的美在于它在请求处理程序中返回HTTP响应,而不是回调函数中
Tornado的异步功能可以非常方便的创建依赖于缓慢查询或外部服务的Web应用
使用 @tornado.gen.coroutine 装饰器是做异步(协程)最简单的方式. 这允许你使用 yield 关键 字执行非阻塞I/O, 并且直到协程返回才发送响应.
1 2 3 4 5 6 7 8
| class MainHandler(tornado.web.RequestHandler): @tornado.gen.coroutine def get(self): http = tornado.httpclient.AsyncHTTPClient() response = yield http.fetch("http://friendfeed-api.com/v2/feed/bret") json = tornado.escape.json_decode(response.body) self.write("Fetched " + str(len(json["entries"])) + " entries " "from the FriendFeed API")
|
协程使用了Python的 yield 关键字代替链式回调来将程序挂起和恢复执行
调用协程时,几乎所有的情况下, 任何一个调用协程的函数都必须是协程它自身, 并且在 调用的时候使用 yield 关键字
HTTP的服务端和客户端实现 (HTTPServer and AsyncHTTPClient).
单线程server
tornado不仅仅是一个web framework,他还是一个简易的web server,这让他可以直接作为一个server来接收处理http请求,而不需要依靠wsgi容器.但是这个webserver过于简单,只支持单进程,所以在生产环境中,官方推荐的多进程多主机部署,启动多个tornado server实例分别监听不同端口,在上层通过类似nginx的成熟高效的http server来做负载均衡,将请求转发到合适端口的tornado实例中.(参考tornado官方文档运行部署篇)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94
| import json import sys import tornado.httpserver import tornado.ioloop import tornado.options import tornado.gen import tornado.httpclient from tornado.options import define, options import tornado.web import os.path define("port", default=8080, type=int) class MainHandler(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:0"} else: rep = json.loads(response.body) return rep class Application(tornado.web.Application): def __init__(self): settings = dict( template_path=os.path.join(os.path.dirname(__file__), "templates"), static_path=os.path.join(os.path.dirname(__file__), "media"), xsrf_cookies=False, cookie_secret="bZJc2sWbQLKos6GkHn/VB9oXwQt8S0R0kRvJ5/xJ89E=", login_url="/a/user/login", debug=True, ) super(Application, self).__init__([(r'/',MainHandler)], **settings) def main(): tornado.options.parse_command_line() ssl_options = { "certfile": os.path.join(os.path.abspath("."), "213976088220462.pem"), "keyfile": os.path.join(os.path.abspath("."), "213976088220462.key"), } print(sys.getrecursionlimit()) print(options.port) Application().listen(options.port) io = tornado.ioloop.IOLoop.current() io.start() if __name__ == "__main__": main()
|
使用Tornado进行长轮询(应该有点 即时通讯IM)
Tornado异步架构的另一个优势是它能够轻松处理HTTP长轮询.这是一个处理实时更新的方法,它既可以应用到简单的数字标记通知,也可以实现复杂的多用户聊天室.
在大多数情况下,你至少希望将结果缓存以便两次相同搜索项的请求不会导致再次向远程API执行完整请求.
在前端使用像JavaScript这样的工具处理异步应用,让客户端承担更多工作,以提高你应用的扩展性.
部署提供实时更新的Web应用对于Web程序员而言是一项长期的挑战.更新用户状态、发送新消息提醒、或者任何一个需要在初始文档完成加载后由服务器向浏览器发送消息方法的全局活动.
所谓的”服务器推送”技术允许Web应用实时发布更新,同时保持合理的资源使用以及确保可预知的扩展.对于一个可行的服务器推送技术而言,它必须在现有的浏览器上表现良好.最流行的技术是让浏览器发起连接来模拟服务器推送更新.这种方式的HTTP连接被称为长轮询或Comet请求.
长轮询意味着浏览器只需启动一个HTTP请求,其连接的服务器会有意保持开启.浏览器只需要等待更新可用时服务器”推送”响应.当服务器发送响应并关闭连接后,(或者浏览器端客户请求超时),客户端只需打开一个新的连接并等待下一个更新.
长轮询的好处
HTTP长轮询的主要吸引力在于其极大地减少了Web服务器的负载.相对于客户端制造大量的短而频繁的请求(以及每次处理HTTP头部产生的开销),服务器端只有当其接收一个初始请求和再次发送响应时处理连接.大部分时间没有新的数据,连接也不会消耗任何处理器资源.
浏览器兼容性是另一个巨大的好处.任何支持AJAX请求的浏览器都可以执行推送请求.不需要任何浏览器插件或其他附加组件.对比其他服务器端推送技术,HTTP长轮询最终成为了被广泛使用的少数几个可行方案之一.
我们已经接触过长轮询的一些使用.实际上,前面提到的状态更新、消息通知以及聊天消息都是目前流行的网站功能.像Google Docs这样的站点使用长轮询同步协作,两个人可以同时编辑文档并看到对方的改变.Twitter使用长轮询指示浏览器在新状态更新可用时展示通知.Facebook使用这项技术在其聊天功能中.长轮询如此流行的一个原因是它改善了应用的用户体验:访客不再需要不断地刷新页面来获取最新的内容