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