Tornado应用笔记02-Web框架

索引

本节内容围绕Tornado的Web框架部分展开, 主要介绍Tornado在Web框架部分中使用频率最高的RequestHandler, 同时也包括Application等其余相关内容.

RequestHandler

作为每一个HTTP请求的"必经之地", 一个请求在RequestHandler内的大致处理流程如下:

  1. 根据正则匹配创建相应RequestHandler
  2. .initialize()初始化
  3. .prepare()准备
  4. 根据请求的http verb method进入相应入口, 如.get() .post()
  5. .finish()完成请求
  6. .on_finish()后续操作

(注: 这个流程是不够严谨的, 只是希望读者对此能先有个大概的认识)

RequestHandler内的方法可以划分成以下几类: 入口, 输入, 输出, Cookie和其他, 这里只分析其中最常用的方法, 如果想要了解全部内容则需要查阅官方文档.

入口(参考链接):

.initialize()

进行初始化工作, 可以接收来自注册路由时传递的参数. 虽然这里也可以做输出操作, 但是并不建议这么做, 输出操作放到.prepare()会使逻辑更清晰.


.prepare()

可以理解为一个请求"真正"的开始, 主要用来处理一些请求的准备工作, 比如预处理请求, 也可以做输出操作. 完成以后进入到.get() .post()等. 需要注意的是, 如果在这里结束请求, 如调用.finish()等, 那就不会执行.get() .post()等. 有一个比较有意思的点是.prepare()是可以"异步"的, 更准确的说法应该是可以"协程化", 通过@gen.coroutine@return_future可以实现(不能使用@asynchronous). 关于Tornado实现协程和异步的方法, 后续会有文章深入探讨, 这里就不展开说了.


.on_finish()

请求完成后自动调用(实际上是由.finish()调用的), 可以根据需要做一些释放资源或写日志等操作. 注意, 这里是不能进行输出操作的.

默认支持的http verb method
.get() .post() .put() .patch() .delete() .head() .options()


跑一个例子能更好的理解这个流程

# -*- coding: utf-8 -*-
# file: request_entry_point.py

import tornado.ioloop
import tornado.web


class BaseHandler(tornado.web.RequestHandler):
    # 扩展默认http方法的办法
    SUPPORTED_METHODS = tornado.web.RequestHandler.SUPPORTED_METHODS + ('PROPFIND',)

    def initialize(self, **kwargs):
        print 'into initialize'
        self._id = kwargs.get('id', -1)

    def prepare(self):
        print 'into prepare'

    def on_finish(self):
        print 'into finish'
        self.release_resource()

    def release_resource(self):
        pass

    def get(self, *args, **kwargs):
        print 'into get'
        arg = args[0] or None
        self.write('a get request, the re arg is |%s|' % arg)

    def propfind(self, *args, **kwargs):
        print 'into extra method propfind'
        self.write('a propfind request')


def make_app():
    return tornado.web.Application([
        # 正则匹配的参数会传入http方法中
        (r"/(.*)", BaseHandler, {'id': '123456'}),
    ])


if __name__ == "__main__":
    app = make_app()
    app.listen(8888)
    print 'Tornado server is running at localhost:8888'
    tornado.ioloop.IOLoop.current().start()

输入(参考链接):

请求参数
.get_argument() .get_arguments()

bodyurl中获取参数(参数都是unicode编码的), 两者不同点在于.get_arguments()返回的是参数列表, 而.get_argument()返回参数列表的最后一个参数, 并且.get_argument()会在目标参数不存在的时候抛出MissingArgumentError异常.


.get_query_argument() .get_query_arguments()

url中获取参数, 区别参考.get_argument() .get_arguments()


.get_body_argument() .get_body_arguments()

body中获取参数, 区别参考.get_argument() .get_arguments()


.get_json()

实际上, Tornado并未直接提供获取json格式数据的方法, 如果有需要的话, 可以参考下面这段代码

def get_json(self):
    import json
    content_type = self.request.headers.get('Content-Type')
    if content_type and content_type.lower().startswith('application/json'):
        try:
            return json.loads(self.request.body)
        except ValueError:
            pass
    raise Exception('get json fail, please check content-type |%s| & body |%s|' % (content_type, body))
请求信息
.request

.request实际上是一个HTTPServerRequest对象, 包含method uri query version headers body remote_ip protocol host arguments query_arguments body_arguments files connection cookies full_url() request_time().

这里只介绍headersfiles(cookies放在后面与相关方法一起介绍), 其余的可以参考官方文档, 又或是print出来看看是什么.

在上传文件时(Content-Type: multipart/form-data; boundary=----WebKitFormBoundary*random_string*), 文件变为HTTPFile对象

# .request.files的结构
{
    'arg_name': [
        {
            'body': '********',
            'content_type': 'image/png',
            'filename': 'picture.png',
        },
    ]
}

headers 是一个HTTPHeaders对象, 使用方法参考:

