Tornado - 入门

Posted brt2

tags:

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

1. 概述

Tornado是一个可扩展的非阻塞Web服务器以及相关工具的总称。Tornado每秒可以处理数以千计的连接,所以对于实时Web服务来说,Tornado是一个理想的Web框架

  • 完备的 Web 框架:
    • URL路由映射
    • Request上下文
    • 基于模板的页面渲染
  • 性能与 Twisted、Gevent 等底层框架相媲美
    • 异步 I/O 支持
    • 超时事件处理
  • 除了做Web服务,还可以用来做爬虫应用、物联网关、游戏服务器等后台应用
  • 自带组件(强大)
    • 高效的HTTP服务器:可直接用于生产环境!
    • 基于异步框架的HTTPClient
  • 支持WebSocket
  • 丰富的用户身份认证功能

因为 Tornado 的上述特点,Tornado常被用作大型站点的接口服务框架,而不像Django那样着眼于建立完整的大型网站。

2. URL路由映射

  1. 固定字串路径

    Handlers = [
        ("/", MainHandler),
        ("/entry", EntryHandler),
        ("/entry/2019", Entry2019Handler)
    ]
    
  2. 正在表达式定义路径

    def make_app():
        return tornado.web.Application([
            ("/id/([^/]+)", MainHandler),
        ])
    

3. 定义Handler的行为函数

RequestHandler 是对一类问题处理的单元。

需要子类继承并定义具体行为的函数在RequestHandler中被称为接入点函数(Entry point),上面的 Hello World 实例中的get()函数就是典型的接入点函数。

3.1. 默认的回调函数

  • initialize()

    class ProfileHandler(RequestHandler):
        def initialize(self,database):
            self.database=database
    
  • prepare()

    用于调用请求处理(get、post等)方法之前的初始化处理,通常用来做资源初始化操作。

  • on_finish()

    用于请求处理结束后的一些清理工作,通常用来清理对象占用的内存或者关闭数据库连接等工作。

3.2. HTTP Action处理函数

  • RequestHandler.get(*args,**kwargs)
  • RequestHandler.post(*args,**kwargs)
  • RequestHandler.head(*args,**kwargs)
  • RequestHandler.delete(*args,**kwargs)
  • RequestHandler.patch(*args,**kwargs)
  • RequestHandler.put(*args,**kwargs)
  • RequestHandler.options(*args,**kwargs)

4. 获取请求参数

输入捕捉是指在RequestHandler中用于获取客户端输入的工具函数和属性。比如获取URL参数、Post提交参数等。

  • get_argument(name)、get_arguments(name)

    RequestHandler.get_argument(name) 与 RequestHandler.get_arguments(name) 都是返回给定参数的值。get_argument 是获取单个值, 而 get_arguments 在参数存在多个值得情况下使用,返回多个值的列表。

    注意:使用这两个方法获取的事 URL 中查询的参数与 POST 提交的参数的参数合集。

  • get_query_argument(name)、get_query_arguments(name)

    功能与上面两个方法类似,唯一区别是这两个方法仅仅从 URL 中查询参数。

  • get_body_argument(name)、get_body_arguments(name)

    功能尚与上面四个方法类似,唯一区别是这两个方法仅仅从 POST 提交的参数中查询。

  • get_cookie(name,default=None)

    根据 Cookie 名称获取 Cookie 的值

提示:实际开发中一般会使用 get_argument、get_arguments 这两个方法,因为他们会包含其他方法的查询结果。

4.1. RequestHandler.request

返回 tornado.httputil.HTTPServerRequest 对象实例的属性,通过该对象可以获取关于 HTTP 请求的一切信息,比如:

class DetailHandler(RequestHandler):
    def get(self):
        ip = self.request.remote_ip  # 获取客户端的IP地址
        host = self.request.host  # 获取请求的主机地址
        result = "ip地址为%s,host为%s"%(ip, host)
        return self.write(result)

常用的 httputil.HTTPServerRequest 对象属性如下表:

属性名 说明
method HTTP 请求方法,例如:GET、POST
uri 客户端请求的 uri 的完整内容。
path uri 路径名,即不包含查询字符串
query uri 中的查询字符串
version 客户端发送请求时使用的 HTTP 版本,例如:HTTP/1.1
headers 以字典方式的形式返回 HTTP Headers
body 以字符串的形式返回 HTTP 消息体
remote_ip 客户端的 IP 地址
protocol 请求协议,例如:HTTP、HTTPS
host 请求消息的主机名
arguments 客户端提交的所有参数。
files 以字典形式返回客户端上传的文件,每个文件名对应一个 HTTPFile
cookies 客户端提交的 Cookies 字典

