索引
本节内容围绕Tornado的Web框架部分展开, 主要介绍Tornado在Web框架部分中使用频率最高的RequestHandler
, 同时也包括Application
等其余相关内容.
RequestHandler
作为每一个HTTP请求的"必经之地", 一个请求在RequestHandler
内的大致处理流程如下:
- 根据正则匹配创建相应
RequestHandler
-
.initialize()
初始化 -
.prepare()
准备 - 根据请求的
http verb method
进入相应入口, 如.get()
.post()
等 -
.finish()
完成请求 -
.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()
从body
和url
中获取参数(参数都是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()
.
这里只介绍headers
和files
(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_user
为None
时跳转到login_url
. 默认情况下, 使用.get_login_url()
需要先在Application
设置login_url
, 当然也可以通过复写.get_login_url()
免去配置, 同时也能更加灵活的配置登录链接.
防御跨站请求伪造
.xsrf_form_html()
内置的防御跨站请求伪造功能, 需要放在html
里面, 使用前要在Application
设置cookie_secret
xsrf_cookies
. 实现原理是给把两个由同一token
签名过的字符串分别放置在cookie
和html
中, 然后在"正式"处理请求前, 解密这两个字符串然后比对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-协程与异步示例