# 获取元素
print self.request.headers.get('Content-Type')
print self.request.headers['Content-Type']
# 可以直接转字典
print json.dumps(dict(self.request.headers), indent=2)

输出(参考链接):

HTTP status
.set_status()

设置响应HTTP状态码


.send_error() .write_error()

.send_error()用于发送HTTP错误页(状态码). 该操作会调用.clear() .set_status() .write_error()用于清除headers, 设置状态码, 发送错误页. 重写.write_error()可以自定义错误页.

HTTP header
.add_header() .set_header() .set_default_headers()

设置响应HTTP头, 前两者的不同点在于多次设置同一个项时, .add_header()会"叠加"参数, 而.set_header()则以最后一次为准.

# add_header
self.add_header('Foo', 'one')
self.add_header('Foo', 'two')
# set_header
self.set_header('Bar', 'one')
self.set_header('Bar', 'two')

# HTTP头的设置结果
# Foo → one, two
# Bar → two

.set_default_headers()比较特殊, 是一个空方法, 可根据需要重写, 作用是在每次请求初始化RequestHandler时设置默认headers.

.clear_header() .clear()

.clear_header()清除指定的headers, 而.clear()清除.set_default_headers()以外所有的headers设置.

数据流
.write()

将数据写入输出缓冲区. 如果直接传入dict, 那Tornado会自动将其识别为json, 并把Content-Type设置为application/json, 如果你不想要这个Content-Type, 那么在.write()之后, 调用.set_header()重新设置就好了. 需要注意的是, 如果直接传入的是list, 考虑到安全问题(json数组会被认为是一段可执行的JavaScript脚本, 且<script src="*/secret.json">可以绕过跨站限制), list将不会被转换成json.


.flush()

将输出缓冲区的数据写入socket. 如果设置了callback, 会在完成数据写入后回调. 需要注意的是, 同一时间只能有一个"等待"的flush callback, 如果"上一次"的flush callback还没执行, 又来了新的flush, 那么"上一次"的flush callback会被忽略掉.


.finish()

完成响应, 结束本次请求. 通常情况下, 请求会在return时自动调用.finish(), 只有在使用了异步装饰器@asynchronous或其他将._auto_finish设置为False的操作, 才需要手动调用.finish().

页面
.render()

返回渲染完成的html. 调用后不能再进行输出操作.

.redirect()

重定向, 可以指定3xx重定向状态码. 调用后不能再进行输出操作.

# 临时重定向 301
self.redirect('/foo')
# 永久重定向 302
self.redirect('/foo', permanent=True)
# 指定状态码, 会忽略参数 permanent
self.redirect('/foo', status=304)

Cookie(参考链接):

获取
.cookies

.request.cookies的别名, Cookie.SimpleCookie()对象(了解更多).

# 如果你想查看字典形式的 cookies, 可以用下面的方法
cookies = self.cookies
print json.dumps({k: cookies[k].value for k in cookies}, indent=2)
设置和解析
.set_cookie() .set_secure_cookie() .get_cookie() .get_secure_cookie()

设置和解析cookies. 两组方法的用法基本一致, 不过使用.set_secure_cookie() .get_secure_cookie()前需要在Application中设置cookie_secret.

import time
# 设置 a=aa; httponly; Path=/ 3600秒后过期
self.set_cookie('a', 'aa', httponly=True, expires=time.time() + 3600)
# 设置 b=bb; secure; Path=/ 1天后过期
self.set_cookie('b', 'bb', secure=True, expires_days=1)
# 获取 cookie 值
self.get_cookie('a', 'default value')
清除
.clear_cookie() .clear_all_cookies()

清除cookie. 前者清除指定值, 后者清除所有.

安全签名
.create_signed_value()

这个方法比较特殊, 作用是生成一个难以被伪造的带时间戳的加密字符串, 这是.set_secure_cookie()之所以"secure"的关键. 同样也需要先在Application中设置cookie_secret.

# 生成安全签名
secure_sign = self.create_signed_value('foo', '123')
# 同样也是用 get_secure_cookie 解密, 不过需要传入可选参数 value
result = self.get_secure_cookie('foo', secure_sign)

其他(参考链接)

Application
.application

获取处理这个请求的Application对象. 可以用来访问Application内部的变量.


.setting

.application.setting 的别名, 用于获取Application当前配置(dict格式).


.require_setting()

查询Application是否有配置此选项, 如果没有会触发异常.

用户验证
.current_user .get_current_user()

获取当前用户. 只有第一次在请求内调用.current_user时, 才会通过.get_current_user()获取当前用户, 所以.current_user相当于当前用户的缓存. .get_current_user()是一个需要复写的空方法, 用于获取当前用户.


.get_login_url()

获取登录页面链接. Tornado内置的身份验证是由@authenticated .current_user .get_login_url()实现的. 使用@authenticated后, 会在.current_userNone时跳转到login_url. 默认情况下, 使用.get_login_url()需要先在Application设置login_url, 当然也可以通过复写.get_login_url()免去配置, 同时也能更加灵活的配置登录链接.

