Tornado Web Server

Tornado 是一个基于Python的Web服务框架和 异步网络库, 最早开发与 FriendFeed 公司. 通过利用非阻塞网络 I/O, Tornado 可以承载成千上万的活动连接, 完美的实现了 长连接, WebSockets, 和其他对于每一位用户来说需要长连接的程序.

Hello, world

这是一个基于Tornado的简易 “Hello, world” web应用程序:

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

这个例子没有用到任何Tornado的异步特性; 如果有需要请查看这个例子 简易聊天室.

安装

自动安装:

pip install tornado

Tornado 可以在 PyPI 中被找到.而且可以通过 pip 或者 ``easy_install``来安装.注意这样安装Tornado 可能不会包含源代码中的示例程序, 所以你或许会需要一份软件的源代码.

手动安装: 下载 tornado-4.4.dev1.tar.gz.

tar xvzf tornado-release.tar.gz
cd tornado-release
python setup.py build
sudo python setup.py install

Tornado源代码 被托管在的 GitHub.

环境要求: Tornado 4.3 可以运行在 Python 2.7, 和 3.3+ 对于 Python 2, 版本 2.7.9 以上是被 强烈 推荐的由于这些版本提供了SSL. 除了在 pip 或者 setup.py install 中安装的依赖需求包之外, 以下包有可能会被用到:

  • concurrent.futures is the recommended thread pool for use with Tornado and enables the use of ThreadedResolver. It is needed only on Python 2; Python 3 includes this package in the standard library.
  • pycurl is used by the optional tornado.curl_httpclient. Libcurl version 7.19.3.1 or higher is required; version 7.21.1 or higher is recommended.
  • Twisted may be used with the classes in tornado.platform.twisted.
  • pycares is an alternative non-blocking DNS resolver that can be used when threads are not appropriate.
  • Monotime adds support for a monotonic clock, which improves reliability in environments where clock adjustments are frequent. No longer needed in Python 3.3.
  • monotonic adds support for a monotonic clock. Alternative to Monotime. No longer needed in Python 3.3.

平台: Tornado 应该运行在类 Unix 平台, 对于Linux (通过 epoll) 和 BSD (通过 kqueue) 可以获得更好的性能和可扩展性, 但我们仅推荐它们来不熟产品 (虽然 Mac OS X 也是基于 BSD 的,并且也支持 kqueue, 但是它的网络性能十分的差所以 我们只推荐用它来进行开发). Tornado 也可以运行在 Windows 上, 虽然这并不是官方所推荐的, 我们仅仅推荐用它来做开发.

文档

这篇文档同时还有 PDF 和 Epub 格式.

用户手册

介绍

Tornado 是一个基于Python的Web服务框架和 异步网络库, 最早开发与 FriendFeed 公司. 通过利用非阻塞网络 I/O, Tornado 可以承载成千上万的活动连接, 完美的实现了 长连接, WebSockets, 和其他对于每一位用户来说需要长连接的程序.

Tornado 可以被分为以下四个主要部分:

  • Web 框架 (包括用来创建 Web 应用程序的 RequestHandler 类, 还有很多其它支持的类).
  • HTTP 客户端和服务器的实现 (HTTPServerAsyncHTTPClient).
  • 异步网络库 (IOLoopIOStream), 对 HTTP 的实现提供构建模块, 还可以用来实现其他协议.
  • 协程库 (tornado.gen) 让用户通过更直接的方法来实现异步编程, 而不是通过回调的方式.

Tornado web 框架和 HTTP 服务器提供了一整套 WSGI 的方案. 可以让Tornado编写的Web框架运行在一个WSGI容器中 (WSGIAdapter), 或者使用 Tornado HTTP 服务器作为一个WSGI容器 (WSGIContainer), 这两种解决方案都有各自的局限性, 为了充分享受Tornado为您带来的特性,你需要同时使用 Tornado的web框架和HTTP服务器.

异步和非阻塞 I/O

实时的web特性通常需要为每个用户一个大部分时间都处于空闲的长连接. 在传统的同步web服务器中,这意味着需要给每个用户分配一个专用的线程,这样的开销是十分巨大的.

