Tornado

Posted Jackie

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Tornado相关的知识,希望对你有一定的参考价值。

概述

Tornado 是 FriendFeed 使用的可扩展的非阻塞式 web 服务器及其相关工具的开源版本。这个 Web 框架看起来有些像web.py 或者 Google 的 webapp,不过为了能有效利用非阻塞式服务器环境,这个 Web 框架还包含了一些相关的有用工具 和优化。

Tornado 和现在的主流 Web 服务器框架(包括大多数 Python 的框架)有着明显的区别:它是非阻塞式服务器,而且速度相当快。得利于其 非阻塞的方式和对 epoll 的运用,Tornado 每秒可以处理数以千计的连接,这意味着对于实时 Web 服务来说,Tornado 是一个理想的 Web 框架。我们开发这个 Web 服务器的主要目的就是为了处理 FriendFeed 的实时功能 ——在 FriendFeed 的应用里每一个活动用户都会保持着一个服务器连接。(关于如何扩容 服务器,以处理数以千计的客户端的连接的问题,请参阅 C10K problem。)

下载安装:

pip3 install tornado
 
源码安装
https://pypi.python.org/packages/source/t/tornado/tornado-4.3.tar.gz

框架使用

技术分享
 1 #!/usr/bin/env python
 2 # -*- coding:utf-8 -*-
 3    
 4 import tornado.ioloop
 5 import tornado.web
 6    
 7    
 8 class MainHandler(tornado.web.RequestHandler):
 9     def get(self):
10         self.write("Hello, world")
11    
12 application = tornado.web.Application([
13     (r"/index", MainHandler),
14 ])
15    
16    
17 if __name__ == "__main__":
18     application.listen(8888)
19     tornado.ioloop.IOLoop.instance().start()
View Code

执行过程:

  • 第一步:执行脚本,监听 8888 端口
  • 第二步:浏览器客户端
    访问 /index  -->  http://127.0.0.1:8888/index
  • 第三步:服务器接受请求,并交由对应的类处理该请求
  • 第四步:类接受到请求之后,根据请求方式(post / get / delete ...)的不同调用并执行相应的方法
  • 第五步:方法返回值的字符串内容发送浏览器
技术分享
 1 #!/usr/bin/env python
 2 # -*- coding:utf-8 -*-
 3 #!/usr/bin/env python
 4 # -*- coding:utf-8 -*-
 5 
 6 import tornado.ioloop
 7 import tornado.web
 8 from tornado import httpclient
 9 from tornado.web import asynchronous
10 from tornado import gen
11 
12 import uimodules as md
13 import uimethods as mt
14 
15 class MainHandler(tornado.web.RequestHandler):
16         @asynchronous
17         @gen.coroutine
18         def get(self):
19             print start get 
20             http = httpclient.AsyncHTTPClient()
21             http.fetch("http://127.0.0.1:8008/post/", self.callback)
22             self.write(end)
23 
24         def callback(self, response):
25             print response.body
26 
27 settings = {
28     template_path: template,
29     static_path: static,
30     static_url_prefix: /static/,
31     ui_methods: mt,
32     ui_modules: md,
33 }
34 
35 application = tornado.web.Application([
36     (r"/index", MainHandler),
37 ], **settings)
38 
39 
40 if __name__ == "__main__":
41     application.listen(8009)
42     tornado.ioloop.IOLoop.instance().start()
异步非阻塞

二、路由系统

路由系统其实就是 url 和 类 的对应关系,这里不同于其他框架,其他很多框架均是 url 对应 函数,Tornado中每个url对应的是一个类。

#!/usr/bin/env python
# -*- coding:utf-8 -*-
   
import tornado.ioloop
import tornado.web
   
   
class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("Hello, world")
   
class StoryHandler(tornado.web.RequestHandler):
    def get(self, story_id):
        self.write("You requested the story " + story_id)
   
class BuyHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("buy.wupeiqi.com/index")
   
