Tornado 4.3 文档翻译: 用户指南-web应用的结构

译者说

Tornado 4.3于2015年11月6日发布,该版本正式支持Python3.5async/await关键字,并且用旧版本CPython编译Tornado同样可以使用这两个关键字,这无疑是一种进步。其次,这是最后一个支持Python2.6Python3.2的版本了,在后续的版本了会移除对它们的兼容。现在网络上还没有Tornado4.3的中文文档,所以为了让更多的朋友能接触并学习到它,我开始了这个翻译项目,希望感兴趣的小伙伴可以一起参与翻译,项目地址是tornado-zh on Github,翻译好的文档在Read the Docs上直接可以看到。欢迎Issues or PR。

Tornado web应用的结构

通常一个Tornado web应用包括一个或者多个RequestHandler 子类,一个可以将收到的请求路由到对应handler的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 可能有一个名字(name), 这将允许它被 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_errorRequestHandler.get_current_user然后子类继承使用你自己的 BaseHandler 而不是RequestHandler在你所有具体的处理程序中.

处理输入请求

处理请求的程序(request handler)可以使用 self.request 访问代表当前请求的对象. 通过tornado.httputil.HTTPServerRequest 的类定义查看完整的属性列表.

使用HTML表单格式请求的数据会被解析并且可以在一些方法中使用, 例如RequestHandler.get_query_argumentRequestHandler.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"))

由于HTLM表单编码不确定一个标签的参数是单一值还是一个列表,RequestHandler 有明确的方法来允许应用程序表明是否它期望接收一个列表.对于列表, 使用RequestHandler.get_query_argumentsRequestHandler.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 中的某些其他方法被设计成了在必要的时候让子类重写. 在每个请求中, 会发生下面的调用序列:

  1. 在每次请求时生成一个新的 RequestHandler 对象
  2. RequestHandler.initialize()Application 配置中的初始化参数被调用. initialize 通常应该只保存成员变量传递的参数; 它不可能产生任何输出或者调用方法, 例如RequestHandler.send_error.
  3. RequestHandler.prepare() 被调用. 这在你所有处理子类共享的基类中是最有用的, 无论是使用哪种HTTP方法, prepare 都会被调用.prepare 可能会产生输出; 如果它调用 RequestHandler.finish(或者 redirect, 等), 处理会在这里结束.
  4. 其中一种HTTP方法被调用: get(), post(), put(),等. 如果URL的正则表达式包含捕获组, 它们会被作为参数传递给这个方法.
  5. 当请求结束, RequestHandler.on_finish() 方法被调用. 对于同步处理程序会在 get() (等)后立即返回; 对于异步处理程序,会在调用RequestHandler.finish() 后返回.

所有这样设计被用来复写的方法被记录在了RequestHandler的文档中.其中最常用的一些被复写的方法包括:

  • RequestHandler.write_error - 输出对错误页面使用的HTML.
  • RequestHandler.on_connection_close - 当客户端断开时被调用;应用程序可以检测这种情况,并中断后续处理. 注意这不能保证一个关闭的连接及时被发现.
  • RequestHandler.get_current_user - 参考 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.writeRequestHandler.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. 这个处理程序会复写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() 方法返回, 请求被认为结束并且返回响应. 因为当一个处理程序正在运行的时候其他所有请求都被阻塞,任何需要长时间运行的处理都应该是异步的, 这样它就可以在非阻塞的方式中调用它的慢操作了. 这个话题更详细的内容包含在async 中; 这部分是关于在 RequestHandler 子类中的异步技术的细节.

使用 coroutine 装饰器是做异步最简单的方式. 这允许你使用 yield 关键字执行非阻塞I/O, 并且直到协程返回才发送响应. 查看 coroutines了解更多细节.

在某些情况下, 协程不如回调为主的风格方便, 在这种情况下tornado.web.asynchronous 装饰器可以用来代替. 当使用这个装饰器的时候,响应不会自动发送; 而请求将一直保持开放直到callback调用RequestHandler.finish. 这需要应用程序确保这个方法被调用或者其他用户的浏览器简单的挂起.

这里是一个使用Tornado's 内置的 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().

为了方便对比, 这里有一个使用协程的相同的例子:


    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() 来在客户端关闭连接之后进行清理(注意看方法的文档来查看警告).

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容