为了减小对于并发连接需要的开销,Tornado使用了一种单线程事件循环的方式. 这意味着所有应用程序代码都应该是异步和非阻塞的,因为在同一时刻只有一个操作是有效的.

异步和非阻塞这两个属于联系十分紧密而且通常交换使用,但是它们并不完全相同

阻塞

一个函数通常在它等待返回值的时候被 阻塞 .一个函数被阻塞可能由于很多原因: 网络I/O,磁盘I/O,互斥锁等等.事实上, 每一个 函数都会被阻塞,只是时间会比较短而已, 当一个函数运行时并且占用CPU(举一个极端的例子来说明为什么CPU阻塞的时间必须考虑在内, 考虑以下密码散列函数像 bcrypt, 这个函数需要占据几百毫秒的CPU时间, 远远超过了通常对于网络和磁盘请求的时间).

一个函数可以在某些方面阻塞而在其他方面不阻塞.举例来说, tornado.httpclient 在默认设置下将阻塞与DNS解析,但是在其它网络请求时不会阻塞 (为了减轻这种影响,可以用 ThreadedResolver 或通过正确配置 libcurl 使用 tornado.curl_httpclient ). 在Tornado的上下文中我们通常讨论网络I/O上下文阻塞,虽然各种阻塞已经被最小化了.

异步

一个 异步 函数在它结束前就已经返回了,而且通常会在程序中触发一些动作然后在后台执行一些任务. (和正常的 同步 函数相比, 同步函数在返回之前做完了所有的事). 这里有几种类型的异步接口:

  • 回调函数
  • 返回一个占位符 (Future, Promise, Deferred)
  • 传送一个队列
  • 回调注册 (例如. POSIX 信号)

不论使用哪一种类型的接口, 依据定义 异步函数与他们的调用者有不同的交互方式; 但没有一种对调用者透明的方式可以将同步函数变成异步函数 (像 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 版本十分复杂, 但是 Futures 是 Tornado 中推荐使用的一种做法, 因为它有两个主要的优势. 错误处理时通过 Future.result 函数可以简单的抛出一个异常 (不同于某些传统的基于回调方式接口的 一对一的错误处理方式), 而且 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) 在 Python 2 中是人为设定的, 因为生成器不允许又返回值. 为了克服这个问题, Tornado 协程抛出了一个叫做 Return 的特殊异常. 协程将会像返回一个值一样处理这个异常.在 Python 3.3+ 中, return response.body 将会达到同样的效果.

协程

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

协程和异步编程的代码一样简单, 而且不用浪费额外的线程, . 它们还可以减少上下文切换 让并发更简单 .

Example:

from tornado import gen

@gen.coroutine
def fetch_coroutine(url):
    http_client = AsyncHTTPClient()
    response = yield http_client.fetch(url)
    # 在 Python 3.3 之前的版本中, 从生成器函数
    # 返回一个值是不允许的,你必须用
    #   raise gen.Return(response.body)
    # 来代替
    return response.body
Python 3.5: asyncawait

Python 3.5 引入了 asyncawait 关键字 (使用了这些关键字的函数通常被叫做 “native coroutines” ). 从 Tornado 4.3 开始, 在协程基础上你可以使用这些来代替 yield. 简单的通过使用 async def foo() 来代替 @gen.coroutine 装饰器, 用 await 来代替 yield. 文档的剩余部分还是使用 yield 来兼容旧版本的 Python, 但是 asyncawait 在可用时将会运行的更快:

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 装饰器), 不是所有的协程都和其它程序兼容.这里有一个 协程运行器 在第一个协程被调用时进行选择, 然后被所有直接调用 await 的协程库共享. Tornado 协程运行器设计时就时多用途且可以接受任何框架的 awaitable 对象. 其它协程运行器可能会有更多的限制(例如, asyncio 协程运行器不能接收其它框架的协程). 由于这个原因, 我们推荐你使用 Tornado 的协程运行器来兼容任何框架的协程. 在 Tornado 协程运行器中调用一个已经用了asyncio协程运行器的协程,只需要用 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 完成, 然后 “解开” Future 将结果像 yield 语句一样返回给生成器. 大多数异步代码从不直接接触到 Future 类, 除非 Future 立即通过异步函数返回给 yield 表达式.

