猿助猿的技术栈是基于Tornado的, 在学习的过程中参考了很多文章, 但是内容大都碎片化, 缺少系统性讲解, 而且不少关于异步应用的内容还是基于过时的旧版本. 因此打算将开发过程中遇到的问题和应用整理下来, 一来方便日后查阅, 二来也希望能够帮助到和我一样的Tornado开发者, 于是就有了这个系列的文章。 (注: 文章并不是为初学者准备的, 阅读前要需要确认你已经了解Python语法, HTTP协议等Web开发所需的基础知识)
在Python Web框架中, 最为人熟知的三个是Django, Flask和Tornado, 前两者是一重一轻的同步框架, 而后者则是以高性能著称的异步框架. 在使用Tornado的开发团队中,Quara和知乎是最常被提起的(参考:How-does-Quora-use-Tornado和知乎使用了哪些框架和开源库?).
我想在正文开始之前, 需要说明的是, 请不要迷信框架所谓的”高性能”, 框架的作用是让开发者更快速和便捷的构建起所需的应用, 而性能则是由包括系统架构和开发人员能力在内的诸多因素决定的. 况且, 在高性能服务器价格相较开发人员的薪资”不值一提”和”面向上线时间编程”的今天, 过度追求高性能, 恐怕只会弊大于利. 倘若你将Tornado作为一个同步框架使用, 并认为框架能够”自主”实现高性能的话, 那我可真是无FUCK说了。
简介
首先来看Tornado的介绍:
Tornado是一个Python Web框架兼异步网络库, 最初由FriendFeed开发. 得益于非阻塞网络I/O, Tornado可以支撑起数以万计的连接, 因此它很非常适合开发长轮询,WebSockets和那些需要与每个用户建立持久连接的应用.
Tornado包含以下四大模块:
Web框架
HTTP服务器和客户端
异步网络库
协程库
Tornado的Hello World:
运行以后, 在浏览器访问localhost:8888, 就能看到Hello 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()
跑一个例子能更好的理解这个流程
输入:请求参数
.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格式数据的方法, 如果有需要的话, 可以参考下面这段代码
请求信息.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对象
headers 是一个HTTPHeaders对象, 使用方法参考:
输出(参考链接):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()则以最后一次为准.
.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脚本, 且可以绕过跨站限制), list将不会被转换成json.
.flush()
将输出缓冲区的数据写入socket. 如果设置了callback, 会在完成数据写入后回调. 需要注意的是, 同一时间只能有一个”等待”的flush callback, 如果”上一次”的flush callback还没执行, 又来了新的flush, 那么”上一次”的flush callback会被忽略掉.
.finish()
完成响应, 结束本次请求. 通常情况下, 请求会在return时自动调用.finish(), 只有在使用了异步装饰器@asynchronous或其他将._auto_finish设置为False的操作, 才需要手动调用.finish().
页面.render()
返回渲染完成的html. 调用后不能再进行输出操作.
.redirect()
重定向, 可以指定3xx重定向状态码. 调用后不能再进行输出操作.
Cookie:获取.cookies
.request.cookies的别名, Cookie.SimpleCookie()对象(了解更多).
设置和解析
.set_cookie() .set_secure_cookie() .get_cookie() .get_secure_cookie()
设置和解析cookies. 两组方法的用法基本一致, 不过使用.set_secure_cookie() .get_secure_cookie()前需要在Application中设置cookie_secret.
清除
.clear_cookie() .clear_all_cookies()
清除cookie. 前者清除指定值, 后者清除所有.
安全签名.create_signed_value()
这个方法比较特殊, 作用是生成一个难以被伪造的带时间戳的加密字符串, 这是.set_secure_cookie()之所以”secure”的关键. 同样也需要先在Application中设置cookie_secret.
其他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是否相同.
有意思的是token的比较并不是简单采用a == b这种方式, 而是使用了一个叫_time_independent_equals的函数. 为什么要绕一大圈呢? 实际上是出于安全的考虑, 常规的比较方法如a == b, 一旦发现两者的不同点, 就会立即退出比较, 这样好像确实也没什么不妥的, 从头到尾比较两个字符串确实太低效. 不过既然考虑到了安全, 就不能以常规的角度去看.
现在我们假设比较一个字符串的时间是1s(当然这是极度夸张放大的耗时), 此时我们需要匹配一个长度为3的字符串, 那么按照a == b比较法, 在命中第一个字符后继续比较第二个字符, 那么此次比较耗时肯定是大于1s的, 如果没有命中第一个字符, 那么耗时是1s. 这样的话, 现在我不就能根据耗时”猜出”我给的第一个字符是否匹配了吗. 当然在实际情况下, 不可能有如此夸张的时间差, 但倘若攻击者能够发起大量请求并分析其结果的话, 这也并不是”mission impossible”, 所以做一个”恒时”匹配还是有比要的.
Application
领取专属 10元无门槛券
私享最新 技术干货