application = tornado.web.Application([
    (r"/index", MainHandler),
    (r"/story/([0-9]+)", StoryHandler),
])
   
application.add_handlers(‘buy.wupeiqi.com$‘, [
    (r‘/index‘,BuyHandler),
])
   
if __name__ == "__main__":
    application.listen(80)
    tornado.ioloop.IOLoop.instance().start()

Tornado中原生支持二级域名的路由,如:

技术分享

三、模板引擎

Tornao中的模板语言和django中类似,模板引擎将模板文件载入内存,然后将数据嵌入其中,最终获取到一个完整的字符串,再将字符串返回给请求者。

Tornado 的模板支持“控制语句”和“表达语句”,控制语句是使用 {% 和 %} 包起来的 例如 {% if len(items) > 2 %}。表达语句是使用 {{ 和 }} 包起来的,例如 {{ items[0] }}

控制语句和对应的 Python 语句的格式基本完全相同。我们支持 ifforwhile 和 try,这些语句逻辑结束的位置需要用 {% end %} 做标记。还通过 extends 和 block 语句实现了模板继承。这些在 template 模块 的代码文档中有着详细的描述。

注:在使用模板前需要在setting中设置模板路径:"template_path" : "tpl"

1、基本使用

技术分享
 1 #!/usr/bin/env python
 2 # -*- coding:utf-8 -*-
 3   
 4 import tornado.ioloop
 5 import tornado.web
 6   
 7   
 8 class MainHandler(tornado.web.RequestHandler):
 9     def get(self):
10         self.render("index.html", list_info = [11,22,33])
11   
12 application = tornado.web.Application([
13     (r"/index", MainHandler),
14 ])
15   
16   
17 if __name__ == "__main__":
18     application.listen(8888)
19     tornado.ioloop.IOLoop.instance().start()
app.py
技术分享
 1 <!DOCTYPE html>
 2 <html>
 3 <head>
 4     <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
 5     <title>老男孩</title>
 6     <link href="{{static_url("css/common.css")}}" rel="stylesheet" />
 7 </head>
 8 <body>
 9 
10     <div>
11         <ul>
12             {% for item in list_info %}
13                 <li>{{item}}</li>
14             {% end %}
15         </ul>
16     </div>
17     
18     <script src="{{static_url("js/jquery-1.8.2.min.js")}}"></script>
19     
20 </body>
21 </html>
index.html
技术分享
 1 在模板中默认提供了一些函数、字段、类以供模板使用:
 2 
 3 escape: tornado.escape.xhtml_escape 的別名
 4 xhtml_escape: tornado.escape.xhtml_escape 的別名
 5 url_escape: tornado.escape.url_escape 的別名
 6 json_encode: tornado.escape.json_encode 的別名
 7 squeeze: tornado.escape.squeeze 的別名
 8 linkify: tornado.escape.linkify 的別名
 9 datetime: Python 的 datetime 模组
10 handler: 当前的 RequestHandler 对象
11 request: handler.request 的別名
12 current_user: handler.current_user 的別名
13 locale: handler.locale 的別名
14 _: handler.locale.translate 的別名
15 static_url: for handler.static_url 的別名
16 xsrf_form_html: handler.xsrf_form_html 的別名
其他方法

2、母版

技术分享
 1 <!DOCTYPE html>
 2 <html>
 3 <head>
 4     <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
 5     <title>老男孩</title>
 6     <link href="{{static_url("css/common.css")}}" rel="stylesheet" />
 7     {% block CSS %}{% end %}
 8 </head>
 9 <body>
10 
11     <div class="pg-header">
12 
13     </div>
14     
15     {% block RenderBody %}{% end %}
16    
17     <script src="{{static_url("js/jquery-1.8.2.min.js")}}"></script>
18     
19     {% block javascript %}{% end %}
20 </body>
21 </html>
layout.html
技术分享
 1 {% extends layout.html%}
 2 {% block CSS %}
 3     <link href="{{static_url("css/index.css")}}" rel="stylesheet" />
 4 {% end %}
 5 
 6 {% block RenderBody %}
 7     <h1>Index</h1>
 8 
 9     <ul>
