简介
Tornado是一个Python Web框架和异步网络库,最初是在FriendFeed上开发的。通过使用非阻塞网络I/O,Tornado可以扩展到数万个开放连接,使其成为长轮询,WebSockets和其他需要与每个用户建立长连接的应用程序的理想选择。
Tornado大致可分为四个主要部分:
- 一个Web框架(包括
RequestHandler
,它是子类,用于创建Web应用程序和各种支持类)。 - HTTP(
HTTPServer
和AsyncHTTPClient
)的客户端和服务器端实现。 - 一个异步网络库,包括IOLoop和IOStream类,它们用作HTTP组件的构建块,也可用于实现其他协议。
- 一个协程库(
tornado.gen
),它允许以比链接回调更直接的方式编写异步代码。这类似于Python 3.5(async def
)中引入的原生协程功能。建议使用原生协程代替tornado.gen
模块。
Tornado Web框架和HTTP服务器一起提供了WSGI的全栈替代方案。虽然可以在WSGI容器(WSGIAdapter
)中使用Tornado Web框架,或者使用Tornado HTTP服务器作为其他WSGI框架(WSGIContainer
)的容器,但是这些组合中的每一个都有局限性,并且要充分利用Tornado,你将会需要一起使用Tornado的Web框架和HTTP服务器。
非阻塞异步IO
实时Web功能需要每个用户保持大部分时间为空闲状态的长连接。在传统的同步Web服务器中,这意味着将一个线程投入到每个用户,这可能非常昂贵。
为了最小化并发连接的成本,Tornado使用单线程事件循环。这意味着所有应用程序代码都应该是异步和非阻塞的,因为一次只能有一个操作处于活动状态。
术语异步和非阻塞是密切相关的,并且通常可以互换使用,但它们并不完全相同。
阻塞
函数在返回之前等待某事发生时会阻塞。一个函数可能由于多种原因而阻塞:网络I/O,磁盘I/O,互斥体等。事实上,每个函数在运行和使用CPU时都会至少有一点阻塞(对于一个极端的例子来说明为什么CPU阻塞必须像其他类型的阻塞一样严肃,考虑密码散列函数,如bcrypt,它设计使用数百毫秒的CPU时间,远远超过典型的网络或磁盘访问)。
函数可以在某些方面阻塞,在其他方面非阻塞。在Tornado的上下文中,我们通常谈论在网络I/O的上下文中阻塞,尽管要最小化所有类型的阻塞。
异步
通常会在触发一些后续操作之前导致某些后台工作发生(这是相对于正常的异步函数来说,正常的异步函数会在return之前做完它们要做的所有事情)。有很多种不同种类的异步接口:
- 参数回调(Callback argument)
- 返回占位符(Future,Promise,Deferred)
- 送入队列
- 注册回调(例如POSIX信号)
无论使用哪种类型的接口,定义的异步函数与其调用者的交互方式都不同,没有一种自由的方式可以使同步函数以对其调用者透明的方式实现异步
(像gevent这样的系统使用轻量级线程来提供与异步系统相当的性能,但它们实际上并没有实现异步)。
Tornado中的异步操作通常返回占位符对象(Futures
),但一些低级别组件除外,比如IOLoop
中就使用回调。Futures
通常会通过await
或yield
关键字返回结果。
例子
下面是一个同步函数的例子:
from tornado.httpclient import HTTPClient
def synchronous_fetch(url):
http_client = HTTPClient()
response = http_client.fetch(url)
return response.body
下面使用原生协程重写,使之变为一个实现相同功能的异步函数:
from tornado.httpclient import AsyncHTTPClient
async def asynchronous_fetch(url):
http_client = AsyncHTTPClient()
response = await http_client.fetch(url)
return response.body
或者为了与旧版本的Python兼容,使用tornado.gen模块:
from tornado.httpclient import AsyncHTTPClient
from tornado import gen
@gen.coroutine
def async_fetch_gen(url):
http_client = AsyncHTTPClient()
response = yield http_client.fetch(url)
raise gen.Return(response.body)
协程是不是看上去有点神奇,但他们内部做的是这样的:
from tornado.concurrent import Future
def async_fetch_manual(url):
http_client = AsyncHTTPClient()
my_future = Future()
fetch_future = http_client.fetch(url)
def on_fetch(f):
my_future.set_result(f.result().body)
fetch_future.add_done_callback(on_fetch)
return my_future
请注意,协程在获取完成之前返回其Future。 这是协程异步的原因。
你可以通过传递回调对象来执行协程所能做的任何事情,但协程可以像写同步代码一样实现异步功能,这样可以简化我们的代码。这对于错误处理尤为重要,因为try/except
块的工作方式与协程中的预期相同,
而回调很难实现。
协程
Coroutines是在Tornado中编写异步代码的推荐方法。Coroutines使用Pythonawait
或yield
关键字来挂起和恢复执行而不是一系列回调(在gevent这样的框架中看到的协作轻量级线程有时也被称为协程,但在Tornado中所有协程都使用显式上下文切换并被称为异步函数)。
协程几乎和同步代码一样简单,而且没有线程那样的昂贵开销。它们还通过减少可能发生的上下文切换来简化并发。
例子:
async def fetch_coroutine(url):
http_client = AsyncHTTPClient()
response = await http_client.fetch(url)
return response.body
原生协程VS装饰器协程
Python 3.5引入了async和await关键字(使用这些关键字的函数也称为“native coroutines”)。 为了与旧版本的Python兼容,您可以使用tornado.gen.coroutine装饰器来使用“decorated”或“yield-based”的协程。
尽可能使用原生协程。 仅在需要与旧版本的Python兼容时才使用装饰器协程。Tornado文档中的示例通常使用原生形式。
两种形式之间的转换通常很简单:
# Decorated: # Native:
# Normal function declaration
# with decorator # "async def" keywords
@gen.coroutine
def a(): async def a():
# "yield" all async funcs # "await" all async funcs
b = yield c() b = await c()
# "return" and "yield"
# cannot be mixed in
# Python 2, so raise a
# special exception. # Return normally
raise gen.Return(b) return b
其它两种形式的协程区别:
- 原生协程通常更快。
- 原生协程可以使用
async for
和async
语句,这使得某些模式更加简单。 - 除非
yield
或await
它们,否则原生协程根本不会运行。装饰器协程一旦被调用就可以“在后台”开始运行。请注意,对于这两种协程,使用await
或yield
很重要,这样任何异常才能正常抛出。 - 装饰器协程与concurrent.futures包有额外的集成,允许直接生成
executor.submi
的结果。对于原生协程,请改用IOLoop.run_in_executor
。 - 装饰器协程通过产生列表或字典来支持等待多个对象的一些简写。使用tornado.gen.multi在原生协程中执行此操作。
- 装饰器协程可以支持与其他软件包的集成,包括通过转换函数注册表的Twisted。要在原生协程中访问此功能,请使用
tornado.gen.convert_yielded
。 - 装饰器协程总是返回一个Future对象。原生协程返回一个不是Future的等待对象。在Tornado中,两者大多可以互换。
工作原理
本节介绍装饰器协程的操作。原生协程在概念上是相似的,但由于与Python运行时的额外集成而稍微复杂一些。
包含yield
的函数是生成器。所有生成器都是异步的,在调用时,它们返回一个生成器对象而不是运行到完成。 @gen.coroutine
装饰器通过yield
表达式与生成器通信,并通过返回Future
与协程的调用者通信。
这是协程装饰器内循环的简化版本:
# Simplified inner loop of tornado.gen.Runner
def run(self):
# send(x) makes the current yield return x.
# It returns when the next yield is reached
future = self.gen.send(self.next)
def callback(f):
self.next = f.result()
self.run()
future.add_done_callback(callback)
装饰器从生成器接收Future
,等待(不阻塞)该Future
完成,然后“展开”Future
并将结果作为yield
表达式的结果发送回生成器。 大多数异步代码从不直接接触Future
类,除非立即将异步函数返回的Future
传递给yield
表达式。
如何调用一个协程
协程不会以正常方式抛出异常:它们抛出的任何异常都将被困在等待对象中,直到它被放弃为止。 这意味着以正确的方式调用协同程序很重要,否则您可能会发现未被注意到的错误:
async def divide(x, y):
return x / y
def bad_call():
# This should raise a ZeroDivisionError, but it won't because
# the coroutine is called incorrectly.
divide(1, 0)
在几乎所有情况下,任何调用协程的函数都必须是协程本身,并在调用中使用await
或yield
关键字。 当重写超类中定义的方法时,请查阅文档以查看是否允许协程(文档应该说方法“可能是协程”或“可能返回Future
”):
async def good_call():
# await will unwrap the object returned by divide() and raise
# the exception.
await divide(1, 0)
有时你可能想要“Fire and forget”一个协程而不等待它的结果。在这种情况下,建议使用IOLoop.spawn_callback
,这使得IOLoop
负责调用。 如果失败,IOLoop
将记录堆栈路径:
# The IOLoop will catch the exception and print a stack trace in
# the logs. Note that this doesn't look like a normal call, since
# we pass the function object to be called by the IOLoop.
IOLoop.current().spawn_callback(divide, 1, 0)
对于使用@gen.coroutine
的函数,建议以这种方式使用IOLoop.spawn_callback
,但是使用async def
的函数需要它(否则协程运行程序将无法启动)。
最后,在程序的顶层,如果IOLoop
尚未运行,您可以启动IOLoop
,运行协程,然后使用IOLoop.run_sync
方法停止IOLoop。 这通常用于启动面向批处理( batch-oriented)程序的main
函数:
# run_sync() doesn't take arguments, so we must wrap the
# call in a lambda.
IOLoop.current().run_sync(lambda: divide(1, 0))
协程模式
调用阻塞函数(Calling blocking functions)
从协程中调用一个阻塞函数的最简单的方法就是使用ThreadPoolExecutor
,返回一个其他协程兼容的Futures
对象:
async def call_blocking():
await IOLoop.current().run_in_executor(None, blocking_func, args)
并行(Parallelism)
multi
函数接受其值为Futures
的列表和dicts,并且并行等待所有这些Futures
:
from tornado.gen import multi
async def parallel_fetch(url1, url2):
resp1, resp2 = await multi([http_client.fetch(url1),
http_client.fetch(url2)])
async def parallel_fetch_many(urls):
responses = await multi ([http_client.fetch(url) for url in urls])
# responses is a list of HTTPResponses in the same order
async def parallel_fetch_dict(urls):
responses = await multi({url: http_client.fetch(url)
for url in urls})
# responses is a dict {url: HTTPResponse}
在装饰器协程中,可以直接yield
列表或字典:
@gen.coroutine
def parallel_fetch_decorated(url1, url2):
resp1, resp2 = yield [http_client.fetch(url1),
http_client.fetch(url2)]
交叉存取(Interleaving)
有时保存一个Future对象比立即yield它会更有用,以便你可以在等待之前开始开始另一个操作:
from tornado.gen import convert_yielded
async def get(self):
# convert_yielded() starts the native coroutine in the background.
# This is equivalent to asyncio.ensure_future() (both work in Tornado).
fetch_future = convert_yielded(self.fetch_next_chunk())
while True:
chunk = yield fetch_future
if chunk is None: break
self.write(chunk)
fetch_future = convert_yielded(self.fetch_next_chunk())
yield self.flush()
这对于装饰的协同程序来说更容易一些,因为它们在被调用时立即启动:
@gen.coroutine
def get(self):
fetch_future = self.fetch_next_chunk()
while True:
chunk = yield fetch_future
if chunk is None: break
self.write(chunk)
fetch_future = self.fetch_next_chunk()
yield self.flush()
循环(Looping)
在原生协程中,可以使用async for
。在旧版本的Python中,循环对于协程来说很棘手,因为无法在for
循环或while
循环的每次迭代中yield
并捕获yield
的结果。相反,您需要将循环条件与访问结果分开,如本例中的Motor
:
import motor
db = motor.MotorClient().test
@gen.coroutine
def loop_example(collection):
cursor = db.collection.find()
while (yield cursor.fetch_next):
doc = cursor.next_object()
后台运行(Running in the background)
PeriodicCallback
通常不与协同程序一起使用。相反,一个协程可以包含一个while True:
:循环并使用tornado.gen.sleep
:
async def minute_loop():
while True:
await do_something()
await gen.sleep(60)
# Coroutines that loop forever are generally started with
# spawn_callback().
IOLoop.current().spawn_callback(minute_loop)
有时可能需要更复杂的循环。 例如,前一个循环每60 + N
秒运行一次,其中N
是do_something()
的运行时间。 要完全每60秒运行一次,请使用上面的交叉存取:
async def minute_loop2():
while True:
nxt = gen.sleep(60) # Start the clock.
await do_something() # Run while the clock is ticking.
await nxt # Wait for the timer to run out.
Queue示例 - 一个并发的网络爬虫
Tornado的tornado.queues
模块为协程实现异步生产者/消费者模式,类似于Python标准库的队列模块为线程实现的模式。
一个yieldQueue.get
的协程直到队列中有元素之前都会暂停。如果队列设置了最大容量,一个yieldQueue.put
的协程在队列有空间之前都会暂停。
一个Queue
维护一个从零开始的未完成任务的计数。put
增加计数; task_done
减少计数。
在此处的web-spider示例中,队列开始仅包含base_url。当一个worker获取一个页面时,它会解析链接并将新的链接放入队列中,然后调用task_done
来减少一次计数器。 最终,一个worker获取一个之前URL已经被访问过的页面,并且队列中也没有剩余的工作。 因此,该worker对task_done
的调用将计数器减少为零。 正在等待join
的主协程将取消暂停然后结束。
#!/usr/bin/env python3
import time
from datetime import timedelta
from html.parser import HTMLParser
from urllib.parse import urljoin, urldefrag
from tornado import gen, httpclient, ioloop, queues
base_url = 'http://www.tornadoweb.org/en/stable/'
concurrency = 10
async def get_links_from_url(url):
"""Download the page at `url` and parse it for links.
Returned links have had the fragment after `#` removed, and have been made
absolute so, e.g. the URL 'gen.html#tornado.gen.coroutine' becomes
'http://www.tornadoweb.org/en/stable/gen.html'.
"""
response = await httpclient.AsyncHTTPClient().fetch(url)
print('fetched %s' % url)
html = response.body.decode(errors='ignore')
return [urljoin(url, remove_fragment(new_url))
for new_url in get_links(html)]
def remove_fragment(url):
pure_url, frag = urldefrag(url)
return pure_url
def get_links(html):
class URLSeeker(HTMLParser):
def __init__(self):
HTMLParser.__init__(self)
self.urls = []
def handle_starttag(self, tag, attrs):
href = dict(attrs).get('href')
if href and tag == 'a':
self.urls.append(href)
url_seeker = URLSeeker()
url_seeker.feed(html)
return url_seeker.urls
async def main():
q = queues.Queue()
start = time.time()
fetching, fetched = set(), set()
async def fetch_url(current_url):
if current_url in fetching:
return
print('fetching %s' % current_url)
fetching.add(current_url)
urls = await get_links_from_url(current_url)
fetched.add(current_url)
for new_url in urls:
# Only follow links beneath the base URL
if new_url.startswith(base_url):
await q.put(new_url)
async def worker():
async for url in q:
if url is None:
return
try:
await fetch_url(url)
except Exception as e:
print('Exception: %s %s' % (e, url))
finally:
q.task_done()
await q.put(base_url)
# Start workers, then wait for the work queue to be empty.
workers = gen.multi([worker() for _ in range(concurrency)])
await q.join(timeout=timedelta(seconds=300))
assert fetching == fetched
print('Done in %d seconds, fetched %s URLs.' % (
time.time() - start, len(fetched)))
# Signal all the workers to exit.
for _ in range(concurrency):
await q.put(None)
await workers
if __name__ == '__main__':
io_loop = ioloop.IOLoop.current()
io_loop.run_sync(main)
一个Tornado网络应用的结构
Tornado Web应用程序通常由一个或多个RequestHandler
子类,一个将请求路由到处理程序(handlers)的Application
对象和一个用于启动服务器的main()
函数组成。
最小的“hello world”示例如下所示:
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()
Application
对象
Application对象负责全局配置,包括将请求映射到处理程序的路由表。
路由表是URLSpec
对象(或元组)的列表,每个对象包含(至少)正则表达式和处理程序类。 顺序很重要: 最终生效的是第一个匹配规则。如果正则表达式包含匹配分组,
这些分组会作为路径参数传递给处理器的HTTP方法。如果一个字典作为URLSpec
的第三方元素被传递进来,它支持initialization参数,这个参数会传递给RequestHandler.initializa
方法。
最后,URLSpec
可能还会有一个名字,这将使它能够被RequestHandler.reverse_url
方法使用。
例如,在此片段中,根URL /
映射到MainHandler
,表单/story/
后跟数字的URL映射到StoryHandler
。 该数字(作为字符串)传递给StoryHandler.get
。
class MainHandler(RequestHandler):
def get(self):
self.write('<a href="%s">link to story 1</a>' %
self.reverse_url("story", "1"))
class StoryHandler(RequestHandler):
def initialize(self, db):
self.db = db
def get(self, story_id):
self.write("this is story %s" % story_id)
app = Application([
url(r"/", MainHandler),
url(r"/story/([0-9]+)", StoryHandler, dict(db=db), name="story")
])
Application
构造器接受许多能用来指定应用行为的关键字参数,并且允许可选特性;完整列表请参看Application.settings
。
基类化RequestHandler
Tornado Web应用程序的大部分工作都是在RequestHandler
的子类中完成的。处理器子类的主要入口是处理对应的HTTP方法:get()
,post()
等等。每个处理程序可以定义一个或多个这些方法来处理不同的HTTP操作。如上所述,这些方法将会加上路径中匹配到的参数来被调用。
在处理程序中,调用RequestHandler.render
或RequestHandler.write
等方法来生成响应。 render()
按名称加载模板,并使用给定的参数呈现它。 write()
用于非基于模板的输出;它接受strings,bytes和字典(dicts将被编码为JSON)。
RequestHandler
中的许多方法都设计为在子类中重写,并可以在整个应用程序中使用。通常定义一个BaseHandler
类来重写诸如write_error
和get_current_user
之类的方法,然后把你的BaseHandler代替RequestHandler
作为所有处理器的基类。
处理请求输入
请求处理程序可以使用self.request
访问表示当前请求的对象。有关完整的属性列表,请参阅HTTPServerRequest
的类定义。
HTML表单提交的请求数据将会为你分析好,并且可以在一些方法像get_query_argument
和get_body_argument中
可用。
class MyFormHandler(tornado.web.RequestHandler):
def get(self):
self.write('<html><body><form action="/myform" method="POST">'
'<input type="text" name="message">'
'<input type="submit" value="Submit">'
'</form></body></html>')
def post(self):
self.set_header("Content-Type", "text/plain")
self.write("You wrote " + self.get_body_argument("message"))
因为HTML表单编码对于一个参数是单值还是列表是模糊不清的,RequestHandler
对方法进行了区分,使得应用能够表明是否希望获得列表,对列表来说,使用get_query_arguments
和get_body_arguments
代替单值的方法。
通过表单上传文件可以通过self.request.files
来获得,映射名字(input的name属性)到文件列表。每个文件都是{"filename":…, "content_type":…, "body":…}
格式的字典。files
对象文件仅在使用表单包装器上传文件时才存在(例如multipart/form-data Content-Type
);如果没有使用这种格式,原生的上传数据会存放在self.request.boby
中。默认情况下上传的文件是全部缓存在内存中的;如果你需要处理一些很大的文件,不方便放在内存中,查看stream_request_body
装饰器。
在demos文件夹中(tornado源码中有一个demos文件夹,存放了几个小的例子),file_reciver.py展示了这两种方法的来接收上传文件。
由于HTML表单编码的问题(单值和多值的模糊性),Tornado并不打算用其他类型的输入来统一表单参数。尤其我们不会去分析JSON请求体。希望使用JSON来代替form-encoding的应用可能会重写prepare
方法来解析它们的请求:
def prepare(self):
if self.request.headers.get("Content-Type", "").startswith("application/json"):
self.json_args = json.loads(self.request.body)
else:
self.json_args = None
重写RequestHandler中的方法
除了get()
和post()
方法,在RequestHandler
还有一些其他方法在必要时也可以被子类重写。在每个请求中,都会发生以下一系列的调用:
- 每个请求中都会新建一个
RequestHandler
对象。 - 如果有从
Application
配置中获得初始参数的话initialize()
函数会被调用,initialize
函数应该只保存传递给成员变量的参数;它可能不会产生任何输出或调用类似send_error
一样的方法。 - 调用
prepare
方法。这在一个基类中是最有用的,基类由你的处理器子类所共享,因为不论使用哪种HTTP方法prepare
函数都会被调用。prepare
可能会产生输出;如果它调用了finish
(或者redirect
方法等),进程就会在此结束。 - 某一个HTTP方法被调用:
get()
,post()
,put()
等等。如果URL正则表达式包含了捕捉分组参数(capturing groups),这些参数也会传递到此方法中。 - 当请求结束时,
on_finish
会被调用,对于大多数处理程序,这一步在get()
(或其他方法)return后就会立即执行;对于使用tornado.web.asynchronous
装饰器的处理程序,它发生在调用finish()
之后。
正如在RequestHandler
文档中提到的一样,所有方法都是可以重写的。 一些最常被重写的方法包括:
write_error
- 输出html的错误页面。on_connection_close
当客户端断开连接的时候调用;应用可能会选择检测这种情况然后停止更深层的处理,注意,不能保证一个已关闭的连接也能被及时检测到。 请注意,无法保证可以立即检测到已关闭的连接。get_current_user
- 请参阅用户身份验证(User authentication)。get_user_locale
- 为当前用户返回一个locale
对象。set_default_headers
- 可用于在response上设置其他的响应头(例如自定义Server
header)
错误处理
如果处理器抛出一个异常,tornado会调用RequestHandler.write_error
来生成一个错误页面。tornado.web.HTTPError
可以被用来产生一个特定的状态码;其他所有异常都返回500状态码。
默认的错误页面包括一个堆栈路径(debug模式下)另外还有一个错误的线上描述(move brand_model.txt to project)。为了生成一个自己的错误页面,重写RequestHandler.write_error
(可能是在一个由你的所有处理器所共享的基类中)这个方法可以用过write
和render
等方法产生正常的输出。如果错误是由一个异常导致的,一个exc_info
会作为一个关键字参数被传递进来(注意,此异常不保证是sys.exc_info
中当前的异常,因此write_error
必须使用traceback.format_exception
来代替traceback.format_exc
)。
通过调用set_status
,写一个响应或return等方法从常规的处理器方法(而不是write_error
)中产生一个错误页面也是可能的。tornado.web.Finish
这个特殊的异常可能会被抛出以终止处理器,在简单地返回不方便的情况下并不调用write_error
方法。
对于404错误来说,使用default_handler_class``````Application setting
。这个处理器用改重写prepare
方法,而不是更详细的如get()
方法,以便在任何HTTP方法中都能使用。它应该产生一个如上所述的错误页面:或者通过抛出一个HTTPError(404)
并重写为write_error
,或者调用self.set_status(404)
并在prepare()
中直接产生响应。
重定向
在Tornado中有两种方式可以重定一个请求:RequestHandler.redirect
和RedirectHandler
。
您可以在RequestHandler
方法中使用self.redirect()
将用户重定向到其他位置。还有一个可选参数permanent
,可用于表明该重定向为永久重定向。 permanent
的默认值为False
,它生成302 Found
HTTP响应代码,适用于成功POST请求后重定向用户等事项。 如果permanent
是true
,301 Moved Permanently
HTTP响应码会被使用,在下面这中情况下是有用的:重定向到一个权威的URL来采用一种SEO友好的方式获取页面。
RedirectHandler
可以让你在你的Application
路由表中直接配置重定向。例如,配置一个静态重定向:
app = tornado.web.Application([
url(r"/app", tornado.web.RedirectHandler,
dict(url="http://itunes.apple.com/my-app-id")),
])
RedirectHandler
还支持正则表达式替换。以下规则将以/pictures/
开头的所有请求重定向到前缀/photos/
:
app = tornado.web.Application([
url(r"/photos/(.*)", MyPhotoHandler),
url(r"/pictures/(.*)", tornado.web.RedirectHandler,
dict(url=r"/photos/{0}")),
])
不像RequestHandler.redirect
,RedirectHandler
默认使用永久重定向。这是因为路由表在运行过程中不会改变并且是永久的,尽管在处理器中的重定向很可能是其他可能回改变的逻辑所导致的。如果想使用RedirectHandler
发起一个临时重定向,只需要把permanent=False
参数加到RedirectHandler
的初始化参数中。
异步处理器
某些处理程序方法(包括prepare()
和HTTP请求方法get()
、post()
等)可以作为协程重写,以实现异步化。
Tornado还支持使用tornado.web.asynchronous
装饰器那样基于回调的异步处理程序样式,但这种样式已弃用,将在Tornado 6.0中删除。新的应用程序应该使用协程代替旧的写法。
下面是一个简单的使用协程的handler示例:
class MainHandler(tornado.web.RequestHandler):
async def get(self):
http = tornado.httpclient.AsyncHTTPClient()
response = await 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")
有关更高级的异步示例,请查看聊天示例应用程序,该应用程序使用长轮询实现AJAX聊天室。 长轮询的用户可能希望在客户端关闭连接后重写on_connection_close
()以进行清理操作(但要注意的是,请参见该方法的docstring)。
模板与UI
Tornado包含一种简单,快速,灵活的模板语言。 本节介绍该语言以及国际化等相关问题。
Tornado可以任意使用其他的Python模板语言,哪怕它们并没有被集成到RequestHandler.render
中。 只需将模板渲染为字符串并将其传递给RequestHandler.write
即可。
配置模板
默认情况下,Tornado在与引用它们的.py文件的目录中查找模板文件。要将模板文件放在不同的目录中,请使用template_path
应用程序设置(如果不同的处理程序具有不同的模板路径,则修改RequestHandler.get_template_path
)即可。
要从非文件系统位置加载模板,请子类化tornado.template.BaseLoader
,并传递一个使用template_loader
应用程序设置的实例。
默认情况下,编译的模板会被缓存;要关闭此缓存和重新加载模板,以便始终可以看到对底层文件的更改,请使用应用程序设置compiled_template_cache = False
或debug = True
。
模板语法
Tornado模板只是使用Python控制语句和内嵌表达式标记的HTML(或任何其他基于文本的格式)。
<html>
<head>
<title>{{ title }}</title>
</head>
<body>
<ul>
{% for item in items %}
<li>{{ escape(item) }}</li>
{% end %}
</ul>
</body>
</html>
如果你将这个模板保存为“template.html”并将它放入python文件所在的目录中,你可以使用下面的代码渲染这个模板:
class MainHandler(tornado.web.RequestHandler):
def get(self):
items = ["Item 1", "Item 2", "Item 3"]
self.render("template.html", title="My title", items=items)
Tornado模板支持控制语句和表达式。 控制语句由{%
和%}
包围,例如{% if len(items) > 2 %}
。 表达式由{{
和}}
包围,例如{{items [0]}}
。
控制语句或多或少地映射到Python语句。我们支持if
,for
,while
和try
,所有这些都以{% end% }
结束。我们还使用extends
和block
语句支持模板继承(template inheritance),这些语句在tornado.template
的文档中有详细描述。
表达式可以是包括函数调用的任何Python表达式。模板代码在包含以下对象和函数的命名空间中执行(请注意,此列表仅适用于使用RequestHandler.render
和render_string
渲染的模板。如果你直接在RequestHandler
之外使用tornado.template
模块,下面的许多项都将无法使用)。
escape
: tornado.escape.xhtml_escapexhtml_escape
: tornado.escape.xhtml_escapeurl_escape
: tornado.escape.url_escapejson_encode
: tornado.escape.json_encodesqueeze
: tornado.escape.squeezelinkify
: tornado.escape.linkifydatetime
: the Python datetime modulehandler
: the current RequestHandler objectrequest
: handler.requestcurrent_user
: handler.current_userlocale
: handler.locale_
: handler.locale.translatestatic_url
: handler.static_urlxsrf_form_html
: handler.xsrf_form_htmlreverse_url
: Application.reverse_url- 所有来自
ui_methods
和ui_modules``````Application
的设置项 - 所有传递给
render
或render_string
的关键词
在构建实际应用程序时,你将可能会使用到Tornado模板的所有功能,尤其是模板继承。 阅读tornado.template
部分中有关这些功能的所有内容(包括UIDodules
在内的某些在tornado.web
模块中实现的功能)
实际上Tornado模板在后台将直接转换为Python语言。你在模板中包含的表达式将逐字复制到表示模板的Python函数中。 我们不会试图阻止模板语言中的任何内容;我们创建模板时就为了提供比其他相对严格的模板系统中所缺少的灵活性。因此,如果在模板表达式中编写不受控制的内容,则在Python中执行模板时将会出现不可预知的错误。
默认情况下,使用tornado.escape.xhtml_escape
函数对所有模板输出进行转义。 可以通过将autoescape = None
传递给Application
或tornado.template.Loader
构造函数,可以进行全局的转义开关设置,也可以使用{% autoescape None %}
指令的模板文件或在单个表达式中使用{% raw ...%}
替换{{...}}
达到关闭转义的目的。另外,在所有位置中都可以使用转义函数的名称代替None
。
请注意,虽然Tornado的自动转义有助于避免XSS漏洞,但不能保证在所有的情况下都有用。出现在某些位置的表达式(例如Javascript或CSS)可能需要额外的转义。此外,必须注意始终在可能包含不可信内容的HTML属性中使用双引号和xhtml_escape,或者必须为属性使用单独的转义函数(可在http://wonko.com/post/html-escaping中查看示例)。
国际化
当前用户的区域设置(无论它们是否登录)始终在请求处理程序中以self.locale
的形式提供,在模板中始终以locale
的形式提供。
语言环境的名称(例如en_US
)储存在locale.name
中,您可以使用Locale.translate
方法翻译字符串。模板还有_()
这样可用于字符串转换的全局函数调用。 translate函数有两种形式:
_("Translate this string")
它根据当前语言环境直接翻译字符串,并且:
_("A person liked this", "%(num)d people liked this",
len(people)) % {"num": len(people)}
它根据第三个参数的值翻译一个可以是单数或复数的字符串。在上面的示例中,如果len(people)
为1,则将返回第一个字符串的翻译,否则将返回第二个字符串的翻译。
下面是一个恰当的国际化模板:
<html>
<head>
<title>FriendFeed - {{ _("Sign in") }}</title>
</head>
<body>
<form action="{{ request.path }}" method="post">
<div>{{ _("Username") }} <input type="text" name="username"/></div>
<div>{{ _("Password") }} <input type="password" name="password"/></div>
<div><input type="submit" value="{{ _("Sign in") }}"/></div>
{% module xsrf_form_html() %}
</form>
</body>
</html>
默认情况下,我们使用用户浏览器发送的Accept-Language
标头检测用户的语言环境。如果我们找不到合适的Accept-Language
值,我们选择en_US
。
如果你允许用户将其区域设置设置为首选项,则可以通过重写RequestHandler.get_user_locale
来替换此默认区域设置选择:
class BaseHandler(tornado.web.RequestHandler):
def get_current_user(self):
user_id = self.get_secure_cookie("user")
if not user_id: return None
return self.backend.get_user_by_id(user_id)
def get_user_locale(self):
if "locale" not in self.current_user.prefs:
# Use the Accept-Language header
return None
return self.current_user.prefs["locale"]
如果get_user_locale
返回None
,我们将返回Accept-Language
标头。
tornado.locale
模块支持以两种格式加载翻译:gettext
及其相关工具使用的.mo
格式,还有简单的.csv
格式。 应用程序通常会在启动时调用tornado.locale.load_translations
或tornado.locale.load_gettext_translations
; 有关支持的格式的详细信息,请参阅这些方法。
您可以使用tornado.locale.get_supported_locales()
获取应用程序中所支持的语言环境列表。 将在支持的语言环境列表中选择最接近的匹配项作为用户的语言环境。例如,如果用户的语言环境是es_GT
,并且支持es
语言环境,那么在该请求中,self.locale
的值就是es
。如果找不到接近的匹配项,我们会切换回en_US
。
UI模块
Tornado支持UI模块(UI modules),这将使你的应用可以更轻松地支持标准化、可复用的UI组件。 UI模块就像用渲染页面组件的特殊函数调用一样,它们可以与自己的CSS和JavaScript打包在一起。
例如,如果你想要自己写一个博客,并且希望在博客主页和每个博客条目页面上都显示博客条目,则可以创建一个Entry
模块以在所有页面上渲染它们。首先,为UI模块创建一个Python模块,例如uimodules.py
:
class Entry(tornado.web.UIModule):
def render(self, entry, show_comments=False):
return self.render_string(
"module-entry.html", entry=entry, show_comments=show_comments)
在应用程序中通过ui_modules
设置告诉Tornado使用uimodules.py
:
from . import uimodules
class HomeHandler(tornado.web.RequestHandler):
def get(self):
entries = self.db.query("SELECT * FROM entries ORDER BY date DESC")
self.render("home.html", entries=entries)
class EntryHandler(tornado.web.RequestHandler):
def get(self, entry_id):
entry = self.db.get("SELECT * FROM entries WHERE id = %s", entry_id)
if not entry: raise tornado.web.HTTPError(404)
self.render("entry.html", entry=entry)
settings = {
"ui_modules": uimodules,
}
application = tornado.web.Application([
(r"/", HomeHandler),
(r"/entry/([0-9]+)", EntryHandler),
], **settings)
在模板中,你可以使用{% module %}
语句调用模块。例如,你可以从home.html
调用Entry
模块:
{% for entry in entries %}
{% module Entry(entry) %}
{% end %}
以及从entry.html
中调用:
{% module Entry(entry, show_comments=True) %}
模块可以通过重写embedded_css
,embedded_javascript
,javascript_files
或css_files
方法来包含自定义CSS和JavaScript函数:
class Entry(tornado.web.UIModule):
def embedded_css(self):
return ".entry { margin-bottom: 1em; }"
def render(self, entry, show_comments=False):
return self.render_string(
"module-entry.html", show_comments=show_comments)
无论在页面上使用模块多少次,CSS和JavaScript模块都将包含在内。 CSS始终包含在页面的<head>
中,并且JavaScript总是包含在页面末尾的</body>
标记之前。
当不需要额外的Python代码时,模板文件本身可以用作模块。例如,可以重写前面的示例以将以下内容放在module-entry.html
中:
{{ set_resources(embedded_css=".entry { margin-bottom: 1em; }") }}
<!-- more template html... -->
将使用下面命令调用重写后的模板模块:
{% module Template("module-entry.html", show_comments=True) %}
set_resources
函数仅在通过{% module Template(...) %}
调用的模板中可用。与{% include ... %}
指令不同,模板模块与其包含模板具有不同的命名空间——它们只能
看到全局模板命名空间和它们自己的关键字参数。
认证与安全
cookies与安全cookies
你可以使用set_cookie
方法在用户的浏览器中设置cookie:
class MainHandler(tornado.web.RequestHandler):
def get(self):
if not self.get_cookie("mycookie"):
self.set_cookie("mycookie", "myvalue")
self.write("Your cookie was not set yet!")
else:
self.write("Your cookie was set!")
Cookie并不安全,客户端可以轻松修改。如果你需要设置Cookie,例如,识别当前登录的用户,则需要对cookie签名以防止伪造。
Tornado支持使用set_secure_cookie
和get_secure_cookie
方法签名的cookie。 要使用这些方法,您需要在创建应用程序时指定名为cookie_secret
的密钥。
您可以将设置作为关键字参数传递给应用程序:
application = tornado.web.Application([
(r"/", MainHandler),
], cookie_secret="__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__")
除了时间戳和HMAC签名之外,签名cookie还包含cookie的编码值。如果cookie是旧的或签名不匹配,get_secure_cookie
将返回None,就像没有设置cookie一样。
以上示例的安全版本:
class MainHandler(tornado.web.RequestHandler):
def get(self):
if not self.get_secure_cookie("mycookie"):
self.set_secure_cookie("mycookie", "myvalue")
self.write("Your cookie was not set yet!")
else:
self.write("Your cookie was set!")
Tornado的安全cookie确保完整性但并未对cookie密。也就是说,cookie虽然不能被修改,但客户端可以看到里面的内容。 cookie_secret是一个对称密钥,必须保密——任何获得此密钥值的人都可以生成自己的签名cookie。
默认情况下,Tornado的安全cookie将在30天后过期。要更改此设置,请使用set_secure_cookie
的expires_days
关键字参数和get_secure_cookie
的max_age_days
参数。 这两个值是分开传递的,这样设计的原因是,比如你可以对于大多数用途,设置有一个有效期为30天的cookie,但对于某些敏感操作(例如更改帐单信息),在读取cookie时使用较小的max_age_days
。
Tornado还支持多个签名密钥以启用签名密钥轮换。cookie_secret
必须是一个以一个整数类型的版本号作为键值,并以相应的密钥作为值的字典。需要注意的是,只能使用应用中通过key_version
设置的那个版本的密钥进行签名,但可以使用字典中所有其他密钥进行cookie签名的验证。 要实现cookie更新,可以通过get_secure_cookie_key_version
查询当前的签名密钥版本。
用户认证
当前经过身份验证的用户可以在每个请求处理程序中的self.current_user
获取到,在每个模板中则从current_user
中获取。 默认情况下,current_user
为None
。
要在应用程序中实现用户身份验证,你需要重写请求处理程序中的get_current_user()
方法,以根据cookie的值确定当前用户。这是一个只需要验证cookie中保存的用户昵称就可以登录应用的示例:
class BaseHandler(tornado.web.RequestHandler):
def get_current_user(self):
return self.get_secure_cookie("user")
class MainHandler(BaseHandler):
def get(self):
if not self.current_user:
self.redirect("/login")
return
name = tornado.escape.xhtml_escape(self.current_user)
self.write("Hello, " + name)
class LoginHandler(BaseHandler):
def get(self):
self.write('<html><body><form action="/login" method="post">'
'Name: <input type="text" name="name">'
'<input type="submit" value="Sign in">'
'</form></body></html>')
def post(self):
self.set_secure_cookie("user", self.get_argument("name"))
self.redirect("/")
application = tornado.web.Application([
(r"/", MainHandler),
(r"/login", LoginHandler),
], cookie_secret="__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__")
你可以要求用户使用Python装饰器tornado.web.authenticated
登录。 如果请求转到使用此装饰器的方法,并且用户未登录,则会将其重定向到login_url(另一个应用程序设置)。
上面的例子可以这样重写:
class MainHandler(BaseHandler):
@tornado.web.authenticated
def get(self):
name = tornado.escape.xhtml_escape(self.current_user)
self.write("Hello, " + name)
settings = {
"cookie_secret": "__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__",
"login_url": "/login",
}
application = tornado.web.Application([
(r"/", MainHandler),
(r"/login", LoginHandler),
], **settings)
如果使用authenticated
装饰器装饰的post()
方法,并且用户未登录,则服务器将发送403响应。 @authenticated
装饰器只是简写,如果不是self.current_user:self.redirect()
,可能不适合非基于浏览器的登录方案。
查看Tornado Blog示例应用程序,获取使用身份验证的完整示例(并将用户数据存储在MySQL数据库中)。
第三方认证
tornado.auth
模块为网络上许多最受欢迎的网站实施身份验证和授权协议,包括Google / Gmail,Facebook,Twitter和FriendFeed。该模块包括通过这些站点记录用户的方法,以及在适用的情况下授权访问服务的方法,以便你下载用户的地址簿或代表他们发布Twitter消息。
以下是使用Google进行身份验证的示例处理程序,将Google凭据保存在Cookie中以供日后访问:
class GoogleOAuth2LoginHandler(tornado.web.RequestHandler,
tornado.auth.GoogleOAuth2Mixin):
async def get(self):
if self.get_argument('code', False):
user = await self.get_authenticated_user(
redirect_uri='http://your.site.com/auth/google',
code=self.get_argument('code'))
# Save the user with e.g. set_secure_cookie
else:
await self.authorize_redirect(
redirect_uri='http://your.site.com/auth/google',
client_id=self.settings['google_oauth']['key'],
scope=['profile', 'email'],
response_type='code',
extra_params={'approval_prompt': 'auto'})
有关更多详细信息,请参阅tornado.auth
模块文档。
跨站点请求伪造(CSRF)保护
跨站点请求伪造(XSRF)是个人Web应用程序的常见问题。有关XSRF如何工作的更多信息,请参阅Wikipedia文章。
普遍接受的防止XSRF的解决方案是为每个用户提供不可预测的值,并将该值作为附加参数包含在网站上的每个表单提交中。 如果cookie和表单提交中的值不匹配,则该请求可能是伪造的。
Tornado内置XSRF保护。 要在你的站点中使用的话需要设置xsrf_cookies
:
settings = {
"cookie_secret": "__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__",
"login_url": "/login",
"xsrf_cookies": True,
}
application = tornado.web.Application([
(r"/", MainHandler),
(r"/login", LoginHandler),
], **settings)
如果设置了xsrf_cookies
,则Tornado Web应用程序将为所有用户设置_xsrf cookie
,并拒绝所有不包含正确_xsrf
值的POST
,PUT
和DELETE
请求。
如果启用此设置,则通过POST提交的所有表单都要包含对应字段。你可以使用所有模板中提供的特殊UIModulexsrf_form_html()
来执行此操作:
<form action="/new_message" method="post">
{% module xsrf_form_html() %}
<input type="text" name="message"/>
<input type="submit" value="Post"/>
</form>
如果您提交AJAX POST
请求,则还需要修改JavaScript以在每个请求中包含_xsrf
值。 这是我们在FriendFeed中用于AJAX POST
请求的jQuery函数,它自动将_xsrf
值添加到所有请求:
function getCookie(name) {
var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
return r ? r[1] : undefined;
}
jQuery.postJSON = function(url, args, callback) {
args._xsrf = getCookie("_xsrf");
$.ajax({url: url, data: $.param(args), dataType: "text", type: "POST",
success: function(response) {
callback(eval("(" + response + ")"));
}});
};
对于PUT
和DELETE
请求(以及不使用表单编码参数的POST
请求),XSRF令牌也可以通过名为X-XSRFToken
的HTTP头传递。 XSRF cookie通常在使用xsrf_form_html
时设置,但在不使用任何常规表单的纯Javascript应用程序中,你可能需要手动获取self.xsrf_token
(只需读取属性就足以将cookie设置为函数副作用)。
如果需要基于每个处理程序自定义XSRF行为,则可以重写RequestHandler.check_xsrf_cookie()
。 例如,如果你的API的身份验证不使用cookie,你可能希望通过使用check_xsrf_cookie()
不执行任何操作来禁用XSRF保护。但是,如果同时支持cookie和非基于cookie的身份验证,则必须在使用cookie对当前请求进行身份验证时使用XSRF保护。
DNS重新绑定攻击(DNS Rebinding)
DNS重新绑定是一种可以绕过同源策略并允许外部站点访问专用网络上的资源的攻击。 此攻击包含一个TTL值特别小的DNS名称,该名称在返回由攻击者控制的IP地址和受害者控制的IP地址之间交替(通常是可猜测的私有IP地址,例如127.0.0.1或192.168.1.1)
使用TLS的应用程序不容易受到此攻击(因为浏览器将显示警告并阻止自动访问,因为被DNS被修改后访问的站点与真实目标站点的证书不匹配)。
无法使用TLS并依赖网络级访问控制的应用程序(例如,假设127.0.0.1上的服务器只能由本地计算机访问)应通过验证Host HTTP标头来防止DNS重新绑定。这意味着将限制主机名模式传递给HostMatches路由器或Application.add_handlers
的第一个参数:
# BAD: uses a default host pattern of r'.*'
app = Application([('/foo', FooHandler)])
# GOOD: only matches localhost or its ip address.
app = Application()
app.add_handlers(r'(localhost|127\.0\.0\.1)',
[('/foo', FooHandler)])
# GOOD: same as previous example using tornado.routing.
app = Application([
(HostMatches(r'(localhost|127\.0\.0\.1)'),
[('/foo', FooHandler)]),
])
此外,Application
和DefaultHostMatches
路由器的default_host
参数不得在可能易受DNS重新绑定攻击的应用程序中使用,因为它与通配符主机模式具有类似的效果。
运行与部署
由于Tornado提供自己的HTTPServer,因此运行和部署它与其他Python Web框架略有不同。 你可以编写一个启动服务器的main()
函数,而不是配置WSGI容器来查找应用程序:
def main():
app = make_app()
app.listen(8888)
IOLoop.current().start()
if __name__ == '__main__':
main()
配置操作系统或进程管理器来运行此程序以启动服务器。请注意,可能需要增加每个进程的打开文件数(以避免“打开太多文件”-Error)。要提高此限制(例如将其设置为50000),
你可以使用资源控制命令,修改/etc/security/limits.conf
或在supervisord配置中设置minfds
。
进程和端口
由于Python GIL(Global Interpreter Lock 全局解释器锁),需要时可以运行多个Python进程以充分利用多核机器性能。通常,每个CPU最好运行一个进程。
Tornado包含一个内置的多进程模式,可以同时启动多个进程。 这需要对标准主要功能稍作改动:
def main():
app = make_app()
server = tornado.httpserver.HTTPServer(app)
server.bind(8888)
server.start(0) # forks one process per cpu
IOLoop.current().start()
尽管有一些限制,但这是启动多个进程并让它们共享同一端口的最简单方法。首先,每个子进程都有自己的IOLoop,因此在fork之前没有任何东西触及(甚至间接)全局IOLoop实例是很重要的。 其次,在此模型中很难进行不停机更新。最后,由于所有进程共享同一个端口,因此很难对它们进行独立的监控。
对于更复杂的部署,建议单独启动进程,并让每个进程侦听不同的端口。supervisord的“进程组”功能是实现此功能的一种好方法。当每个进程使用不同的端口时,通常需要外部负载均衡器(如HAProxy或nginx)向外部访问者提供统一的地址。
运行于负载均衡器之后
在像nginx这样的负载均衡器后面运行时,建议将xheaders = True
传递给HTTPServer
构造函数。这将告诉Tornado使用像X-Real-IP
这样的标头来获取用户的IP地址,而不是将所有流量识别为负载均衡服务器的IP地址。
这是一个准系统nginx配置文件,其结构类似于我们在FriendFeed上使用的配置文件。它假定nginx和Tornado服务器在同一台机器上运行,并且四个Tornado服务器在端口8000 - 8003上运行:
user nginx;
worker_processes 1;
error_log /var/log/nginx/error.log;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
use epoll;
}
http {
# Enumerate all the Tornado servers here
upstream frontends {
server 127.0.0.1:8000;
server 127.0.0.1:8001;
server 127.0.0.1:8002;
server 127.0.0.1:8003;
}
include /etc/nginx/mime.types;
default_type application/octet-stream;
access_log /var/log/nginx/access.log;
keepalive_timeout 65;
proxy_read_timeout 200;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
gzip on;
gzip_min_length 1000;
gzip_proxied any;
gzip_types text/plain text/html text/css text/xml
application/x-javascript application/xml
application/atom+xml text/javascript;
# Only retry if there was a communication error, not a timeout
# on the Tornado server (to avoid propagating "queries of death"
# to all frontends)
proxy_next_upstream error;
server {
listen 80;
# Allow file uploads
client_max_body_size 50M;
location ^~ /static/ {
root /var/www;
if ($query_string) {
expires max;
}
}
location = /favicon.ico {
rewrite (.*) /static/favicon.ico;
}
location = /robots.txt {
rewrite (.*) /static/robots.txt;
}
location / {
proxy_pass_header Server;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Scheme $scheme;
proxy_pass http://frontends;
}
}
}
静态文件和主动文件缓存
你可以通过在应用程序中指定static_path
设置来提供Tornado中的静态文件:
settings = {
"static_path": os.path.join(os.path.dirname(__file__), "static"),
"cookie_secret": "__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__",
"login_url": "/login",
"xsrf_cookies": True,
}
application = tornado.web.Application([
(r"/", MainHandler),
(r"/login", LoginHandler),
(r"/(apple-touch-icon\.png)", tornado.web.StaticFileHandler,
dict(path=settings['static_path'])),
], **settings)
进行上面的设置后,所有以/static/
开头的请求都将自动从静态目录中查找对应的文件,比如访问http://localhost:8888/static/foo.png
,返回的就是指定静态目录中的foo.png。我们还会自动从静态目录中提供/robots.txt
和/favicon.ico
(即使它们不以/static/
前缀开头)。
在上面的设置中,我们已经明确地使用StaticFileHandler
将Tornado配置为从根路径即可访问到apple-touch-icon.png
,尽管实际上该文件存放在静态文件目录中。 (为了告诉StaticFileHandler
所请求的文件名,正则表达式中的匹配组是必需的;请记住,匹配组将作为方法参数传递给处理程序。)你可以执行相同的操作来访问站点根目录的sitemap.xml
。 当然,您也可以通过在HTML中使用相应的<link />
标记来避免伪造从根目录访问apple-touch-icon.png
。
为了提高性能,浏览器通常会主动地缓存静态资源,因此浏览器不会发送不必要的If-Modified-Since
或Etag
请求,因为这些可能阻止页面的渲染。TornadoTornado支持这种开箱即用的静态内容版本控(static content versioning)。。
要使用此功能,请在模板中使用static_url
方法,而不是直接在HTML中键入静态文件的URL:
<html>
<head>
<title>FriendFeed - {{ _("Home") }}</title>
</head>
<body>
<div><img src="{{ static_url("images/logo.png") }}"/></div>
</body>
</html>
static_url()
函数将该相对路径转换为类似于/static/images/logo.png?v=aae54
的URI。v
参数是logo.png中内容的哈希值,它的存在使得Tornado服务器向用户的浏览器发送缓存头,这将使浏览器无限期地缓存内容。
v
参数是由文件内容决定的,如果更新文件并重新启动服务器,它将开始发送新的v
值,用户的浏览器将自动获取新文件。 如果文件的内容没有改变,浏览器将继续使用本地缓存的副本,而无需检查服务器上的更新,从而显着提高了渲染性能。
在生产环境中,你可能希望使用像nginx这样静态文件性能更好的服务器提供静态文件。你几乎可以配置任何Web服务器以识别static_url()
使用的版本标记,并设置相应的缓存头。
以下是我们在FriendFeed中使用的相对应的nginx配置:
location /static/ {
root /var/friendfeed/static;
if ($query_string) {
expires max;
}
}
调试模式和自动重载
如果将debug = True
传递给Application
构造函数,则应用程序将以调试/开发模式运行。 在此模式下,将启用在开发时为方便起见的若干功能(每个功能也可作为单独的标志使用;如果两者都指定,则单个标志优先):
autoreload = True
:应用程序将监视其源文件的更改,并在发生任何更改时重新加载。 这减少了在开发过程中手动重启服务器的需要。但是,某些故障(例如导入时的语法错误)仍然会以调试模式目前无法恢复的方式使服务器宕机compiled_template_cache = False
:不缓存模板。static_hash_cache = False
:静态文件哈希值(由static_url
函数使用)不会被缓存serve_traceback = True
:当未捕获RequestHandler
中的异常时,将生成包含堆栈路径的错误页面。
自动重载模式与HTTPServer的多进程模式不兼容。如果使用自动重载模式,则不得向HTTPServer.start
提供除1以外的参数(或调用tornado.process.fork_processes
)。
调试模式的自动重载功能可作为tornado.autoreload
中的独立模块使用。 这两者可以结合使用,可以提高程序鲁棒性,能够更容易发现语法错误:在应用程序中设置autoreload = True
以检测运行时的更改,并使用python -m tornado.autoreload myserver.py
启动服务器以捕获任何语法错误或其他启动时的错误。
重新加载会丢失所有Python解释器的命令行参数(例如-u),因为它使用sys.executable和sys.argv重新执行Python。 此外,修改这些变量将导致重新加载出错。
在某些平台(包括Windows和10.6之前的Mac OSX)上,该过程无法“就地”更新,因此当检测到代码更改时,旧服务器退出并启动新服务器。 众所周知,这会混淆一些IDE。
WSGI和Google App Engine
Tornado通常并不使用WSGI容器,而是独立运行。 但是,在某些环境(例如Google App Engine)中,只允许WSGI,并且应用程序无法运行自己的服务器。 在这种情况下,Tornado支持有限的操作模式,该模式不支持异步操作,但允许在仅WSGI环境中使用Tornado功能的子集。 WSGI模式中不允许的功能包括协程,@ asynchronous
装饰器,AsyncHTTPClient
,auth
模块和WebSockets
。
你可以使用tornado.wsgi.WSGIAdapter
将Tornado应用程序转换为WSGI应用程序。 在此示例中,配置WSGI容器以查找应用程序对象:
import tornado.web
import tornado.wsgi
class MainHandler(tornado.web.RequestHandler):
def get(self):
self.write("Hello, world")
tornado_app = tornado.web.Application([
(r"/", MainHandler),
])
application = tornado.wsgi.WSGIAdapter(tornado_app)