Tornado完全解读「译」

Tornado Guide

Posted by decaywood on 2016-01-13
- 错误校对

原文

简介

Tornado是一个基于Python开发的异步网络库以及网络框架,由FriendFeed着手开发。通过非阻塞网络IO,Tornado能同时处理一万个连接。在Long polling, WebSockets,以及其他需要长连接的场合下能够提供理想的性能。

Tornado的结构大致可概括为下面几个部分:

  • 网络框架(包括RequestHandler,RequestHandler可以用来创建Web应用)。
  • 基于HTTP的客户端以及服务端(HTTPServer/AsyncHTTPClient)。
  • 异步网络库(IOLoop/IOStream),用于给Tornado的HTTP组件提供底层服务,也可以用来实现其他协议。
  • 协程库(tornado.gen),可以帮助用户写出更优雅直观的异步代码,而不是一系列看不懂的回调。

Tornado网络框架和HTTP服务端提供了替代WSGI的全栈式解决方案。当然,你也有可能在WSGI容器(WSGIAdapter)中使用Tornado框架,或者将Tornado的HTTP服务端作为容器并在其中使用WSGI框架(WSGIContainer),两种组合方式都有一定的限制。所以,如果你想完全发挥Tornado的优势,就必须同时使用Tornado提供的网络框架以及HTTP服务端。

异步 & 非阻塞IO

实时性的实现需要用户与服务端保持长连接,在传统的同步服务器中,这意味着处理一个用户的连接请求就得使用一个线程,代价无疑是非常高的。

为了最小化并发连接的代价,Tornado使用单线程的Event loop模型。这意味着所有应用代码都必须是异步并且非阻塞的,应为同一时间只可能进行一个操作,否则程序就假死了。

异步和非阻塞是两个联系紧密但却经常被混淆的术语,事实上他们不是一个概念,博客里有一个专门讲解同步/异步,阻塞/非阻塞区别的章节

阻塞

一个函数在执行后,返回值之前都是阻塞的。阻塞有很多中原因:网络IO,磁盘IO,互斥锁等等…事实上,每个函数在运算时都会阻塞,只是时间长短问题而已(某些极端的例子就可以证明为什么CPU运算阻塞会比其他IO阻塞严重许多,比如bcrypt这种密码哈希函数,耗时数百毫秒,比一般的网络/磁盘IO耗时还要久)。

一个函数可以在某些地方阻塞执行,也可以在某些地方非阻塞地执行。例如,在tornado.httpclient的默认配置中,解析DNS时是阻塞的(减轻ThreadedResolver的使用频率),而访问网络时则为非阻塞的。在本文中讨论的阻塞主要指网络IO。

异步

异步函数指的是函数执行后在其结束之前能够立即返回,这种机制一般会调用后台线程执行一些任务,任务完成后触发相应的行为(与一般的同步函数相反,异步函数在返回之前,当前线程可以继续执行其他任务)。以下是一些异步风格的接口:

  • Callback argument
  • Return a placeholder (Future, Promise, Deferred)
  • Deliver to a queue
  • Callback registry (e.g. POSIX signals)

不管是使用哪种接口,由于定义上的不同,对于调用者来说,使用异步函数跟使用同步函数多少会有区别。目前没有合适的办法使同步方法转化为异步方式调用(有些系统例如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 asynchronous_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当中推荐使用,主要有两个优势:首先,在错误处理方面更加合理,Future.result方法可以简单地抛出异常,其次,能够更好地与协程配合使用,协程将在下一个章节深入讲解。下面是协程版本的例子,和同步版本非常相似:

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)这种表达是Python 2(以及3.2)的产物,这两个版本不允许返回值,为了能够返回一个值,Tornado协程抛出一个特殊的异常Return来模拟返回值。协程会捕获这个异常,并把它当做返回值来使用。Python3.3及以上版本则使用类似return response.body这种表达形式了。

协程

Tornado中推荐使用 协程 写异步代码. 协程使用了Python的 yield 关键字代替链式回调来将程序挂起和恢复执行(像在gevent中出现的轻量级线程合作方式有时也被称为协程,但是在Tornado中所有的协程使用明确的上下文切换,并被称为异步函数).

协程就是用户态线程,比内核线程低廉,上下文切换成本低; 单调度器下,访问共享资源无需上锁,用于提高cpu单核的并发能力。缺点是无法利用多核资源,只能开多进程才行,不过现在使用协程的语言都用到了多调度器的架构,单进程下的协程也能用多核了。其最大的意义就是可以用同步方式编写异步代码。

协程的例子:

from tornado import gen