5. 设置应答参数

  • RequestHandler.set_status(status_code,reason=None)

    设置 HTTP Response 中的返回码,如果有描述性的语句,则可以赋值给 reason 参数。

  • RequestHandler.set_header(name,value)

    以键值对的方式设置 HTTP Response 中的 HTTP 头参数,使用 set_header 配置的 Header 值将覆盖之前配置的 Header。

  • RequestHandler.add_header(name,value)

    以键值对的方式设置 HTTP Response 中的 HTTP 头参数。与 set_header 不同的是 add_header 配置的 Header 值将不会覆盖之前配置的 Header。

  • RequestHandler.write(chunk)

    将给定的块作为 HTTP Body 发送客户端。在一般情况下,用本函数输出字符串给客户端。
    如果给定的块是一个字典,则会将这个块以 JSON 格式发送给客户端,同时将 HTTP Header 中的 Content_Type 设置为 application/json.

  • RequestHandler.finish(chunk=None)

    本方法通知 Tornado.Response 的生成工作已完成,chunk 参数是需要传递给客户端的 HTTP body。调用 finish() 后,Tornado 将向客户端发送 HTTP Response。
    本方法适用于对 RequestHandler 的异步请求处理,在同步或协程访问处理的函数中,无须调用 finish() 函数。

  • RequestHandler.render(template_name,**kwargs)

    用给定的参数渲染模块,可以在本函数中传入模板文件名称和模板参数。

    import tornado.web
    class MainHandler(tornado.web.RequestHandler):
        def get(self):
            items=["Python","C++","Java"]
            #第一个参数是模板名称,后面是模板参数
            self.render("template.html",title="Tornado Template",items=items)
    
  • RequestHandler.redirect(url,permanent=False,status=None)

    进行页面重定向。在 RequestHandler 处理过程中,可以随时调用 redirect() 函数进行页面重定向。

  • RequestHandler.clear()

    清空所有在本次请求中之前写入的 Header 和 Body 内容。

  • RequestHandler.set_cookie(name,value)

    按键值对设置 Response 中的 Cookie 的值。

  • RequestHandler.clear_all_cookies(path="/",domain=None)

    清空本次请求中的所有 Cookie。

6. 异步与协程化

Tornado有两种方式可改变同步的处理流程:

  • 异步化(已过期,不再讨论)

    针对RequestHandler的处理函数使用 @tornado.web.asynchronous 修饰器,将默认的同步机制改为异步机制。该方法已经过期。

  • 协程化

    针对RequestHandler的处理函数使用 @tornado.gen.coroutine 修饰器,将默认的同步机制改为协程机制。

Tornado协程结合了同步处理和异步处理的有点,使得代码即清晰易懂,又能够适应海量客户端的高并发请求。

class MainHandler(tornado.web.RequestHandler):
    @tornado.gen.coroutine
    def get(self):
        http = tornado.httpclient.AsyncHTTPClient()
        response = yield http.fetch("http://www.baidu.com")
        self.write(response.body)

协程化的关键技术点如下:

  • @tornado.gen.coroutine 装饰MainHandler的get()、post()等处理函数。
  • 使用异步对象处理耗时操作,比如本例的AsyncHTTPClient。
  • 调用yield关键字获取异步对象的处理结果。

7. Cookie机制

Cookie是很多网站为了辨别用户的身份而存储在用户本地终端(Client Side)d的数据,在Tornado中使用RequestHandler.get_cookie()、RequestHandler.set_cookie()可以方便地对Cookie进行读写。

session_id = 1
class MainHandler(tornado.web.RequestHandler):
    def get(self):
        global session_id
        if not self.get_cookie("session"):
            self.set_cookie("session",str(session_id))
            session_id += 1
            self.write("设置新的session")
        else:
            self.write("已经具有session")

因为Cookie总是被保存在客户端,所以如何保存其不被篡改是服务器端程序必须解决的问题。Tornado为Cookie提供了信息加密机制,使得客户端无法随意解析和修改Cookie的键值。

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        ...
        if not self.get_secure_cookie("session"):
            #set_secure_cookie代替set_cookie
            self.set_secure_cookie("session",str(session_id))
...

if __name__ == ‘__main__‘:
    app=tornado.web.Application([
        ("/",MainHandler)
    ],cookie_secret="JIA_MI_MI_YAO")
    app.listen("8888")
    tornado.ioloop.IOLoop.current().start()