10     {%  for item in li %}
11         <li>{{item}}</li>
12     {% end %}
13     </ul>
14 
15 {% end %}
16 
17 {% block JavaScript %}
18     
19 {% end %}
index.html

3、导入

技术分享
1 <div>
2     <ul>
3         <li>1024</li>
4         <li>42区</li>
5     </ul>
6 </div>
header
技术分享
 1 <!DOCTYPE html>
 2 <html>
 3 <head>
 4     <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
 5     <title>老男孩</title>
 6     <link href="{{static_url("css/common.css")}}" rel="stylesheet" />
 7 </head>
 8 <body>
 9 
10     <div class="pg-header">
11         {% include header.html %}
12     </div>
13     
14     <script src="{{static_url("js/jquery-1.8.2.min.js")}}"></script>
15     
16 </body>
17 </html>
index.html

4、自定义UIMethod以UIModule

a. 定义

技术分享
1 # uimethods.py
2  
3 def tab(self):
4     return UIMethod
uimethods.py
技术分享
 1 #!/usr/bin/env python
 2 # -*- coding:utf-8 -*-
 3 from tornado.web import UIModule
 4 from tornado import escape
 5 
 6 class custom(UIModule):
 7 
 8     def render(self, *args, **kwargs):
 9         return escape.xhtml_escape(<h1>wupeiqi</h1>)
10         #return escape.xhtml_escape(‘<h1>wupeiqi</h1>‘)
uimodules.py

b. 注册

技术分享
 1 #!/usr/bin/env python
 2 # -*- coding:utf-8 -*-
 3 #!/usr/bin/env python
 4 # -*- coding:utf-8 -*-
 5 
 6 import tornado.ioloop
 7 import tornado.web
 8 from tornado.escape import linkify
 9 import uimodules as md
10 import uimethods as mt
11 
12 class MainHandler(tornado.web.RequestHandler):
13     def get(self):
14         self.render(index.html)
15 
16 settings = {
17     template_path: template,
18     static_path: static,
19     static_url_prefix: /static/,
20     ui_methods: mt,
21     ui_modules: md,
22 }
23 
24 application = tornado.web.Application([
25     (r"/index", MainHandler),
26 ], **settings)
27 
28 
29 if __name__ == "__main__":
30     application.listen(8009)
31     tornado.ioloop.IOLoop.instance().start()
View Code

c. 使用

技术分享
<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title></title>
    <link href="{{static_url("commons.css")}}" rel="stylesheet" />
</head>
<body>
    <h1>hello</h1>
    {% module custom(123) %}
    {{ tab() }}
</body>
View Code

四、静态文件

对于静态文件,可以配置静态文件的目录和前段使用时的前缀,并且Tornaodo还支持静态文件缓存。

技术分享
 1 #!/usr/bin/env python
 2 # -*- coding:utf-8 -*-
 3  
 4 import tornado.ioloop
 5 import tornado.web
 6  
 7  
 8 class MainHandler(tornado.web.RequestHandler):
 9     def get(self):
10         self.render(home/index.html)
11  
12 settings = {
13     template_path: template,
14     static_path: static,
15     static_url_prefix: /static/,
16 }
17  
18 application = tornado.web.Application([
19     (r"/index", MainHandler),
20 ], **settings)
21  
22  
23 if __name__ == "__main__":
24     application.listen(80)
25     tornado.ioloop.IOLoop.instance().start()
app.py
技术分享
 1 <!DOCTYPE html>
 2 <html>
 3 <head lang="en">
 4     <meta charset="UTF-8">
 5     <title></title>
 6     <link href="{{static_url("commons.css")}}" rel="stylesheet" />
 7 </head>
 8 <body>
 9     <h1>hello</h1>
