.. currentmodule:: tornado.web
.. testsetup::
import tornado.web
Tornado web应用的结构
通常一个Tornado web应用包括一个或者多个 .RequestHandler
子类,
一个可以将收到的请求路由到对应handler的 .Application
对象,和
一个启动服务的 main()
函数.
一个最小的”hello world”例子就像下面这样:
.. testcode::
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()
.. testoutput:: :hide:
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
类, 复写一些方法例如
~.RequestHandler.write_error
和 ~.RequestHandler.get_current_user
然后子类继承使用你自己的 BaseHandler
而不是 .RequestHandler
在你所有具体的处理程序中.
处理输入请求
处理请求的程序(request handler)可以使用 ``self.request`` 访问代表当
前请求的对象. 通过
`~tornado.httputil.HTTPServerRequest` 的类定义查看完整的属性列表.
使用HTML表单格式请求的数据会被解析并且可以在一些方法中使用, 例如
`~.RequestHandler.get_query_argument` 和
`~.RequestHandler.get_body_argument`.
.. testcode::
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"))
.. testoutput::
:hide:
由于HTLM表单编码不确定一个标签的参数是单一值还是一个列表,
`.RequestHandler` 有明确的方法来允许应用程序表明是否它期望接收一个列表.
对于列表, 使用
`~.RequestHandler.get_query_arguments` 和
`~.RequestHandler.get_body_arguments` 而不是它们的单数形式.
通过一个表单上传的文件可以使用 ``self.request.files``,
它遍历名字(HTML 标签 ``<input type="file">`` 的name)到一个文件列表.
每个文件都是一个字典的形式
``{"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
, 等), 处理会在这里结束.- 其中一种HTTP方法被调用:
get()
,post()
,put()
, 等. 如果URL的正则表达式包含捕获组, 它们会被作为参数传递给这个方 法. - 当请求结束,
~.RequestHandler.on_finish()
方法被调用. 对于同步 处理程序会在get()
(等)后立即返回; 对于异步处理程序,会在调用~.RequestHandler.finish()
后返回.
所有这样设计被用来复写的方法被记录在了 .RequestHandler
的文档中.
其中最常用的一些被复写的方法包括:
~.RequestHandler.write_error
- 输出对错误页面使用的HTML.~.RequestHandler.on_connection_close
- 当客户端断开时被调用; 应用程序可以检测这种情况,并中断后续处理. 注意这不能保证一个关闭 的连接及时被发现.~.RequestHandler.get_current_user
- 参考 :ref:user-authentication
~.RequestHandler.get_user_locale
- 返回.Locale
对象给当前 用户使用~.RequestHandler.set_default_headers
- 可以被用来设置额外的响应 头(例如自定义的Server
头)
错误处理
如果一个处理程序抛出一个异常, Tornado会调用
`.RequestHandler.write_error` 来生成一个错误页.
`tornado.web.HTTPError` 可以被用来生成一个指定的状态码; 所有其他的异常
都会返回一个500状态.
默认的错误页面包含一个debug模式下的调用栈和另外一行错误描述
(e.g. "500: Internal Server Error"). 为了创建自定义的错误页面, 复写
`RequestHandler.write_error` (可能在一个所有处理程序共享的一个基类里面).
这个方法可能产生输出通常通过一些方法, 例如 `~RequestHandler.write` 和
`~RequestHandler.render`. 如果错误是由异常引起的, 一个 ``exc_info``
将作为一个关键字参数传递(注意这个异常不能保证是 `sys.exc_info` 当前的
异常, 所以 ``write_error`` 必须使用 e.g. `traceback.format_exception` 代替
`traceback.format_exc`).
也可以在常规的处理方法中调用 `~.RequestHandler.set_status` 代替
``write_error`` 返回一个(自定义)响应来生成一个错误页面. 特殊的例外
`tornado.web.Finish` 在直接返回不方便的情况下能够在不调用 ``write_error``
前结束处理程序.
对于404错误, 使用 ``default_handler_class`` `Application setting
<.Application.settings>`. 这个处理程序会复写
`~.RequestHandler.prepare` 而不是一个更具体的方法, 例如 ``get()``
所以它可以在任何HTTP方法下工作. 它应该会产生如上所说的错误页面: 要么raise
一个 ``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响应, 更适合
e.g. 在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/\1")),
])
不像 `.RequestHandler.redirect`, `.RedirectHandler` 默认使用永久重定向.
这是因为路由表在运行时不会改变, 而且被认为是永久的.
当在处理程序中发现重定向的时候, 可能是其他可能改变的逻辑的结果.
用 `.RedirectHandler` 发送临时重定向, 需要添加 ``permanent=False`` 到
`.RedirectHandler` 的初始化参数.
异步处理
~~~~~~~
Tornado默认会同步处理: 当 get()
/post()
方法返回, 请求被认为结束
并且返回响应. 因为当一个处理程序正在运行的时候其他所有请求都被阻塞,
任何需要长时间运行的处理都应该是异步的, 这样它就可以在非阻塞的方式中调用
它的慢操作了. 这个话题更详细的内容包含在
:doc:async
中; 这部分是关于在 .RequestHandler
子类中的异步技术的细节.
使用 .coroutine
装饰器是做异步最简单的方式. 这允许你使用 yield
关键
字执行非阻塞I/O, 并且直到协程返回才发送响应. 查看 :doc:coroutines
了解
更多细节.
在某些情况下, 协程不如回调为主的风格方便, 在这种情况下
.tornado.web.asynchronous
装饰器可以用来代替. 当使用这个装饰器的时候,
响应不会自动发送; 而请求将一直保持开放直到callback调用
.RequestHandler.finish
. 这需要应用程序确保这个方法被调用或者其他用户
的浏览器简单的挂起.
这里是一个使用Tornado’s 内置的 .AsyncHTTPClient
调用FriendFeed API的例
子:
.. testcode::
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()
.. testoutput:: :hide:
当 get()
返回, 请求还没有完成. 当HTTP客户端最终调用
on_response()
, 这个请求仍然是开放的, 通过调用 self.finish()
, 响应最终刷到客户端.
为了方便对比, 这里有一个使用协程的相同的例子:
.. testcode::
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")
.. testoutput:: :hide:
更多高级异步的示例, 请看 chat
example application
<https://github.com/tornadoweb/tornado/tree/stable/demos/chat>
, 实现了一个
使用 长轮询(long polling)
<http://en.wikipedia.org/wiki/Push_technology#Long_polling>
的AJAX聊天室.
长轮询的可能想要覆盖 on_connection_close()
来在客户端关闭连接之后进行清
理(注意看方法的文档来查看警告).