怎样调用协程

协程在一般情况下不抛出异常: 在 Future 被生成时将会把异常报装进来. 这意味着正确的调用协程十分的重要, 否则你可能忽略很多错误:

@gen.coroutine
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)

近乎所有情况中, 任何一个调用协程自身的函数必须时协程, 通过利用关键字 yield 来调用. 当你在覆盖了父类中的方法, 请查阅文档来判断协程是否被支持 ( 文档中应该写到那个方法 “可能是一个协程” 或者 “可能返回一个 Future”):

@gen.coroutine
def good_call():
    # yield will unwrap the Future returned by divide() and raise
    # the exception.
    yield divide(1, 0)

有时你并不想等待一个协程的返回值. 在这种情况下我们推荐你使用 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)
最后, 在程序的最顶层, 如果 `.IOLoop` 没有正在运行, 你可以启动 IOLoop, 运行协程, 然后通过

IOLoop.run_sync 方法来停止 IOLoop. 这通常被用来启动面向批处理程序的 main 函数:

# run_sync() doesn't take arguments, so we must wrap the
# call in a lambda.
IOLoop.current().run_sync(lambda: divide(1, 0))
协程模式
结合 callbacks

为了使用回调来代替 Future 与异步代码进行交互, 讲这个调用报装在 Task 中. 这将会在你生成的 Future 对象中添加一个回调参数:

@gen.coroutine
def call_task():
    # Note that there are no parens on some_function.
    # This will be translated by Task into
    #   some_function(other_args, callback=callback)
    yield gen.Task(some_function, other_args)
调用阻塞函数

在协程中调用阻塞函数的最简单方法时通过使用 ThreadPoolExecutor, 这将返回与协程兼容的 Futures

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]
    # responses is a list of HTTPResponses in the same order

@gen.coroutine
def parallel_fetch_dict(urls):
    responses = yield {url: http_client.fetch(url)
                        for url in urls}
    # responses 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()
循环

因为在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 和通常的协程不同. 相反, 协程中 通过使用 tornado.gen.sleep 可以包含 while True: 循环:

@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 秒运行一次, 其中 Ndo_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 暂停直到有空间来存放.

Queue 从零开始维护了一系列未完成的任务. put 增加计数; task_done 来减少它.

在这个网络爬虫的例子中, 队列开始仅包含 base_url. 当一个 worker 获取一个页面 他会讲链接解析并将其添加到队列中, 然后调用 task_done 来减少计数. 最后, 一个 worker 获取到页面的 URLs 都是之前抓取过的, 队列中没有剩余的工作要做. worker 调用 task_done 将计数减到0 . 主协程中等待 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 子类, 一个 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 被传递到第三个参数中时, 它将作为 初始参数 传递给 RequestHandler.initialize. 最后, URLSpec 可能会有一个名字 这样允许和 RequestHandler.reverse_url 一起使用.

例如, 根 URL / 被映射到 MainHandler 而且 /story/ 形式的后面跟着数字的 URLs 被映射到 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 子类中完成的. 对于一个控制器子类来说主入口点被 get()post() 等等这样的 HTTP 方法来控制着. 每一个控制器可能会定义一个或多个 HTTP 方法. 如上所述, 这些方法将会被匹配到相应 的路由组中并进行参数调用.

在控制器中, 像调用 RequestHandler.render 或者 RequestHandler.write 将会产生一个相应. render() 通过名字作为参数加载一个 Template . write() 将产生一个不使用模版的纯输出; 它接收字符串, 字节序列和字典 (dicts 将会转换成 JSON).

许多在 RequestHandler 中的方法被设计成为能够在子类中覆盖的方法以在整个应用程序中使用. 通常是定义一个 BaseHandler 类来覆盖像 write_errorget_current_user 然后继承时使用你的 BaseHandler 而不是 RequestHandler.