@gen.coroutine
def fetch_coroutine(url):
    http_client = AsyncHTTPClient()
    response = yield http_client.fetch(url)
    # 在Python 3.3之前, 在generator中是不允许有返回值的
    # 必须通过抛出异常来代替.
    # 就像 raise gen.Return(response.body).
    return response.body

Python 3.5:async与await

Python 3.5引入了async与await两个关键字(使用这两个关键字的函数也称作“原生协程”)。Tornado 4.3开始,你可以使用他们代替基于yield关键字的协程。用async def foo()代替@gen.coroutine装饰器,然后用await代替yield即可。由于兼容性考虑,本系列文章任然使用yield方式,不过使用async和await关键字的原生协程运行得更快。下面是一个例子:

async def fetch_coroutine(url):
    http_client = AsyncHTTPClient()
    response = await http_client.fetch(url)
    return response.body

await关键字功能上没有yield强大,例如yield写的协程可以获得一个Futures列表,而原生协程则需要将列表封装到tornado.gen.multi中。当然,你也可以用tornado.gen.convert_yielded来把任何使用yield修饰的代码转换成await修饰的代码。

虽然原生协程没有明显依赖于特定框架(例如它们没有使用装饰器,例如tornado.gen.coroutine或者asyncio.coroutine),故协程间不一定相互兼容。当第一个协程被调用时,协程会选择一个Runner(协程运行器),接下来被await调用的协程都会共享这个Runner。Tornado提供的Runner功能比其他协程运行器更加强大,可以支持大部分框架的awaitable对象。其他协程运行器在这方面限制相对较多(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):
    # 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中的结果并作为yield表达式的结果回传给生成器,再次进行一轮循环(补充:这里之所以进行递归循环是由于用户可能嵌套了多个异步调用)。大部分异步代码不会直接与Future类交互,除非直接将异步函数返回的Future传给yield表达式。

如何调用协程

协程一般不会抛出异常:任何抛出的异常都会被Future捕获,直到它被获取。这里正确调用协程就显得比较重要了,否则错误将被忽略掉。

@gen.coroutine
def divide(x y):
    return x / y

def bad_call():
    # 这里应该抛出一个 ZeroDivisionError 的异常, 但事实上并没有
    # 因为协程的调用方式是错误的.
    divide(1 0)

调用协程的正确做法是调用协程的函数本身也必须为协程,并且使用yield关键字进行调用。当覆盖一个父类方法时,必须查看说明是否支持协程(一般文档会说“此方法为协程”或者“此方法返回一个Future类型”):

@gen.coroutine
def good_call():
    # yield 将会解开 divide() 返回的 Future 并且抛出异常
    yield divide(1 0)

有时你可能想执行一个协程后就不在等待其结果了。 在这种情况下,建议使用IOLoop.spawn_callback,协程将由IOLoop负责接管。如果执行失败了,IOLoop会在日志中把调用栈记录下来:

# IOLoop 将会捕获异常,并且在日志中打印栈记录.
# 注意这不像是一个正常的调用, 因为我们是通过
# IOLoop 调用的这个函数.
IOLoop.current().spawn_callback(divide 1 0)

最后,在程序顶层,如果IOLoop尚未运行, 你可以启动IOLoop,执行协程,然后使用IOLoop.run_sync方法停止IOLoop。这通常被用来启动面向批处理程序的main函数:

# run_sync() 不接收参数,所以我们必须把调用包在lambda函数中.
IOLoop.current().run_sync(lambda: divide(1 0))

协程模式

结合 callback

为了使用回调代替Future与异步代码进行交互, 把调用封装在Task 类中。 这将为你添加一个回调参数并且返回一个可以yield的Future:

@gen.coroutine
def call_task():
    # 注意这里没有传进来some_function.
    # 这里会被Task翻译成
    #   some_function(other_args, callback=callback)
    yield gen.Task(some_function other_args)

调用阻塞函数

从协程调用阻塞函数最简单的方式是使用ThreadPoolExecutor, 它将返回和协程兼容的Future:

thread_pool = ThreadPoolExecutor(4)

@gen.coroutine
def call_blocking():
    yield thread_pool.submit(blocking_func args)

并行

协程装饰器能识别列表或者字典对象中各自的Futures,并且并行的等待这些Futures:

@gen.coroutine
def parallel_fetch(url1 url2):
    resp1 resp2 = yield [http_client.fetch(url1) http_client.fetch(url2)]

@gen.coroutine
def parallel_fetch_many(urls):
    responses = yield [http_client.fetch(url) for url in urls]
    # 响应是和HTTPResponses相同顺序的列表

@gen.coroutine
def parallel_fetch_dict(urls):
    responses = yield {url: http_client.fetch(url) for url in urls}
    # 响应是一个字典 {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()