对比上面的简单Cookie实例可以发现不同之处:

  • tornado.web.Application 对象初始化时赋予 cookie_secret 参数,该参数值时一个字符串,用于保存本网站Cookie加密时的密钥。
  • 读取: 使用 RequestHandler.get_secure_cookie 代替原来的get_cookie()。
  • 写入: 用 RequestHandler.set_secure_cookie 替换原来的set_cookie()。

这样,就不需要担心Cookie伪造的问题了,但是cookie_secret参数值作为加密密钥,需要好好保护,不能泄露。

8. 用户身份认证

在RequestHandler类中有一个 current_user 属性用于保存当前请求的用户名。RequestHandler.get_current_user 的默认值是None,在get()、post()等处理函数中可以随时读取该属性以获取当前的用户名。RequestHandler.current_user 是一个只读属性,所以如果想要设置该属性值,需要重载 RequestHandler.get_current_user() 函数以设置该属性值。

使用 current_user 属性及 get_current_user() 方法来实现用户身份控制。

import tornado.web
import tornado.ioloop
import uuid  # UUID 生成库

dict_sessions = {}  # 保存所有登录的Session

class BaseHandler(tornado.web.RequestHandler):  #公共基类
    #写入current_user的函数
    def get_current_user(self):
        session_id=self.get_secure_cookie("session_id")
        return dict_sessions.get(session_id)

class MainHandler(BaseHandler):
    @tornado.web.authenticated  #需要身份认证才能访问的处理器
    def get(self):
        name=tornado.escape.xhtml_escape(self.current_user)
        self.write("Hello,"+name)

class LoginHandler(BaseHandler):
    def get(self):   #登陆页面
        self.write(‘<html><>body‘
                   ‘<form action="/login" method="post">‘
                   ‘Name:<input type="text" name="name">‘
                   ‘<input type="submit" value="Sign in">‘
                   ‘</form></body></html>‘)
    def post(self):  #验证是否运行登陆
        if len(self.get_argument("name"))<3:
            self.redirect("/login")
        session_id=str(uuid.uuid1())
        dict_sessions[session_id]=self.get_argument("name")
        self.set_secure_cookie("session_id",session_id)
        self.redirect("/")

setting = {
    "cookie_secret":"SECRET_DONT_LEAK", #Cookie加密秘钥
    "login_url":"/login"  #定义登陆页面
}

application = tornado.web.Application([
    (r"/",MainHandler),        #URL映射定义
    (r"/login",LoginHandler)
],**setting)

if __name__ == ‘__main__‘:
    application.listen(8888)
    tornado.ioloop.IOLoop.current().start()     #挂起监听

注意:加入身份认证的所有页面处理器需要继承自BaseHandler类,而不是直接继承原来的tornado.web.RequestHandler类。

9. 防止跨站攻击

跨站请求伪造(Cross-site request forgery,CSRF 或XSRF)是一种对网站的恶意利用。通过CSRF,攻击者可以冒用用户的身份,在用户不知情的情况下执行恶意操作。

下图展示了 CSRF 的基本原理。其中 Site1 是存在 CSRF 漏洞的网站,而 SIte2 是存在攻击行为的恶意网站。

技术图片

上图内容解析如下:

  1. 用户首先访问了存在 CSRF 漏洞网站 Site1,成功登陆并获取了 Cookie,此后,所有该用户对 Site1 的访问均会携带 Site1 的 Cookie,因此被 Site1 认为是有效操作。

  2. 此时用户又访问了带有攻击行为的站点 Site2,而 Site2 的返回页面中带有一个访问 Site1 进行恶意操作的连接,但却伪装成了合法内容,比如下面的超链接看上去是一个抽奖信息,实际上却是想 Site1 站点提交提款请求

    <a href=‘http:三百万元抽奖,免费拿‘ >
    
  3. 用户一旦点击恶意链接,就在不知情的情况下向 Site1 站点发送了请求。因为之前用户在 Site1 进行过登陆且尚未退出,所以 Site1 在收到用户的请求和附带的 Cookie 时将被认为该请求是用户发送的正常请求。此时,恶意站点的目的也已经达到。

为了防范 CSRF 攻击,要求每个请求包括一个参数值作为令牌的匹配存储在 Cookie 中的对应值。

Tornado 应用可以通过一个 Cookie 头和一个隐藏的 HTML 表单元素向页面提供令牌。这样,当一个合法页面的表单被提交时,它将包括表单值和已存储的 Cookie。如果两者匹配,则 Tornado 应用认可请求有效。