处理输入请求

处理输入请求时可以勇 self.request 来代表当前处理的请求. 详情请查看 HTTPServerRequest 的定义.

通过 HTML 表单形式的数据可以利用 get_query_argumentget_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_argumentsget_body_arguments 而不是它们的单数形式.

通过 self.request.files 可以实现文件上传, 它会映射名字 ( HTML 标签的名字 <input type="file"> 元素) 到每一个文件中. 每一个文件将会生成一个字典 {"filename":..., "content_type":..., "body":...}. files 对象只有再被某些属性报装后才是有效的 (例如. 一个 multipart/form-data 的 Content-Type); 如果没有使用这种方法 原始的文件上传数据将会在 self.request.body 中. 默认上传的文件是缓存在内存当中的; 如果你上传的文件很大, 不适合缓存在内存当中, 详见 stream_request_body 类修饰符.

由于 HTML 的编码形式十分古怪 (例如. 不区分单一参数还是列表参数), Tornado 不会试图去统一这些参数. 特别的, 我们不会解析 JSON 请求的请求体. 应用程序希望使用 JSON 在编码上代替 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 中的方法也可以被覆盖. 每次请求时, 会发生以下过程:

  1. 一个新的 RequestHandler 将会为每一个请求创建
  2. initialize()Application 的初始化配置参数下被调用. initialize 通常只保存成员变量传递的参数; 它将不会产生任何输出或者调用像 send_error 一样的方法.
  3. prepare() 被调用. 这时基类在与子类共享中最有用的一个方法, 不论是否使用了 HTTP 方法 prepare 都将会被调用. prepare 可能会产生输出; 如果她调用了 finish (或者 redirect, 等等), 处理会在这终止.
  4. HTTP方法将会被调用: get(), post(), put(), 等等. 如果 URL 正则表达式中包含匹配组, 它们将被传递当这些方法的参数中.
  5. 当这些请求结束以后, 会调用 on_finish() . 对于同步处理来说调用会在 get() (等) 返回后立即执行; 对于异步处理来说这将会发生在调用 finish() 之后.

所有像这样可以被覆盖的方法都记录在 RequestHandler 的文档中. 其中一些最常用的覆盖方法有:

错误处理

如果一个控制器抛出了异常, Tornado 将会调用 RequestHandler.write_error 来生成一个错误页. tornado.web.HTTPError 可以用来生成一个指定的错误状态码; 其它异常时将会返回 500 .

在 debug 模式中默认的错误页中包含栈调用记录和一行的错误描述信息
(例如. “500: Internal Server Error”). 要生成一个个人定制的错误页, 覆盖

RequestHandler.write_error (可以声明在父类中用来修改所有的控制器).这种方式可以正常的通过像 writerender 一样的方法来处理输出. 如果错误时由于异常引起的, exc_info 将作为关键字参数传递到错误信息中 (注意: 这里无法确保发生的异常就是当时在 sys.exc_info 中的异常, 所以 write_error 必须使用例如像 traceback.format_exception 来代替 traceback.format_exc).

使用通常的处理方式来代替调用 write_error 也是可以的. 利用 set_status, 写入一个应答, 然后返回. 特殊异常 tornado.web.Finish 在简单的返回不可用的情况下可能在抛出时不会调用 write_error 函数.

对于 404 错误, 利用 default_handler_class Application设置. 处理器将会被覆盖 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 为真, 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/\1")),
    ])

不像 RequestHandler.redirect, RedirectHandler 默认使用的持久重定向. 因为路由表是不会改变的, 在运行时它被假定时持久的, 在处理程序中发现重定向的时候, 可能时会改变的跳转结果. 通过 RedirectHandler 定义的一个持久跳转链接, 在 RedirectHandler 初始化参数中添加 permanent=False .

异步处理

Tornado 处理程序默认是同步的: 当 get()/post() 方法返回时, 结果将会被作为应答发送. 当运行的处理程序中所有请求都被阻塞时 , 任何需要长时间运行的处理程序应该被设计成异步的这样它们可以非阻塞的处理这一段程序.详情见 异步和非阻塞 I/O; 这部分主要针对 RequestHandler 子类中的异步技术.