循环

协程的循环是棘手的,因为在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()

在后台运行

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)   # 开始计时.
        yield do_something()  # 计时后运行.
        yield nxt             # 等待计时结束.

示例 - 一个并发网络爬虫

Tornado的tornado.queues模块实现了异步生产者/消费者模式的协程,类似于由Python标准库的queue模型实现的线程模式。

协程在yield Queue.get时只有在队列中有值的时候才会往下执行。同理,如果队列设置了最大长度,在yield Queue.put时只有队列没满的情况才会往下执行。

一个Queue维护了一系列未完成任务,从0开始计数。put增加计数;task_done减少计数.

这里的网络爬虫的例子,队列开始的时候只包含base_url。当一个worker抓取到一个页面它会解析链接并把它添加到队列中,然后调用task_done减少计数一次。最后, 当一个worker抓取到的页面URL都是之前抓取到过的并且队列中没有任务了.于是worker调用 task_done把计数减到0。等待 Queue.join 的主协程取消暂停并且完成。

import time
from datetime import timedelta

try:
    from HTMLParser import HTMLParser
    from urlparse 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):
            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


@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)
            fetching.add(current_url)
            urls = yield 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):
                    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 work queue to be empty.
    for _ in range(concurrency):
        worker()
    yield q.join(timeout=timedelta(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 web应用的结构

通常一个Tornado web应用由三个部分组成,包括一个或者多个RequestHandler子类,一个可以将收到的请求路由到对应handler的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对象是负责全局配置的,它维护了一张映射请求到handler的路由表。

路由表是一系列URLSpec对象(或元组)组成的列表,列表中每个元素都包含(至少)一个正则表达式和一个处理类。且路由表对顺序敏感,使用第一个匹配成功的规则对应的handler处理请求。如果正则表达式包含捕获组,这些组会被作为路径参数传递给处理函数的HTTP方法。如果一个字典作为URLSpec的第三个参数被传递,它会作为初始参数传递给RequestHandler.initialize。最后,URLSpec可能有一个名字(name),这将允许它被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动作。如上所述,这些方法将被匹配路由规则的捕获组对应的参数调用。

在handler中,可以调用RequestHandler.render或者RequestHandler.write方法产生一个响应。render()通过名字加载一个Template并使用给定的参数渲染它。write()用于简单输出; 它接受字符串,字节,和字典(字典会被编码成JSON)。

RequestHandler中的很多方法被设计成用于在子类中覆盖并在整个应用中使用。常用的方法是定义一个BaseHandler类, 覆盖一些方法,例如RequestHandler.write_errorRequestHandler.get_current_user,接下来应用中所有具体的handler都继承BaseHandler而不是RequestHandler

处理输入请求

请求handler可以使用self.request访问代表当前请求的对象。通过HTTPServerRequest的类定义查看完整的属性列表。

使用HTML表单格式请求的数据会被解析并且可以在一些方法中获取,例如RequestHandler.get_query_argumentRequestHandler.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"))

由于HTLM表单编码不确定一个标签的参数是单一值还是一个列表,RequestHandler有明确的方法来允许应用程序表明是否它期望接收一个列表。对于列表,使用RequestHandler.get_query_argumentsRequestHandler.get_body_arguments而不是它们的单数形式。

通过一个表单上传的文件可以使用self.request.files访问,这个方法将名字(HTML标签<input type=”file”>的名字)映射到一个文件列表.每个文件都是一个字典的形式{“filename”:…, “content_type”:…, “body”:…}。 files对象是当前唯一的如果文件上传是通过一个表单包装(i.e. a multipart/form-data Content-Type); 如果没用这种格式,原生上传的数据可以调用self.request.body使用。默认上传的文件是完全缓存在内存中的; 如果你需要处理占用内存太大的文件可以参考stream_request_body类装饰器。

由于HTML表单编码格式的怪异 (e.g.在单数和复数参数的含糊不清),Tornado不会试图统一表单参数和其他输入类型的参数。特别是,我们不解析JSON请求体。应用程序希望使用JSON代替表单编码可以覆盖RequestHandler.prepare来解析它们的请求:

def prepare(self):
    if self.request.headers["Content-Type"].startswith("application/json"):
        self.json_args = json.loads(self.request.body)
    else:
        self.json_args = None

覆盖RequestHandler的方法

除了get()/post()/等, 在RequestHandler中的某些其他方法也设计成让子类覆盖的形式。每当请求发生时,会执行下面的一系列动作:

  • 在每次请求时生成一个新的RequestHandler对象

  • 调用RequestHandler.initialize()并传入Application配置中的初始化参数。initialize通常应该只保存传递给成员变量的参数; 它不能产生或调用任何输出或方法,例如RequestHandler.send_error

  • 调用RequestHandler.prepare()。这个方法在所有子类中共享,它非常有用,无论是使用哪种HTTP方法,prepare都会被调用prepare可能会产生输出;如果它调用RequestHandler.finish(或者redirect等),处理会在这里结束。

  • 当get(),post(),put(),等中的其中一种方法被调用时,如果URL的正则表达式包含捕获组,它们会被作为参数传递给这个方法。

  • 当请求结束,RequestHandler.on_finish()方法将被调用. 对于同步处理程序会在get()等方法后立即返回; 对于异步处理程序,则会在调用RequestHandler.finish()后返回.

所有可覆盖的方法在RequestHandler的文档中都有说明。其中最常用的一些可覆盖的方法包括:

错误处理

如果一个处理程序抛出一个异常,Tornado会调用RequestHandler.write_error来生成一个错误页tornado.web.HTTPError可以被用来生成一个指定的状态码;所有其他的异常都会返回一个500状态。

默认的错误页面包含一个debug模式下的调用栈和一行错误描述(e.g. “500: Internal Server Error”)。为了创建自定义的错误页面,可以对RequestHandler.write_error进行覆盖(方法可能在一个所有处理程序共享的一个基类里面)。write_error可以通过特定方法产生一些输出,例如RequestHandler.renderRequestHandler.write如果错误是由异常引起的,方法将传入一个exc.info关键字参数(注意这个异常不保证是当前在sys.exc_info中的异常,所以write_error必须使用 e.g. traceback.format_exception代替traceback.format_exc)。

也可以在常规的处理方法中调用 RequestHandlerset_status代替write_error返回一个(自定义)响应来生成一个错误页面。在不方便直接返回的情况下,可以通过抛出特殊的tornado.web.Finish异常实现在调用write_error前结束处理程序。

对于404错误, 使用default_handler_class Application setting。这个处理程序会直接覆盖RequestHandler.prepare方法而不是像get()这样的具体的方法,所以它可以在任何HTTP方法下执行。它应该会产生如上所说的错误页面: 要么raise一个HTTPError(404)要么覆盖write_error, 或者调用self.set_status(404)或者在prepare()中直接生成响应。

重定向

要在Tornado中重定向请求,这里提供两种主流的方法:使用RequestHandler.redirectRedirectHandler

你可以使用RedirectHandler中的self.redirect()方法把用户重定向到其他地方。它有一个可选参数permanent,可以使用它来表明这个重定向为永久重定向。permanent的默认值是False,这会生成一个302 FoundHTTP响应状态码,适合类似在用户的POST请求成功后的重定向。如果permanent为true,则会生成301 Moved PermanentlyHTTP响应状态码,这种策略适用于重定向页面到高权重的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/\1")),
    ])

