Tornado Web 应用程序结构

Tornado Web 应用程序通常包含一个或多个 RequestHandler 子类,一个 Application 对象,该对象将传入的请求路由到处理程序,以及一个用于启动服务器的 main() 函数。

一个最小的“hello world”示例看起来像这样

import asyncio
import tornado

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("Hello, world")

def make_app():
    return tornado.web.Application([
        (r"/", MainHandler),
    ])

async def main():
    app = make_app()
    app.listen(8888)
    shutdown_event = asyncio.Event()
    await shutdown_event.wait()

if __name__ == "__main__":
    asyncio.run(main())

主协程

从 Tornado 6.2 和 Python 3.10 开始,启动 Tornado 应用程序的推荐模式是创建一个 main 协程,并使用 asyncio.run 运行它。(在旧版本中,通常在普通函数中执行初始化,然后使用 IOLoop.current().start() 启动事件循环。但是,此模式从 Python 3.10 开始产生弃用警告,并且将在 Python 的未来某个版本中中断。)

main 函数返回时,程序退出,因此对于大多数 Web 服务器,main 应该永远运行。等待 asyncio.Event(其 set() 方法永远不会被调用)是一种方便的方法,可以使异步函数永远运行。(如果您希望 main 作为优雅关闭过程的一部分提前退出,您可以调用 shutdown_event.set() 使其退出)。

Application 对象

Application 对象负责全局配置,包括将请求映射到处理程序的路由表。

路由表是一个 URLSpec 对象(或元组)列表,每个对象(至少)包含一个正则表达式和一个处理程序类。顺序很重要;第一个匹配规则将被使用。如果正则表达式包含捕获组,这些组就是 *路径参数*,并将传递给处理程序的 HTTP 方法。如果将字典作为 URLSpec 的第三个元素传递,它将提供 *初始化参数*,这些参数将传递给 RequestHandler.initialize。最后,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.renderRequestHandler.write 之类的函数来生成响应。 render() 按照名称加载 Template 并使用给定的参数呈现它。 write() 用于基于非模板的输出;它接受字符串、字节和字典(字典将被编码为 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":...} 的字典。只有在使用表单包装器(即 multipart/form-data Content-Type)上传文件时,才会存在 files 对象;如果未使用此格式,则原始上传数据在 self.request.body 中可用。默认情况下,上传的文件将在内存中完全缓冲;如果您需要处理过大而无法方便地保留在内存中的文件,请参阅 stream_request_body 类装饰器。

在 demos 目录中,file_receiver.py 演示了接收文件上传的两种方法。

由于 HTML 表单编码的奇特性(例如,围绕单数和复数参数的歧义),Tornado 不尝试将表单参数与其他类型的输入统一起来。特别是,我们不解析 JSON 请求主体。希望使用 JSON 而不是表单编码的应用程序可以覆盖 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 中的某些其他方法旨在根据需要被子类覆盖。在每个请求上,都会执行以下调用序列

  1. 在每个请求上都会创建一个新的 RequestHandler 对象。

  2. initialize() 使用来自 Application 配置的初始化参数进行调用。 initialize 通常只将传递给成员变量的参数保存下来;它可能不会产生任何输出或调用 send_error 等方法。

  3. prepare() 被调用。这在所有处理程序子类共享的基类中最有用,因为无论使用哪种 HTTP 方法,都会调用 prepareprepare 可以产生输出;如果它调用 finish (或 redirect 等),处理将在此处停止。

  4. 调用其中一个 HTTP 方法: get()post()put() 等。如果 URL 正则表达式包含捕获组,则将它们作为参数传递给此方法。

  5. 请求完成后,将调用 on_finish() 。这通常在 get() 或其他 HTTP 方法返回之后。

所有旨在被覆盖的方法都在 RequestHandler 文档中进行了说明。一些最常被覆盖的方法包括

错误处理

如果处理程序引发异常,Tornado 将调用 RequestHandler.write_error 来生成错误页面。 tornado.web.HTTPError 可用于生成指定的狀態碼;所有其他异常都会返回 500 状态码。

默认错误页面在调试模式下包含堆栈跟踪,否则包含错误的单行描述(例如“500:内部服务器错误”)。要生成自定义错误页面,请覆盖 RequestHandler.write_error (可能在所有处理程序共享的基类中)。此方法可以通过 writerender 等方法正常产生输出。如果错误是由异常引起的,则将以关键字参数的形式传递 exc_info 三元组(请注意,此异常不能保证是 sys.exc_info 中的当前异常,因此 write_error 必须使用例如 traceback.format_exception 而不是 traceback.format_exc)。

还可以通过调用 set_status 、写入响应并返回,从常规处理程序方法而不是 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.redirectRedirectHandler

您可以在 RequestHandler 方法中使用 self.redirect() 将用户重定向到其他地方。还有一个可选参数 permanent ,您可以使用它来指示重定向被认为是永久的。 permanent 的默认值为 False ,它会生成 302 Found HTTP 响应代码,适用于在成功 POST 请求后重定向用户。如果 permanentTrue ,则使用 301 Moved Permanently HTTP 响应代码,这对于例如以 SEO 友好的方式将用户重定向到页面的规范 URL 非常有用。

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 发送临时重定向,请在 RedirectHandler 初始化参数中添加 permanent=False

异步处理程序

某些处理程序方法(包括 prepare() 和 HTTP 动词方法 get()/post() 等)可以被覆盖为协程,以使处理程序异步。

例如,以下是一个使用协程的简单处理程序。

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() 以在客户端关闭连接后进行清理(但请参阅该方法的文档字符串以了解注意事项)。