使用异步处理程序的最简单方式是使用 coroutine 修饰符. 这将会允许你通过关键字 yield 生成一个 非阻塞 I/O, 当协程没有相应之前不会有信息被发出. 查看 协程 获取更多信息.

在某些时候, 协程可能不如一些基于回调的方式更方便, 在这些情况下 tornado.web.asynchronous 修饰符可以被取代. 这个修饰符通常不会自动发送应答; 相反请求将会被保持直到有些回调函数调用 RequestHandler.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() 返回时, 请求没有终止. 当 HTTP 客户端最终调用 on_response() 时, 请求依然是打开的, 当最终调用 self.finish() 时客户端的相应才被发出.

For comparison, here is the same example using a coroutine:

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() 来 在客户端结束后关闭链接 (注意查看方法文档中的警告).

模版和 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 模本文件仅仅是一个 HTML (或者其他基于文本的文件格式) 附加 Python 控制语句和内建的表达式:

<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 模版支持 控制语句 (control statements)表达式 (expressions) . 控制语句被 {% and %} 包裹着, 例如., {% if len(items) > 2 %}. 表达式被 {{}} 围绕, 再例如., {{ items[0] }}.

模版中的控制语句多多少少与 Python 中的控制语句相映射. 我们支持 if, for, while, 和 try, 所有这些都包含在 {%  %} 之中. 我们也支持 模板继承 使用 extendsblock 语句, 详见 tornado.template.

表达式可以时任何的 Python 表达式, 包括函数调用. 模版代码可以在以下对象和函数的命名空间中被执行. (注意这个列表可用在 RequestHandler.renderrender_string. 如果你直接在 RequestHandler 外使用 tornado.template 模块, 下面许多别名是不可用的).

当你真正创建一个应用程序时, 你可能会去查看所有 Tornado 模版的特性, 特别时模版继承. 这些内容详见 tornado.template 部分 (某些特性, 包括 UIModulestornado.web 模块中描述)

在引擎下, Tornado 模版被直街翻译成 Python. 在你模版文件中的表达式将会被翻译成 Python 函数来代表原来的模版; 我们不在模版语言中阻止任何东西; 我们创造它的目的时为了提供更灵活的特性, 而不是有严格限制的模版系统. 因此, 如果你在你的模版文件中随意写入了表达式, 你再执行时将会得到相依随机的错误.

默认情况下, 所有模版文件的输出将会被 tornado.escape.xhtml_escape 方法转义. 这个设置可以通过给 Application 传递全局参数 autoescape=None 或者使用 tornado.template.Loader 构造器进行修改, 或者在模版文件中检测到 {% autoescape None %} , 或者简单的将 {{ ... }} 替换成 {% raw ...%} 的表达式. 此外, 可以在设置这些地方的转义函数为 None 已达到相同的效果.

注意, 尽管 Tornado’s 的自动转义在防止 XSS 漏洞上是有帮助的, 但是不能适用于所有的情况. 出现在适当位置的表达式, 例如 Javascript 或者 CSS, 可能需要额外的转义. 此外, 必须要额外注意使用在 HTML 中使用双括号和 xhtml_escape 中包含一些不可信的内容, 或者在属性中使用单独的转义函数 (查看示例. http://wonko.com/post/html-escaping)

国际化

目前用户的位置 (不论用户是否登陆) 在请求处理程序中的 self.locale 和 模版中的 locale 都是可用的. 位置的名字 (例如, en_US) 在 locale.name 中是可用的, 你也可以通过 Locale.translate 方法来翻译字符串. 模版中也有一个全局函数叫做 _() 用来翻译字符串. 翻译函数有两种形式:

_("翻译这段文字")

这将会根据用户的位置直接翻译, 还有:

_("A person liked this", "%(num)d people liked this",
  len(people)) % {"num": len(people)}

可以根据第三个参数的数量来决定单复数形式. 在以上的例子中, 第一个翻译将会在 len(people)1 时被激活, 在其它情况下会激活第二个翻译.

大多是翻译时利用 Python 中的变量占位符 ( 前面例子中的 %(num)d ) 占位符在翻译时可以被替换.

这是一个正确的国际化模版:

<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 来确定语言. 当我们不能找到默认的语言时我们使用 en_US 作为 Accept-Language 的值. 如果你希望用户自己设定自己的位置, 你可以通过修改默认选项 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 模块支持两种格式的翻译: 一种使用 getttext 和有关工具的 .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 组件. UI 模块就像特殊的方法调用一样用来显示页面上的组件, 它们也可以被报装在 CSS 和 JavaScript 中.

例如, 如果你正在实现一个博客, 你想把博客的入口同时放置在主页和每一页的入口, 你可以定义一个 Entry 模块来实现它们. 首先, 创建一个 Python 模块当作一个 UI 模块, 例如 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) %}