10 </body>
11 </html>
index.html

注:静态文件缓存的实现

技术分享
 1  def get_content_version(cls, abspath):
 2         """Returns a version string for the resource at the given path.
 3 
 4         This class method may be overridden by subclasses.  The
 5         default implementation is a hash of the file‘s contents.
 6 
 7         .. versionadded:: 3.1
 8         """
 9         data = cls.get_content(abspath)
10         hasher = hashlib.md5()
11         if isinstance(data, bytes):
12             hasher.update(data)
13         else:
14             for chunk in data:
15                 hasher.update(chunk)
16         return hasher.hexdigest()
View Code

五、cookie

Tornado中可以对cookie进行操作,并且还可以对cookie进行签名以放置伪造。

1、基本操作

技术分享
1 class MainHandler(tornado.web.RequestHandler):
2     def get(self):
3         if not self.get_cookie("mycookie"):
4             self.set_cookie("mycookie", "myvalue")
5             self.write("Your cookie was not set yet!")
6         else:
7             self.write("Your cookie was set!")
View Code

2、加密cookie(签名)

Cookie 很容易被恶意的客户端伪造。加入你想在 cookie 中保存当前登陆用户的 id 之类的信息,你需要对 cookie 作签名以防止伪造。Tornado 通过 set_secure_cookie 和 get_secure_cookie 方法直接支持了这种功能。 要使用这些方法,你需要在创建应用时提供一个密钥,名字为 cookie_secret。 你可以把它作为一个关键词参数传入应用的设置中:

技术分享
  1 def _create_signature_v1(secret, *parts):
  2     hash = hmac.new(utf8(secret), digestmod=hashlib.sha1)
  3     for part in parts:
  4         hash.update(utf8(part))
  5     return utf8(hash.hexdigest())
  6 
  7 # 加密
  8 def _create_signature_v2(secret, s):
  9     hash = hmac.new(utf8(secret), digestmod=hashlib.sha256)
 10     hash.update(utf8(s))
 11     return utf8(hash.hexdigest())
 12 
 13 def create_signed_value(secret, name, value, version=None, clock=None,
 14                         key_version=None):
 15     if version is None:
 16         version = DEFAULT_SIGNED_VALUE_VERSION
 17     if clock is None:
 18         clock = time.time
 19 
 20     timestamp = utf8(str(int(clock())))
 21     value = base64.b64encode(utf8(value))
 22     if version == 1:
 23         signature = _create_signature_v1(secret, name, value, timestamp)
 24         value = b"|".join([value, timestamp, signature])
 25         return value
 26     elif version == 2:
 27         # The v2 format consists of a version number and a series of
 28         # length-prefixed fields "%d:%s", the last of which is a
 29         # signature, all separated by pipes.  All numbers are in
 30         # decimal format with no leading zeros.  The signature is an
 31         # HMAC-SHA256 of the whole string up to that point, including
 32         # the final pipe.
 33         #
 34         # The fields are:
 35         # - format version (i.e. 2; no length prefix)
 36         # - key version (integer, default is 0)
 37         # - timestamp (integer seconds since epoch)
 38         # - name (not encoded; assumed to be ~alphanumeric)
 39         # - value (base64-encoded)
 40         # - signature (hex-encoded; no length prefix)
 41         def format_field(s):
 42             return utf8("%d:" % len(s)) + utf8(s)
 43         to_sign = b"|".join([
 44             b"2",
 45             format_field(str(key_version or 0)),
 46             format_field(timestamp),
 47             format_field(name),
 48             format_field(value),
 49             b‘‘])
 50 
 51         if isinstance(secret, dict):
 52             assert key_version is not None, Key version must be set when sign key dict is used
 53             assert version >= 2, Version must be at least 2 for key version support
 54             secret = secret[key_version]
 55 
 56         signature = _create_signature_v2(secret, to_sign)
 57         return to_sign + signature
 58     else:
 59         raise ValueError("Unsupported version %d" % version)
 60 
 61 # 解密
 62 def _decode_signed_value_v1(secret, name, value, max_age_days, clock):
 63     parts = utf8(value).split(b"|")
 64     if len(parts) != 3:
 65         return None
 66     signature = _create_signature_v1(secret, name, parts[0], parts[1])
 67     if not _time_independent_equals(parts[2], signature):
 68         gen_log.warning("Invalid cookie signature %r", value)
 69         return None
 70     timestamp = int(parts[1])
 71     if timestamp < clock() - max_age_days * 86400:
 72         gen_log.warning("Expired cookie %r", value)
 73         return None
 74     if timestamp > clock() + 31 * 86400:
 75         # _cookie_signature does not hash a delimiter between the
 76         # parts of the cookie, so an attacker could transfer trailing
 77         # digits from the payload to the timestamp without altering the
 78         # signature.  For backwards compatibility, sanity-check timestamp
 79         # here instead of modifying _cookie_signature.
 80         gen_log.warning("Cookie timestamp in future; possible tampering %r",
 81                         value)
 82         return None
 83     if parts[1].startswith(b"0"):
 84         gen_log.warning("Tampered cookie %r", value)
 85         return None
 86     try:
 87         return base64.b64decode(parts[0])
 88     except Exception:
 89         return None
 90 
 91 
 92 def _decode_fields_v2(value):
 93     def _consume_field(s):
 94         length, _, rest = s.partition(b:)
 95         n = int(length)
 96         field_value = rest[:n]
 97         # In python 3, indexing bytes returns small integers; we must
 98         # use a slice to get a byte string as in python 2.
 99         if rest[n:n + 1] != b|:
