首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

tornado全面剖析与实践系列1

猿助猿的技术栈是基于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

  • 发表于:
  • 原文链接http://kuaibao.qq.com/s/20171216G0NCHK00?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券