不像RequestHandler.redirectRedirectHandler默认使用永久重定向。这是因为路由表在运行时不会改变,并被定义为永久的。虽然在handler中的重定向可能是由其他逻辑修改的。使用RedirectHandler发送临时重定向,需要添加permanent=False到RedirectHandler的初始化参数.

异步handler

Tornado的handler默认为同步的:当get()/post()方法返回时, 请求随即结束并且返回响应。由于当一个handler正在运行的时候其他所有请求都会被阻塞,所以任何需要长时间运行的handler都应该以异步方式运行,这样的话,即便handler中的处理方法很耗时,也不会阻塞其他handler的执行。这个话题在Linux下的五种IO模型中有更详细的讨论。

使用coroutine装饰器是做异步最简单的方式。这允许你使用yield关键字执行非阻塞IO,并且直到协程返回才发送响应。查看协程了解更多细节。

在某些情况下,协程不如回调方便,在这种情况下tornado.web.asynchronous装饰器可以用来代替协程。当使用这个装饰器的时候,响应不会自动发送;在callback调用RequestHandler.finish之前,请求将会一直打开。故应用程序需要确保finish方法被调用,否则客户端浏览器会被挂起。

这里是一个使用Tornado内置的AsyncHTTPClient调用FriendFeed API的例子:

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()

当get()返回后,此时请求还没有完成。事实上最终调用on_response()时,这个请求仍然是打开的,直到调用self.finish(),响应才最终刷新到客户端。

为了方便对比, 这里有一个使用协程的相同的例子:

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")

更多高级异步的示例, 请看chat example application,这个例子实现了一个使用长轮询(long polling)的AJAX聊天室.如果你使用长轮询,可以覆盖on_connection_close()来在客户端关闭连接之后进行资源释放(注意查询方法的文档来了解如何使用).