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.render 或 RequestHandler.write 之类的函数来生成响应。 render() 按照名称加载 Template 并使用给定的参数呈现它。 write() 用于基于非模板的输出;它接受字符串、字节和字典(字典将被编码为 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 中可用,它将名称(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 中的某些其他方法旨在根据需要被子类覆盖。在每个请求上,都会执行以下调用序列
在每个请求上都会创建一个新的
RequestHandler对象。initialize()使用来自Application配置的初始化参数进行调用。initialize通常只将传递给成员变量的参数保存下来;它可能不会产生任何输出或调用send_error等方法。prepare()被调用。这在所有处理程序子类共享的基类中最有用,因为无论使用哪种 HTTP 方法,都会调用prepare。prepare可以产生输出;如果它调用finish(或redirect等),处理将在此处停止。调用其中一个 HTTP 方法:
get(),post(),put()等。如果 URL 正则表达式包含捕获组,则将它们作为参数传递给此方法。请求完成后,将调用
on_finish()。这通常在get()或其他 HTTP 方法返回之后。
所有旨在被覆盖的方法都在 RequestHandler 文档中进行了说明。一些最常被覆盖的方法包括
write_error- 输出用于错误页面的 HTML。on_connection_close- 在客户端断开连接时调用;应用程序可以选择检测这种情况并停止进一步处理。请注意,无法保证可以及时检测到已关闭的连接。get_current_user- 请参见 用户认证。get_user_locale- 返回Locale对象以供当前用户使用。set_default_headers- 可用于在响应中设置附加标头(例如自定义Server标头)。
错误处理¶
如果处理程序引发异常,Tornado 将调用 RequestHandler.write_error 来生成错误页面。 tornado.web.HTTPError 可用于生成指定的狀態碼;所有其他异常都会返回 500 状态码。
默认错误页面在调试模式下包含堆栈跟踪,否则包含错误的单行描述(例如“500:内部服务器错误”)。要生成自定义错误页面,请覆盖 RequestHandler.write_error (可能在所有处理程序共享的基类中)。此方法可以通过 write 和 render 等方法正常产生输出。如果错误是由异常引起的,则将以关键字参数的形式传递 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.redirect 和 RedirectHandler 。
您可以在 RequestHandler 方法中使用 self.redirect() 将用户重定向到其他地方。还有一个可选参数 permanent ,您可以使用它来指示重定向被认为是永久的。 permanent 的默认值为 False ,它会生成 302 Found HTTP 响应代码,适用于在成功 POST 请求后重定向用户。如果 permanent 为 True ,则使用 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() 以在客户端关闭连接后进行清理(但请参阅该方法的文档字符串以了解注意事项)。