开启 Tornado 的 CSRF 防范功能需要两个步骤。

  1. 开启 xsrf_cookies=True 参数:

    application=tornado.web.Application([
            (r‘/‘,MainHandler),
        ],
        cookie_secret=‘DONT_LEAK_SECRET‘,
        xsrf_cookies=True,
    )
    
  2. 在每个具有 HTML 表达的模板文件中,为所有表单添加 xsrf_form_html() 函数标签:

    <form action="/login" method="post">
        {% module xsrf_form_html() %}  # 为表单添加隐藏元素以防止跨站请求
        <input type="text" name="message"/>
        <input type="submit" value="Post"/>
    </form>
    

    这里的 {% module xsrf_form_html() %}起到了为表单添加隐藏元素以防止跨站请求的作用。

Tornado的安全Cookie支持和XSRF防范框架减轻了应用开发者的很多负担,没有他们,开发者需要思考很多防范的细节措施,因此Tornado内建的安全功能也非常有用。

10. 部署

之前着重讲解Tornado的编程知识点,所有之前的例子都使用最简单的IOLoop启动方式运行。本节学习如何优化Tornado的运行方式,以达到快捷、易用及资源利用优化的目的。

10.1. WebServer的debug运行模式

  • py文件的更新后,自动重启Sevser程序;

  • 错误追溯,显示到浏览器中;

  • 禁用模板缓存

    在运营环境中模板缓存能提高效率,但在调试期间占用了更多的系统资源,所以将其禁用有利于开发者进行调试。

def make_app():
    return tornado.web.Application([
            #此处写入映射
        ],
        debug=True  #调试模式
    )

10.2. 支持 Ctrl+C 退出

try:
    tornado.ioloop.IOLoop.current().start()
except KeyboardInterrupt:
    tornado.ioloop.IOLoop.current().stop()
    # 此处执行资源回收工作
    print("Program exit!")

10.3. 部署静态文件

10.3.1. 配置对URL路径

def make_app():
    return tornado.web.Application([
            #此处写入映射
        ],
        static_path=os.path.join(os.path.dirname(__file__),‘static‘),
        static_url_prefix="/_static/"  # 挂载点,默认为"/static/"
    )

10.3.2. StaticFileHandler

如果除了http://mysite.com/static目录还有其他存放静态文件的URL,则可以用RequestHandler的子类StaticFileHandler进行配置,比如:

def make_app():
    return tornado.web.Application([
            #此处写入映射

            #这里配置了3个StaticFileHandler
            (r‘/css/(.*)‘,tornado.web.StaticFileHandler,{‘path‘:‘assets/css‘}),
            (r‘/images/png/(.*)‘,tornado.web.StaticFileHandler,{‘path‘:‘assets/image‘}),
            (r‘/js/(.*)‘,tornado.web.StaticFileHandler,{‘path‘:‘assets/js‘,‘default_filename‘:‘templates/index.html‘}),
        ],
        static_path=os.path.join(os.path.dirname(__file__),‘static‘)
    )

本例中除了static_path,还用StaticFileHandler配置了另外3个静态文件目录。

  • 所有对 http://mysite.com/css/* 的访问被映射到相对路径assets/css中。
  • http://mysite.com/images/png/* 的访问被映射到assets/images目录中。
  • http://mysite.com/js/* 的访问被映射到assets/js目录中;该条StaticFileHandler的参数中还被配置了 default_filename 参数,即当用户访问了 http://mysite.com/js 目录本身时,将返回 templates/index.html 文件。

10.3.3. 优化静态文件访问(缓存以减少重复传送)

优化静态文件访问的目的在于减少静态文件的重复传送,提高网络及服务器的利用效率,通过在模板文件中用static_url方法修饰静态文件链接可以达到这个目的:

<html>
    <body>
        <div><img src="{{static_url(‘images/logo.png‘)}}"/><div>
    </body>
</html>

本例中的静态图像链接将被设置为类似 /static/images/logo.png?v=5ad4e 的形式,其中的 v=5ad4e 是logo.png文件内容的哈希值,当Tornado静态文件处理器发现该参数时,将通知浏览器该文件可以无限期缓存,因此避免了之后访问该文件时的反复传输。

11. WebSocket

Tornado 第三章:HTML5 WebSocket概念及应用



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

tornado基础入门——简单了解tornado

Python Tornado初学笔记之表单与模板

tornado框架——入门

tornado 入门篇

Tornado从入门到进阶 打造支持高并发的技术论坛

tornado 使用入门