100             raise ValueError("malformed v2 signed value field")
101         rest = rest[n + 1:]
102         return field_value, rest
103 
104     rest = value[2:]  # remove version number
105     key_version, rest = _consume_field(rest)
106     timestamp, rest = _consume_field(rest)
107     name_field, rest = _consume_field(rest)
108     value_field, passed_sig = _consume_field(rest)
109     return int(key_version), timestamp, name_field, value_field, passed_sig
110 
111 
112 def _decode_signed_value_v2(secret, name, value, max_age_days, clock):
113     try:
114         key_version, timestamp, name_field, value_field, passed_sig = _decode_fields_v2(value)
115     except ValueError:
116         return None
117     signed_string = value[:-len(passed_sig)]
118 
119     if isinstance(secret, dict):
120         try:
121             secret = secret[key_version]
122         except KeyError:
123             return None
124 
125     expected_sig = _create_signature_v2(secret, signed_string)
126     if not _time_independent_equals(passed_sig, expected_sig):
127         return None
128     if name_field != utf8(name):
129         return None
130     timestamp = int(timestamp)
131     if timestamp < clock() - max_age_days * 86400:
132         # The signature has expired.
133         return None
134     try:
135         return base64.b64decode(value_field)
136     except Exception:
137         return None
138 
139 
140 def get_signature_key_version(value):
141     value = utf8(value)
142     version = _get_version(value)
143     if version < 2:
144         return None
145     try:
146         key_version, _, _, _, _ = _decode_fields_v2(value)
147     except ValueError:
148         return None
149 
150     return key_version
View Code

签名Cookie的本质是:

写cookie过程:

  • 将值进行base64加密
  • 对除值以外的内容进行签名,哈希算法(无法逆向解析)
  • 拼接 签名 + 加密值

读cookie过程:

  • 读取 签名 + 加密值
  • 对签名进行验证
  • base64解密,获取值内容

注:许多API验证机制和安全cookie的实现机制相同。

 1 #!/usr/bin/env python
 2 # -*- coding:utf-8 -*-
 3  
 4 import tornado.ioloop
 5 import tornado.web
 6  
 7  
 8 class MainHandler(tornado.web.RequestHandler):
 9  