模块可以通过覆盖包含定制的 CSS 和 JavaScript 方法 embedded_css, embedded_javascript, javascript_files , 或者 css_files 方法:

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 和 secure cookies

你可以使用 set_cookie 方法在用户的浏览器中设置 cookies:

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

Cookies 是不安全的而且很容易被客户端修改. 如果你通过设置 cookies 来 识别当前登陆的用户, 你需要利用签名来防止 cookies 被伪造. Tornado 利用 set_secure_cookieget_secure_cookie 方法来对 cookies签名. 为了使用这些方法, 你需要在创建应用程序时指定一个叫做 cookie_secret 的密匙. 你可以在应用程序的设置中通过传递参数来注册密匙:

application = tornado.web.Application([
    (r"/", MainHandler),
], cookie_secret="__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__")

对 cookies 签名后就有确定的编码后的值, 还有时间戳和一个 HMAC . 如果 cookes 过期或者签名不匹配, 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 的 secure cookies 保证完整性但不保证保密性. 就是说, cookie 将不会被修改, 但是它会让用户看到. cookie_secret 是一个对称密钥, 所以它必须被保护起来 – 任何一个人得到密钥的值就将会制造一个签名的 cookie.

默认情况下, Tornado 的 secure cookies 将会在 30 天后过期. 如果要修改这个值, 使用 expires_days 关键词参数传递给 set_secure_cookie max_age_days 参数传递给 get_secure_cookie. 这两个值的传递是相互独立的, 你可能会在大多数情况下会使用一个 30 天内合法的密匙, 但是对某些敏感操作 (例如修改账单信息) 你可以使用一个较小的 max_age_days .

Tornado 也支持多个签名的密匙, 这样可以使用密匙轮换. 这样 cookie_secret 必须是一个具有整数作为密匙版本的字典. 当前正在使用的签名密匙版本必须在应用程序中被设置为 key_version 如果一个正确的密匙版本在 cookie 中被设置, 密匙字典中的其它密匙也可以被用来作为 cookie 的签名认证, 为了实现 cookie 的更新, 可以在 get_secure_cookie_key_version 中查询当前的密匙版本.

用户认证

当前通过认证的用户在请求处理器的 self.current_user 当中, 而且还存在于模版中的 current_user. 默认情况下, current_user 的值为 None.

为了在你的应用程序中实现用户认证, 你需要覆盖请求控制器中的 get_current_user() 方法 来确认怎样获取当前登陆的用户, 例如, 从 cookie 的值中获取该信息. 下面这个例子展示了通过用户的昵称来确定用户身份, 值被保存在 cookies 中:

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 装饰器 (decorator) 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)

如果你的 post() 方法被 authenticated 修饰, 而且用户还没有登陆, 这时服务器会产生一个 403 错误. @authenticated 描述符仅仅是精简版的 if not self.current_user: self.redirect() , 而且可能对于非浏览器的登陆者是不适用的.

点击 Tornado Blog example application 来查看一个完整的用户认证程序 (将用户的数据保存在 MySQL 数据库中).

第三方认证

tornado.auth 模块既实现了认证, 而且还支持许多知名网站的认证协议, 这其中包括 Google/Gmail, Facebook, Twitter, 和 FriendFeed. 模块内包含