Tornado官方文档翻译--持续更新
简介
Tornado是一个Python的网络框架和异步网络库,最初由FriendFeed开发。通过使用非阻塞的网络I/O,Tornado可以吞吐数以万计的开放连接,这使它得以完美应用于长轮询和WebSockets网络协议,以及其他需要彼此进行长连接的应用。
Tornado可以被大致分成四个主要部分:
网络框架(包括作为基类的RequestHandler, RequestHandler用来创建网络应用,以及各种支持类)。
执行HTTP协议(HTTPServer和AsyncHTTPClient)的客户端与服务器端。
异步网络库,包括类IOLoop和IOStream,作为HTTP组件的构建模块,也可以被用来执行其他协议。
协程库(tornado.gen),允许异步代码用一种比链式调用更直接的方式来写。
Tornado网络框架和HTTP服务器一起提供了一个除了WSGI以外的其他选择。尽管可以在一个WSGI容器中使用Tornado网络框架,或者Tornado HTTP服务器作为其他WSGI框架的容器,但是这些组合每一个都有其局限,为了利用好Tornado,你需要将Tornado的网络框架和HTTP服务器结合起来使用。
异步和非阻塞I/O
实时的网络特点需要每个用户有一个长连接并且还要有足够的性能,在一个传统的异步网络服务器中,这意味着要为每一个用户分配一个线程,这样做代价是非常昂贵的。
为了减小并发连接的代价,Tornado使用单线程的event loop。这意味着所有应用的代码都应该是异步和非阻塞的,因为一次只能有一个操作是活跃的。
异步和非阻塞两个术语是紧密关联的,它们在使用的时候经常不加区分,但是它们并不是同一个概念。
阻塞
如果程序在return之前需要等待另一个事件发生,那么它就会阻塞。程序阻塞的原因很多:网络I/O,磁盘I/O, 锁等等。事实上,每个函数当它在运行和使用CPU的时候都会阻塞,至少也是一小部分(举一个极端的例子来论证为什么CPU阻塞必须和其他类型的阻塞一样要严肃对待,想一想密码哈希函数比如bcrypt,它故意占用几百毫秒的CPU事件,远超过一个典型的网络或磁盘访问事件)
一个函数可能在一些方面是阻塞的而其他方面是非阻塞的。例如:tornado.httpclient在默认配置下会在DNS解析上阻塞,但是其他网络请求中不会阻塞(可以使用有正确配置libcurl的ThreadedResolver或tornado.curl_httpclient模块减轻这个影响)。
异步
异步函数在它结束之前return,并且在应用中,通常会在触发一些后续操作之前导致某些后台工作发生(这是相对于正常的异步函数来说,正常的异步函数会在return之前做完它们要做的所有事情)。有很多种不同的异步接口:
参数回调(Callback argument)
返回占位符(Future,Promise,Deferred)
送入队列
注册回调(Callback registry)
不管使用何种类型的接口,从定义上来说这些异步函数和他们的调用者之间的交互是不同的;没有任何一种方式可以使一个异步函数用一种相对它的调用者来说透明的方式来实现异步(像gevent这样的系统使用轻量级的线程来提供和异步系统相近的性能,但是它们实际上并没有实现异步)。
例子
下面是一个异步函数的例子
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
def asyncchronous_fetch(url, callback):
http_client = AsyncHTTPClient()
def handle_response(response):
callback(response.body)
http_client.fetch(url, callback=handle_response)
再用Future改写:
from tornado.concurrent import Future
def async_fetch_future(url):
http_client = AsyncHTTPClient()
my_future = Future()
fetch_future = http_client.fetch(url)
fetch_future.add_done_callback(
lambda f: my_future.set_result(f.result())
)
return my_future
原生的Future版本更加复杂,但是尽管如此仍然推荐在Tornado中进行练习Futures,因为它们有两个主要优势。错误处理更加连贯,因为Future.result方法可以简单抛出一个exception(相对于callback-oriented接口中普遍的ad-hoc错误处理来说),并且Futures非常适合用协程。多线程会再此手册的下一部分深入讨论。下面是我们的样例函数的协程版,和最初的异步版本非常相似。
from tornado import gen
@gen.coroutine
def fetch_coroutine(url):
http_client = AsyncHTTPClient()
response = yield http_client.fetch(url)
raise gen.Return(response.body)
声明raise gen.Return(response.body)是python2的一个神器,在这个里面生成器是不允许返回值的。为了克服这个问题,Tornado线程抛出一种特殊的异常称作Return。协程会捕捉到这个异常然后把它当成一个返回值来对待。在python3.3以及更高的版本中,return response.body也能获得同样的结果。
Coroutines
在Tornado中推荐使用协程的方式来写异步代码。协程使用Python的yield关键字来暂停和恢复执行而不是链式回调(类似gevent一样的框架中的合作轻量级线程有时候也被称为协程,但是在Tornado中所有的协程都使用明确的上下文转换,称为异步函数)
协程几乎和异步代码一样简单,但是没有线程的代价。它们还通过减少上下文切换可能发生的位置的数量来使并发更容易推理。
例子:
from tornado import gen
@gen.coroutine
def fetch_coroutine(url):
http_client = AsyncHTTPClient()
response = yield http_client.fetch(url)
# In Python versions prior to 3.3,returing a value from
# a generator is not allowed and you must use
# raise gen.Return(response.body)
# instead
return response.body
Python 3.5: async and await
Python3.5引进了async和await关键字(使用这些关键字的函数也被称为原生协程(native coroutines))。从Tornado4.3开始,你可以使用它们来代替基于yield的协程(查看接下来的段落中它的局限)。很简单地使用async def foo()的函数定义来代替@gen.coroutine装饰器,然后await代替yield。为了兼容低版本的Python此文档的剩余部分仍然使用yield的方式,但是在能够使用的情况下async和await运行速度更快:
async def fetch_coroutine(url):
http_client = AsyncHTTPClient()
response = await http_client.fetch(url)
return response.body
await关键字没有yield功能多。例如,在一个基于yield的协程中,你可以yield一个Futures对象的列表,而在原生协程中则必须用tornado.gen.multi来打包这个列表。这也消除了与concurrent.futures的整合。你可以使用tornado.gen.convert_yielded把任何能用yield起作用的代码转换成await方式:
async def f():
executor = concurrent.futures.ThreadPoolExecutor()
await tornado.gen.convert_yielded(executor.submit(g))
---g是任务函数(译者注)
尽管原生协程并没有很明显的和某个特定框架绑定在一起(即:它们没有使用像tornado.gen.coroutine或asyncio.coroutine一样的装饰器),不是所有的协程都彼此兼容。有一个协程运行程序是被第一个调用的协程选中的,然后被所有通过await调用的协程所共享。Tornado的协程运行程序被设计成多用途,并且接受任何框架的可await的对象;其他协程可能可能会更加局限(例如,asyncio协程运行程序不接受来自其他框架的协程)。因此,在需要结合多个框架的的应用中推荐使用Tornado的协程运行程序。如果想要在一个已经使用asyncio运行程序的协程中调用Tornado的协程运行程序,使用tornado.platform.asyncio.to_asyncio_future适配器即可。
工作原理
包含yield的函数就是一个生成器。所有的生成器都是异步的;当被调用的时候它们会返回一个生成器对象,而不是运行结束。@gen.coroutine装饰器通过yield表达式和生成器沟通,通过返回一个Future对象来和协程调用者沟通。
下面是一个简单版的协程装饰器内部循环:
# Simplified inner loop of tornado.gen.Runner
def run(self):
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对象立即传递给yield表达式以外,大多数异步代码从不会直接触碰到Future类。
怎样调用协程
协程不会用正常的方式来抛出异常:它们抛出的任何异常在它被yield之前都会被困在FUture对象中。这意味着用正确的方式调用协程是非常重要的,否则你可能会忽略一些错误:
@gen.coroutine
def divide(x, y):
return x / y
def bad_call():
# This should raise a ZeroDivisionError, but it won't bacause the coroutine is called incorrectly.
divide(1, 0)
在几乎所有情形下,任何一个调用协程的函数本身必须也是协程,并且在调用中使用yield关键字。当你重写父类中定义的方法时,最好查阅文档看是否允许协程(文档应该说明此方法“是一个协程”或“返回Future对象”):
@gen.coroutine
def good_call():
# yield will unwrap the Future returned by divide() and raise
# the exception
yield 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)的项目的主函数:
# run_sync() doesn't take arguments, so we must wrap the
# call in a lambda.
IOLoop.current().run_sync(lambda: divide(1, 0))
协程模式
回调交互(Interaction with callbacks)
为了和使用回调而不是Future的异步代码交互,要在Task中打包此调用。这将会为你增加回调参数,并返回一个你可以yield的Future对象:
@gen.coroutine
def call_task():
# Note that there are no paterns on some_funxtion.
# This will be translated by Task into
# some_function(other_args, callback=callback)
yield gen.Task(some_function, other_args)
调用阻塞函数(Calling blocking functions)
从协程中调用一个阻塞函数的最简单的方法就是使用ThreadPoolExecutor,返回一个其他协程兼容的Futures对象:
thread_pool = ThreadPoolExecutor()
@gen.coroutine
def call_blocking():
yield thread_pool.submit(blocking_func, args)
并行(Parallelism)
协程装饰器能够识别列表和字典,因为它们的值是Futures对象,并且并行等待所有的这些Futures:
@gen.coroutine
def parallel_fetch(url1, url2):
resp1, resp2 = yield [http_client.fetch(url1),
http_client.frtch(url2)]
@gen.coroutine
def parallel_fetch_many(urls):
reponse = yield [http_client.fetch(url) for url in urls]
# response is a list of HTTPResponse in the same order
@gen.coroutine
def parallel_fetch_dict(urls):
response = yield {url: http_client.fetch(url) for url in urls}
# response is a dict {url: HTTPResponse}
交叉存取
有时保存一个Future对象比立即yield它会更有用,以便你可以在等待之前开始开始另一个操作:
@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()
这种模式用@gen.coroutine是最有用的。如果fetch_next_chunk()使用async def,那么为了开启后台进程,它一定会被当作fetch_future = tornado.gen.convert_yielded(self.fetch_next_chunk())来调用。
循环
对协程来说循环是很复杂的,原因是python中在for或者while循环中对每一次迭代都yield并且捕捉到yield的结果是不可能的。相反,你需要根据不同的结果区分循环条件,如下面的例子:
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()
后台运行
使用协程后PeriodicCallback便不能正常使用。相反,协程可以包含一个while True循环,然后使用tornado.gen.sleep:
@gen.coroutine
def minute_loop():
while True:
yield do_something()
yield 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秒运行一次,使用上面的交叉存储方法:
@gen.coroutine
def minute_loop2():
while True:
nxt = gen.sleep(60) # start the clock.
yield do_something() # Run while the clock is ticking.
yield nxt # Wait for the timer to run out.
Queue例子 - 并发网络爬虫
Tornado的Tornado.queues模块为协程补充了一个生产者和消费者模式,类似Python标准库中的queue模块为线程补充的模型。
一个yield Queue.get的协程直到队列中有元素之前都会暂停。如果队列设置了最大容量,一个yield Queue.put的协程在队列有空间之前都会暂停。
队列中包含了许多未结束的任务,并且队列是从0开始的。put增加数量;task_done减少。
在这个网络爬虫的例子中,队列开始只包含一个base_url。当worker获取到一个页面后,它会分析连接,然后把新的连接放进队列中,然后调用一次task_done来减少数量。最终,worker获取到一个页面,这个页面的URL在之前的页面中已经全部用过了,然后队列中就没有work剩余了。因此worker对task_done的调用会把计数器减少至0.正在等待join的主要协程会取消暂停然后结束。
# !/usr/bin/env python
import time
from datetime import timedelta
try:
from HTMLParser import HTMLParser
from urlparser import urljoin, urldefrag
except ImportError:
from html.parser import HTMLParser
from urllib.parse import urljoin, urldefrag
from tornado import httpclient, gen, ioloop, queues
base_url = 'http://www.tornadoweb.org/en/stable/'
concurrency = 10
@gen.coroutine
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'.
"""
try:
response = yield httpclient.AsyncHTTPClient().fetch(url)
print('fetched %s' %url)
html = response.body if isinstance(response.body, str) \
else response.body.decode()
urls = [urljoin(url, remove_fragment(new_url))
for new_url in get_links(html)]
except Exception as e:
print("Exception: %s %s" % (e, url))
raise gen.Return([])
raise gen.Return(urls)
def remove_fragment(url):
pure_url, frag = urldefrag(url)
return pure_url
def get_links(html):
class URLSeeker(HTMLParser):
def __init__(self):
HTMLPrser.__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
@gen.coroutine
def main():
q = queues.Queue()
start = time.time()
fetching, fetched = set(), set()
@gen.coroutine
def fetch_url():
current_url = yield q.get()
try:
if current_url in fetching:
return
print('fetching %s' % current_url)
fetched.add(current_url)
for new_url in urls:
# Only follow links beneath the base URL
if new_url.startwith(base_url):
yield q.put(new_url)
finally:
q.task_done()
@gen.coroutine
def worker():
while True:
yield fetch_url()
q.put(base_url)
# Start workers, then wait for the worker queue to be empty.
for _ in rage(concurrency):
worker()
yield q.join(timeout=timedalta(seconds=300))
assert fetching == fetched
print('Done in %d seconds, fetched %s URLs.' %(
time.time() - start, len(fetched)))
if __name__ == "__main__":
import logging
logging.basicConfig()
io_loop = ioloop.IOLoop.current()
io_loop.run_sync(main)
一个Tornado网络应用的结构
一个Tornado网络应用通常是由一个或多个RequestHandler的子类组成的,Application将接收到的请求传递给处理器(handler),然后main()函数开启服务。
下面是一个最简单的"hello world"例子:
import tornado.ioloop
improt 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方法使用。
例如,在下面的代码段中,根路径/映射到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(Subclassing RequestHandler)
Tornado网络应用的绝大部分工作都是在RequestHandler的子类中完成的。处理器子类的主要入口是HTTP方法:get(), post() 等等。每一个处理器可能会定义一个或多个这些方法来处理不同的HTTP动作。正如上面所描述的,这些方法将会加上路径中匹配到的参数来被调用。
在一个处理器中,调用一些例如RequestHandler.render或者RequestHandler.write的方法来产生响应。render()方法通过名字加载了Template类,然后加上参数进行提交。write()用来在非模板环境下使用;它接受strings,bytes和dictionaries(字典必须被编码成json格式)。
在RequestHandler中的很多方法都被设计为可在子类中重写,并且可以在整个应用中使用。定义一个BaseHandler,并重写一些方法例如write_error和get_current_user,然后把你的BaseHandler代替RequestHandler作为所有处理器的基类,这种做法是非常常见的。
处理请求输入(Handling request input)
请求处理器可以通过self.request来获取代表当前请求的对象。查看HTTPServerRequest的定义来获取完整的属性列表。
method, uri, path, query(The query portion of uri),
version(HTTP version specified in request,e.g. "HATTP/1.1"), headers, body, remote_ip, protocol, host等等,详细介绍请点击属性列表。
---译者注
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["Content-Type"].startwith("application/json"):
self.json_args = json.loads(self.request.body)
else:
self.json_args = None
重写RequestHandler中的方法
除了get和post方法外,在RequestHandler还有一些其他方法在必要时也可以被子类重写。在每个请求中,丢回发生以下一系列的调用:
1.每个请求中都会新建一个RequestHandler对象
2.如果有从Application配置中获得初始参数的话initialize()函数会被调用,initialize函数应该只保存传递给成员变量的参数;它可能不会产生任何输出或调用类似send_error一样的方法。
3.调用prepare方法。这在一个基类中是最有用的,基类由你的处理器子类所共享,因为不论使用哪种HTTP方法prepare函数都会被调用。prepare可能会产生输出;如果它调用了finish(或者redirect方法等),进程就会在此结束。
4.某一个HTTP方法被调用:get,post,put等等。如果URL正则表达式包含了捕捉分组参数(capturing groups),这些参数也会传递到此方法中。
5.当请求结束时,on_finish会被调用,对异步处理器来说,这一步在get(或其他方法)return后就会立即发生;对异步处理器来说,它发生在调用finish之后。
正如在RequestHandler文档中注释的一样,所有方法都是可以重写的。最常用扽一些可以被重写的方法包括:
write_error -输出html的错误页面。
on_connection_close -当客户端断开连接的时候调用;应用可能会选择检测这种情况然后停止更深层的处理,注意,不能保证一个已关闭的连接也能被及时检测到。
get_current_user -查看User authentication
get_user_locale 为当前用户返回一个locale对象
set_default_headers -可能会被用来在响应中设置另外的响应头(例如一个custom Server响应头)
错误处理
如果处理器抛出一个异常,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 PermanentlyHTTP响应码会被使用,在下面这中情况下是有用的:重定向到一个权威的URL来采用一种相对搜索引擎友好的方式获取页面。
RedirectHandler可以让你在你的Application路由表中直接配置重定向。例如,配置一个静态重定向:
app = tornado.web.Application([
url(r"/app", tornado.web.RedirectHandler,
dict(url="http://itunes.apple.com/my-app-id")),
])
下面的规则把所有以/pictures/开头的请求重定向到前缀是/photos/
app = tornado.web.Application([
url(r"/photos/(.*)", MyPhotoHandler),
url(r"/pictures/(.*)", tornado.web.RedirectHandler,
dict(url=r"/photos/{0}")),
])
不像RequestHandler.redirect,RedirectHandler默认使用永久重定向。这是因为路由表在运行过程中不会改变并且是永久的,尽管在处理器中的重定向很可能是其他可能回改变的逻辑所导致的。如果想发起一个临时重定向,只需要把permanent=False参数加到RedirectHandler的初始化参数中。
异步处理器
Tornado的处理器默认是同步的:当get()/post()方法返回值的时候,请求会被问为结束,然后响应会被返回。因为在一个处理器运行的时候其他所有请求都会阻塞,所以任何长时间运行的处理器都应该做成异步方式以便用一种非阻塞的方式调用它的后续操作。这个话题在 Asynchronous and non-Blocking I/O章节中会重新讲解;这部分是关于异步技术在RequestHandler中的细节的。