10     def get(self):
11         login_user = self.get_secure_cookie("login_user", None)
12         if login_user:
13             self.write(login_user)
14         else:
15             self.redirect(/login)
16  
17  
18 class LoginHandler(tornado.web.RequestHandler):
19     def get(self):
20         self.current_user()
21  
22         self.render(login.html, **{status: ‘‘})
23  
24     def post(self, *args, **kwargs):
25  
26         username = self.get_argument(name)
27         password = self.get_argument(pwd)
28         if username == wupeiqi and password == 123:
29             self.set_secure_cookie(login_user, 武沛齐)
30             self.redirect(/)
31         else:
32             self.render(login.html, **{status: 用户名或密码错误})
33  
34 settings = {
35     template_path: template,
36     static_path: static,
37     static_url_prefix: /static/,
38     cookie_secret: aiuasdhflashjdfoiuashdfiuh
39 }
40  
41 application = tornado.web.Application([
42     (r"/index", MainHandler),
43     (r"/login", LoginHandler),
44 ], **settings)
45  
46  
47 if __name__ == "__main__":
48     application.listen(8888)
49     tornado.ioloop.IOLoop.instance().start()
技术分享
 1 #!/usr/bin/env python
 2 # -*- coding:utf-8 -*-
 3  
 4 import tornado.ioloop
 5 import tornado.web
 6  
 7 class BaseHandler(tornado.web.RequestHandler):
 8  
 9     def get_current_user(self):
10         return self.get_secure_cookie("login_user")
11  
12 class MainHandler(BaseHandler):
13  
14     @tornado.web.authenticated
15     def get(self):
16         login_user = self.current_user
17         self.write(login_user)
18  
19  
20  
21 class LoginHandler(tornado.web.RequestHandler):
22     def get(self):
23         self.current_user()
24  
25         self.render(login.html, **{status: ‘‘})
26  
27     def post(self, *args, **kwargs):
28  
29         username = self.get_argument(name)
30         password = self.get_argument(pwd)
31         if username == wupeiqi and password == 123:
32             self.set_secure_cookie(login_user, 武沛齐)
33             self.redirect(/)
34         else:
35             self.render(login.html, **{status: 用户名或密码错误})
36  
37 settings = {
38     template_path: template,
39     static_path: static,
40     static_url_prefix: /static/,
41     cookie_secret: aiuasdhflashjdfoiuashdfiuh,
42     login_url: /login
43 }
44  
45 application = tornado.web.Application([
46     (r"/index", MainHandler),
47     (r"/login", LoginHandler),
48 ], **settings)
49  
50  
51 if __name__ == "__main__":
52     application.listen(8888)
53     tornado.ioloop.IOLoop.instance().start()
基于签名Cookie实现用户验证

3、JavaScript操作Cookie

由于Cookie保存在浏览器端,所以在浏览器端也可以使用JavaScript来操作Cookie。

1
2
3
4
5
6
7
8
9
/*
设置cookie,指定秒数过期
 */
function setCookie(name,value,expires){
    var temp = [];
    var current_date = new Date();
    current_date.setSeconds(current_date.getSeconds() + 5);
    document.cookie = name + "= "+ value +";expires=" + current_date.toUTCString();
}

对于参数:

  • domain   指定域名下的cookie
  • path       域名下指定url中的cookie
  • secure    https使用

注:jQuery中也有指定的插件 jQuery Cookie 专门用于操作cookie,猛击这里


以上是关于Tornado的主要内容,如果未能解决你的问题,请参考以下文章

tornado v6 似乎已经放弃了 tornado.web.asynchronous 协程。在代码中解决这个问题有啥不同的方法吗?

源代码剖析tornado-memcached-sessions —— Tornado session 支持的实现

tornado服务器实现原理

1.Tornado简介&&本专栏搭建tornado项目简介

tornado 启动WSGI应用(Flask)使用多线程将同步代码变成异步

有没有一种好方法可以将 pychecker/pylint 应用于 Tornado 模板中的 python 代码?