防御跨站请求伪造
.xsrf_form_html()

内置的防御跨站请求伪造功能, 需要放在html里面, 使用前要在Application设置cookie_secret xsrf_cookies. 实现原理是给把两个由同一token签名过的字符串分别放置在cookiehtml中, 然后在"正式"处理请求前, 解密这两个字符串然后比对token是否相同.

<!-- `.xsrf_form_html()`参考用法 -->
 <form action="/login" method="post">
    {% raw xsrf_form_html() %}
    <input type="text" name="message"/>
    <input type="submit" value="Post1"/>
</form>

有意思的是token的比较并不是简单采用a == b这种方式, 而是使用了一个叫_time_independent_equals的函数. 为什么要绕一大圈呢? 实际上是出于安全的考虑, 常规的比较方法如a == b, 一旦发现两者的不同点, 就会立即退出比较, 这样好像确实也没什么不妥的, 从头到尾比较两个字符串确实太低效. 不过既然考虑到了安全, 就不能以常规的角度去看.

现在我们假设比较一个字符串的时间是1s(当然这是极度夸张放大的耗时), 此时我们需要匹配一个长度为3的字符串, 那么按照a == b比较法, 在命中第一个字符后继续比较第二个字符, 那么此次比较耗时肯定是大于1s的, 如果没有命中第一个字符, 那么耗时是1s. 这样的话, 现在我不就能根据耗时"猜出"我给的第一个字符是否匹配了吗. 当然在实际情况下, 不可能有如此夸张的时间差, 但倘若攻击者能够发起大量请求并分析其结果的话, 这也并不是"mission impossible", 所以做一个"恒时"匹配还是有比要的.

# 这种比较方法, 只有两种"常量"耗时
# 一是比较两者长度的耗时,  二是在一的基础上叠加完全匹配两者的耗时
# 需要注意的是, 这里的"常量"指的是它的耗时几乎只受字符串长度的影响
def _time_independent_equals(a, b):
    if len(a) != len(b):
        return False
    result = 0
    # a, b = 'abc', 'def'
    # zip(a, b) => [('a', 'd'), ('b', 'e'), ('c', 'f')]
    for x, y in zip(a, b):
        result |= ord(x) ^ ord(y)
    return result == 0

Application(参考链接)

Application的初始化可以考虑采用下面的方法. Application中的可配置项有很多, 这里只挑了其中最常用的做解释, 想了解更多关于配置的内容可以进入到上方的参考链接查看.

class Application(tornado.web.Application):
    def __init__(self):
        """
            以 /xxxx/([\d]*)/ 为例
            "([\d]*)"会以参数的形式传递给路由,
            在下面的情况, "([\d]*)", 就是 yyyy
            def MyHandler(BaseHandler):
                get(self, yyyy):
                    print yyyy

            对于有多组匹配的路由, 参数会按从左到右的顺序传递给路由
            以 /xxxx/([\w\-]+)/([\d]*)/ 为例,
            "([\w\-]+)" 对应 yyyy, "([\d]*)" 对应 zzzz,
            def MyHandler(BaseHandler):
                get(self, yyyy, zzzz):
                    print yyyy, zzzz
        """
        handlers = [
            (r'/skill/([\w\-]+)/', SkillHandler),
        ]

        tornado_settings = dict(
            # debug 模式开关, 开启 debug 文件变化时自动重载
            debug=False,

            # 自带防跨站脚本, 开启后将验证每次请求的 _xsrf 参数
            xsrf_cookies=True,

            # 模板和静态文件路径配置
            template_path=os.path.join(os.path.dirname(__file__), 'templates'),
            static_path=os.path.join(os.path.dirname(__file__), 'static'),

            # sercure_cookie 秘钥
            cookie_secret=config.COOKIE_SECRET,

            # 与tornado自带的验证配合, 未登录下用户重定向 uri
            login_url='/login/',

            # log_function 可用于记录完整的请求信息, 复写可自定义日志输出
            log_function=my_log,
        )

        # 添加 application 配置, 追加到父类 __init__
        super(Application, self).__init__(handlers, **tornado_settings)

        # application的变量, 可在 handler 中通过 self.application.db 调用
        self.db = torndb.Connection(**config.mysql_settings)

另外需要注意的是, Tornado支持通过 x-real-ip 或 x-forwarded-for来获取IP, 但前提是需要在你的HTTPServer实例中增加xheaders=True参数, 如:

http_server = tornado.httpserver.HTTPServer(Application(), xheaders=True)
http_server.listen(8888)
tornado.ioloop.IOLoop.current().start()

否则通过RequestHandler.request.remote_ip取到的IP只能是127.0.0.1

本节内容就是这些了, 涵盖了Tornado的Web框架部分中日常开发能使用到的绝大部分方法, 看起来内容不少, 实际上来来回回都是这几样套路. 下节内容将开始介绍Tornado异步和协程的内容.

NEXT ===> Tornado应用笔记03-协程与异步示例

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

推荐阅读更多精彩内容