TOP

Tornado 框架使用详解, 从懵逼到开撸

安装

pip install tornado

简单 web_demo

代码

  # coding:utf-8
 
  import tornado.web
  import tornado.ioloop
 
  class IndexHandler(tornado.web.RequestHandler):
      """主路由处理类"""
      def get(self):  # 我们修改了这里
          """对应http的get请求方式"""
          self.write("Hello Itcast!")
 
  if __name__ == "__main__":
      app = tornado.web.Application([
          (r"/", IndexHandler),
      ])
      app.listen(8000)
      tornado.ioloop.IOLoop.current().start()

测试效果

演示解析

继承自  tornado.web.RequestHandler  的类, 重写 基于 http method 的小写方法。 则可以指定该请求方式下的逻辑

如果位指定具体的请求类型。 则不支持, 返回 405 

验证 405 

 

核心组件说明

代码demo

# coding:utf-8

import tornado.web
import tornado.ioloop
import tornado.httpserver  # 新引入httpserver模块


class IndexHandler(tornado.web.RequestHandler):
    """主路由处理类"""

    def get(self):
        """对应http的get请求方式"""
        self.write("Hello Itcast!")


if __name__ == "__main__":
    app = tornado.web.Application([
        (r"/", IndexHandler),
    ])
    http_server = tornado.httpserver.HTTPServer(app)
    http_server.listen(8000)
    tornado.ioloop.IOLoop.current().start()

解析

组件

 ioloop

 tornado.ioloop.IOLoop.current()     

它是全局的tornado事件循环,是服务器的引擎核心 ( 可包含多个 app,示例中只有1个,实际上可以允许多个,不过一般几乎不会使用多个)

app

  app = tornado.web.Application([ (r"/", IndexHandler), ]) 

代表着一个完成的后端app,它会挂接一个服务端套接字端口对外提供服务。

一个ioloop实例里面可以有多个app实例

handler

 class IndexHandler(tornado.web.RequestHandler): ... 

代表着业务逻辑,我们进行服务端开发时就是编写一堆一堆的handler用来服务客户端请求

路由表

 [ (r"/", IndexHandler), ] 

它将指定的url规则和handler挂接起来,形成一个路由映射表。当请求到来时,根据请求的访问url查询路由映射表来找到相应的业务handler

组件关系

一个ioloop包含多个app , 一个app包含一个路由表,一个路由表包含多个handler,同一个ioloop实例运行在一个单线程环境下

ioloop是服务的引擎核心,它是发动机,负责接收和响应客户端请求,负责驱动业务handler的运行,负责服务器内部定时任务的执行

当一个请求到来时,ioloop读取这个请求解包成一个http请求对象, 找到该套接字上对应app的路由表, 通过请求对象的url查询路由表中挂接的handler

然后执行handler, handler方法执行后一般会返回一个对象,ioloop负责将对象包装成http响应对象序列化发送给客户端

单多进程

简单 demo 中的   app.listen()  和  http_server = tornado.httpserver.HTTPServer(app) /  http_server.listen(8000) 不同就在于

 app.listen() 是只能在单进程下使用, listen是bind和start指令的简称

相当于以下代码的简写, 就是直接只绑定了一个进程 

http_server.bind(options.port)
http_server.start(num_processes=1)

多进程实现

tornado 具备一次性开启多个进程的能力,如以下代码  num_processes=2  就代表开启两个进程

ps: 在使用多进程启动的时候,需要关掉 debug 模式  settings = {'debug' : False} 

from tornado.httpserver import HTTPServer

if __name__ == '__main__':

    # 单进程启动
    # app.listen(8000)
    # Run.instance().start()

    # 多进程启动
    server = HTTPServer(app)
    server.bind(8000)
    server.start(num_processes=2)
    Run.instance().start()

多进程原理

此处引用自 这里 

Tornado 多进程的处理流程是先创建 socket, 然后再 fork 子进程, 这样所有的子进程实际都监听 一个(或多个)文件描述符, 也就是都在监听同样的 socket.

当连接过来所有的子进程都会收到可读事件, 这时候所有的子进程都会跳到回调函数, 尝试建立连接.

一旦其中一个子进程成功的建立了连接, 当其他子进程再尝试建立这个连接的时候就会触发 EWOULDBLOCK (或 EAGAIN) 错误. 这时候回调函数判断是这个错误则返回函数不做处理.

当成功建立连接的子进程还在处理这个连接的时候又过来一个连接, 这时候就会有另外一个子进程接手这个连接.

Tornado 就是通过这样一种机制, 利用多进程提升效率, 由于连接只能由一个子进程成功创建, 同一个请求也就不会被多个子进程处理.

多进程问题解析

提供了便利的同时, 但是也带来了一系列问题

  • 每个子进程都会从父进程中复制一份IOLoop实例,如过在创建子进程前我们的代码动了IOLoop实例,那么会影响到每一个子进程,势必会干扰到子进程IOLoop的工作
  • 所有进程是由一个命令一次开启的,也就无法做到在不停服务的情况下更新代码
  • 所有进程共享同一个端口,想要分别单独监控每一个进程就很困难

因此不建议直接使用自带的 多进程启动方式, 而是手动开启多个进程,并且绑定不同的端口 这部分的内容则放在部署方面后面再说

options 全局配置

tornado.options模块, 提供了全局参数定义、存储、转换

全局的options对象,所有定义的选项变量都会作为该对象的属性

导入

import tornado.options

定义

 tornado.options.define()  用来定义options选项变量的方法,定义的变量可以在全局的  tornado.options.options.xxxx  中获取使用,传入参数:

  • name   选项变量名,须保证全局唯一性,否则会报“Option 'xxx' already defined in ...”的错误;
  • default 选项变量的默认值,如不传默认为None;
  • type  选项变量的类型,从命令行或配置文件导入参数的时候tornado会根据这个类型转换输入的值
    • 转换不成功时会报错,可以是str、float、int、datetime、timedelta中的某个
    • 若未设置则根据default的值自动推断,若default也未设置,那么不再进行转换。
    • 可以通过利用设置type类型字段来过滤不正确的输入。
  • multiple  选项变量的值是否可以为多个,布尔类型,默认值为False
    • 如果multiple为True,那么设置选项变量时值与值之间用英文逗号分隔,
    • 选项变量是一个list列表(若默认值和输入均未设置,则为空列表[])。
  • help选项变量的帮助提示信息
    • 在命令行启动tornado时,通过加入命令行参数 --help 可以查看所有选项变量的信息
    • 注意,代码中需要加入 tornado.options.parse_command_line() 

 tornado.options.options     全局的options对象,所有定义的选项变量都会作为该对象的属性。

 tornado.options.parse_command_line()     转换命令行参数,并将转换后的值对应的设置到全局options对象相关属性上。 追加命令行参数的方式是--myoption=myvalue

 tornado.options.parse_config_file(path)      从配置文件导入option,配置文件中的选项格式如下

ps: 配置文件中的优先级会别代码中的高, 会覆盖代码中的配置使用文件中配置

myoption = "myvalue"
myotheroption = "myothervalue"

代码示例

import tornado.web
import tornado.ioloop
import tornado.httpserver
import tornado.options  # 新导入的options模块

tornado.options.define("port", default=8000, type=int, help="run server on the given port.")  # 定义服务器监听端口选项
tornado.options.define("itcast", default=[], type=str, multiple=True, help="itcast subjects.")  # 无意义,演示多值情况


class IndexHandler(tornado.web.RequestHandler):


    def get(self):
        self.write("Hello Itcast!")


if __name__ == "__main__":
    tornado.options.parse_command_line()
    print(tornado.options.options.port)  # 输出 port 配置
    print(tornado.options.options.itcast)  # 输出 itcast 配置
    app = tornado.web.Application([
        (r"/", IndexHandler),
    ])
    http_server = tornado.httpserver.HTTPServer(app)
    http_server.listen(tornado.options.options.port)
    tornado.ioloop.IOLoop.current().start()

输出

e1e3dd436abd:python -u /opt/project/test_t.py
8000
[]

文件配置写入示例

配置文件

port = 8000
itcast = ["python","c++","java","php","ios"]

代码

# coding:utf-8

import tornado.web
import tornado.ioloop
import tornado.httpserver
import tornado.options 

tornado.options.define("port", default=9000, type=int, help="run server on the given port.")
tornado.options.define("itcast", default=[], type=str, multiple=True, help="itcast subjects.")


class IndexHandler(tornado.web.RequestHandler):

    def get(self):
        self.write("Hello Itcast!")


if __name__ == "__main__":
    tornado.options.parse_config_file("./config")  # 仅仅修改了此处, 新添加了一行,文件中优先级比代码中的高
    print(tornado.options.options.port)  # 输出 port 配置
    print(tornado.options.options.itcast)  # 输出 itcast 配置
    app = tornado.web.Application([
        (r"/", IndexHandler),
    ])
    http_server = tornado.httpserver.HTTPServer(app)
    http_server.listen(tornado.options.options.port)
    tornado.ioloop.IOLoop.current().start()

输出

6faf7901fdae:python -u /opt/project/test_t.py
8000
['python', 'c++', 'java', 'php', 'ios']

特殊日志说明

调用 parse_command_line() 或者 parse_config_file() 的方法时

tornado会默认为我们配置标准logging模块

即默认开启了日志功能,并向标准输出(屏幕)打印日志信息。

如果想关闭tornado默认的日志功能

可以在命令行中添加 --logging=none  或者在代码中执行如下操作

from tornado.options import options, parse_command_line
options.logging = None
parse_command_line()

配置文件写入

 prase_config_file()  的时候,配置文件的书写格式仍需要按照python的语法要求,

其优势是可以直接将配置文件的参数转换设置到全局对象tornado.options.options中

然而,其不方便的地方在于需要在代码中调用tornado.options.define()来定义选项

而且不支持字典类型,故而在实际应用中大都不使用这种方法

在使用配置文件的时候,通常会新建一个python文件(如config.py)

然后在里面直接定义python类型的变量(可以是字典类型)

在需要配置文件参数的地方,将config.py作为模块导入从而使用其中的变量参数

示例

配置文件 config.py

# conding:utf-8
 
# Redis配置
redis_options = {
    'redis_host':'127.0.0.1',
    'redis_port':6379,
    'redis_pass':'',
}
 
# Tornado app配置
settings = {
    'template_path': os.path.join(os.path.dirname(__file__), 'templates'),
    'static_path': os.path.join(os.path.dirname(__file__), 'statics'),
    'cookie_secret':'0Q1AKOKTQHqaa+N80XhYW7KCGskOUE2snCW06UIxXgI=',
    'xsrf_cookies':False,
    'login_url':'/login',
    'debug':True,
}
 
# 日志
log_path = os.path.join(os.path.dirname(__file__), 'logs/log')

导入

# conding:utf-8
 
import tornado.web
import config
 
if __name__ = "__main__":
    app = tornado.web.Application([], **config.settings)

debug 模式

debug,设置tornado是否工作在调试模式,默认为False即工作在生产模式

当设置debug=True 后,tornado会工作在调试/开发模式

特性

在此种模式下,tornado为方便我们开发而提供了几种特性:

  • 自动重启,tornado应用会监控我们的源代码文件,当有改动保存后便会重启程序
    • 一旦我们保存的更改有错误,自动重启会导致程序报错而退出,从而需要我们保存修正错误后手动启动程序
    • 这一特性也可单独通过autoreload=True设置
  • 取消缓存编译的模板
    • 可以单独通过compiled_template_cache=False来设置
  • 取消缓存静态文件hash值
    • 可以单独通过static_hash_cache=False来设置
  • 提供追踪信息,当RequestHandler或者其子类抛出一个异常而未被捕获后,会生成一个包含追踪信息的页面
    • 可以单独通过serve_traceback=True来设置

代码生效开启 debug

import tornado.web
tornado.options.parse_command_line()
app = tornado.web.Application([], debug=True)

 

路由映射

路由映射支持多个参数和多种模式如下

[
    (r"/", Indexhandler),
    (r"/cpp", ItcastHandler, {"subject":"c++"}),
    url(r"/python", ItcastHandler, {"subject":"python"}, name="python_url")
]

常规模式   [(r"/", IndexHandler),]  直接映射到handler

带拓展信息的模式   (r"/cpp", ItcastHandler, {"subject":"c++"}),    此时路由中的字典, 会传入到对应的  RequestHandler的initialize() 

from tornado.web import RequestHandler
class ItcastHandler(RequestHandler):
    def initialize(self, subject):
        self.subject = subject
 
    def get(self):
        self.write(self.subject)

带 name 的时候则需要特殊的方法来封装  from tornado.web import url, RequestHandler   tornado.web.url 

name是给该路由起一个名字,可以通过调用  RequestHandler.reverse_url(name)  来获取该名字对应的url

代码示例

# coding:utf-8
 
import tornado.web
import tornado.ioloop
import tornado.httpserver
import tornado.options
from tornado.options import options, define
from tornado.web import url, RequestHandler
 
define("port", default=8000, type=int, help="run server on the given port.")
 
class IndexHandler(RequestHandler):
    def get(self):
        python_url = self.reverse_url("python_url")
        self.write('<a href="%s">itcast</a>' %
                   python_url)
 
class ItcastHandler(RequestHandler):
    def initialize(self, subject):
        self.subject = subject
 
    def get(self):
        self.write(self.subject)
 
if __name__ == "__main__":
    tornado.options.parse_command_line()
    app = tornado.web.Application([
            (r"/", Indexhandler),
            (r"/cpp", ItcastHandler, {"subject":"c++"}),
            url(r"/python", ItcastHandler, {"subject":"python"}, name="python_url")
        ],
        debug = True)
    http_server = tornado.httpserver.HTTPServer(app)
    http_server.listen(options.port)
    tornado.ioloop.IOLoop.current().start()

演示

request 相关

获得请求参数

HTTP 请求的参数获取途径如下

  • 查询字符串(query string),形如  key1=value1&key2=value2 
  • 请求体(body)中发送的数据,比如表单数据、json、xml
  • 提取 url 的特定部分,如  /blogs/2016/09/0001 ,可以在服务器端的路由中用正则表达式截取
  • 在http报文的头(header)中增加自定义字段,如   X-XSRFToken=itcast 

获取查询URL中拼接参数

get_query_argument(name, default=_ARG_DEFAULT, strip=True)

name: 要获取的参数值,如果出现多个同名参数,则返回最后一个的值。

default: 为设值未传name参数时返回的默认值,如若default也未设置,则会抛出 tornado.web.MissingArgumentError 异常。

strip: 表示是否过滤掉左右两边的空白字符,默认为过滤。

get_query_arguments(name, strip=True)

注意返回的是list列表(即使对应name参数只有一个值)

若未找到name参数,则返回空列表[]

获取请求体中参数

get_body_argument(name, default=_ARG_DEFAULT, strip=True)

从请求体中返回指定参数name的值,如果出现多个同名参数,则返回最后一个的值

default与strip同前,不再赘述

get_body_arguments(name, strip=True)

 一样返回的是列表

获取URL或请求体中参数

get_argument(name, default=_ARG_DEFAULT, strip=True)
get_arguments(name, strip=True)

正则定义参数

tornado中对于路由映射也支持正则提取uri

提取出来的参数会作为RequestHandler中对应请求方式的成员方法参数

若在正则表达式中定义了名字,则参数按名传递

若未定义名字,则参数按顺序传递。

提取出来的参数会作为对应请求方式的成员方法的参数

建议:提取多个值时最好用命名方式

request 中的其他属性

详细

    """A single HTTP request.

    All attributes are type `str` unless otherwise noted.

    .. attribute:: method

       HTTP request method, e.g. "GET" or "POST"

    .. attribute:: uri

       The requested uri.

    .. attribute:: path

       The path portion of `uri`

    .. attribute:: query

       The query portion of `uri`

    .. attribute:: version

       HTTP version specified in request, e.g. "HTTP/1.1"

    .. attribute:: headers

       `.HTTPHeaders` dictionary-like object for request headers.  Acts like
       a case-insensitive dictionary with additional methods for repeated
       headers.

    .. attribute:: body

       Request body, if present, as a byte string.

    .. attribute:: remote_ip

       Client's IP address as a string.  If ``HTTPServer.xheaders`` is set,
       will pass along the real IP address provided by a load balancer
       in the ``X-Real-Ip`` or ``X-Forwarded-For`` header.

    .. versionchanged:: 3.1
       The list format of ``X-Forwarded-For`` is now supported.

    .. attribute:: protocol

       The protocol used, either "http" or "https".  If ``HTTPServer.xheaders``
       is set, will pass along the protocol used by a load balancer if
       reported via an ``X-Scheme`` header.

    .. attribute:: host

       The requested hostname, usually taken from the ``Host`` header.

    .. attribute:: arguments

       GET/POST arguments are available in the arguments property, which
       maps arguments names to lists of values (to support multiple values
       for individual names). Names are of type `str`, while arguments
       are byte strings.  Note that this is different from
       `.RequestHandler.get_argument`, which returns argument values as
       unicode strings.

    .. attribute:: query_arguments

       Same format as ``arguments``, but contains only arguments extracted
       from the query string.

       .. versionadded:: 3.2

    .. attribute:: body_arguments

       Same format as ``arguments``, but contains only arguments extracted
       from the request body.

       .. versionadded:: 3.2

    .. attribute:: files

       File uploads are available in the files property, which maps file
       names to lists of `.HTTPFile`.

    .. attribute:: connection

       An HTTP request is attached to a single HTTP connection, which can
       be accessed through the "connection" attribute. Since connections
       are typically kept open in HTTP/1.1, multiple requests can be handled
       sequentially on a single connection.

    .. versionchanged:: 4.0
       Moved from ``tornado.httpserver.HTTPRequest``.
    """
取自 HTTPServerRequest 代码注释

常用

 RequestHandler.request  对象存储了关于请求的相关信息,具体属性有:

  • method    HTTP的请求方式,如GET或POST
  • host    被请求的主机名
  • uri    请求的完整资源标示,包括路径和查询字符串
  • path    请求的路径部分
  • query    请求的查询字符串部分
  • version    使用的HTTP版本
  • headers    请求的协议头,是类字典型的对象,支持关键字索引的方式获取特定协议头信息,例如:request.headers["Content-Type"]
  • body    请求体数据
  • remote_ip    客户端的IP地址
  • files    用户上传的文件,为字典类型

 tornado.httputil.HTTPFile 是接收到的文件对象,它有三个属性:

  • filename 文件的实际名字,与form_filename1不同,字典中的键名代表的是表单对应项的名字
  • body 文件的数据实体
  • content_type 文件的类型

这三个对象属性可以像字典一样支持关键字索引,如 request.files["form_filename1"][0]["body"] 

response 相关

write(chunk)

代码示例

class IndexHandler(RequestHandler):
    def get(self):
        self.write("hello itcast!")

支持多次写入如下

class IndexHandler(RequestHandler):
    def get(self):
        self.write("hello itcast 1!")
        self.write("hello itcast 2!")
        self.write("hello itcast 3!")

json 的写入, 会自动做序列化

class IndexHandler(RequestHandler):
    def get(self):
        stu = {
            "name":"zhangsan",
            "age":24,
            "gender":1,
        }
        self.write(stu)

注意, 如果是自己手动  json.dumps()  的方式写入时  Content-Type:text/html; charset=UTF-8 

自动序列化的时候则为  application/json; charset=UTF-8 

set_header(name, value)

可以手动设置一个名为name、值为value的响应头header字段

代码示例

import json
 
class IndexHandler(RequestHandler):
    def get(self):
        stu = {
            "name":"zhangsan",
            "age":24,
            "gender":1,
        }
        stu_json = json.dumps(stu)
        self.write(stu_json)
        self.set_header("Content-Type", "application/json; charset=UTF-8")

 set_default_headers()

该方法会在进入HTTP处理方法前先被调用,可以重写此方法来预先设置默认的headers

代码示例

class IndexHandler(RequestHandler):
    def set_default_headers(self):
        print "执行了set_default_headers()"
        # 设置get与post方式的默认响应体格式为json
        self.set_header("Content-Type", "application/json; charset=UTF-8")
        # 设置一个名为itcast、值为python的header
        self.set_header("itcast", "python")
 
    def get(self):
        print "执行了get()"
        stu = {
            "name":"zhangsan",
            "age":24,
            "gender":1,
        }
        stu_json = json.dumps(stu)
        self.write(stu_json)
        self.set_header("itcast", "i love python") # 注意此处重写了header中的itcast字段
 
    def post(self):
        print "执行了post()"
        stu = {
            "name":"zhangsan",
            "age":24,
            "gender":1,
        }
        stu_json = json.dumps(stu)
        self.write(stu_json)

set_status(status_code, reason=None)

为响应设置状态码。

参数说明:

  • status_code int类型,状态码,
  • reason string类型,描述状态码的词组,

redirect(url)

告知浏览器跳转到url

代码示例

class IndexHandler(RequestHandler):
    """对应/"""
    def get(self):
        self.write("主页")
 
class LoginHandler(RequestHandler):
    """对应/login"""
    def get(self):
        self.write('<form method="post"><input type="submit" value="登陆"></form>')
 
    def post(self):
        self.redirect("/")

send_error(status_code=500, **kwargs)

抛出HTTP错误状态码  status_code ,默认为500, kwargs 为可变命名参数

使用  send_error  抛出错误后, tornado会调用  write_error()  方法进行处理

并返回给浏览器处理后的错误页面

class IndexHandler(RequestHandler):
    def get(self):
        self.write("主页")
        self.send_error(404, content="出现404错误")

注意:默认的  write_error()  方法不会处理 send_error() 抛出的kwargs参数

即上面的代码中  content="出现404错误"  是没有意义的

write_error(status_code, **kwargs)

用来处理 send_error() 抛出的错误信息并返回给浏览器错误信息页面

可以重写此方法来定制自己的错误显示页面

class IndexHandler(RequestHandler):
    def get(self):
        err_code = self.get_argument("code", None) # 注意返回的是unicode字符串,下同
        err_title = self.get_argument("title", "")
        err_content = self.get_argument("content", "")
        if err_code:
            self.send_error(err_code, title=err_title, content=err_content)
        else:
            self.write("主页")
 
    def write_error(self, status_code, **kwargs):
        self.write(u"<h1>出错了,程序员GG正在赶过来!</h1>")
        self.write(u"<p>错误名:%s</p>" % kwargs["title"])
        self.write(u"<p>错误详情:%s</p>" % kwargs["content"])

接口与调用顺序

initialize()

对应每个请求的处理类 Handler 在构造一个实例后, 首先执行i此方法

路由映射中的第三个字典型参数会作为该方法的命名参数传递,如:

class ProfileHandler(RequestHandler):
    def initialize(self, database):
        self.database = database
 
    def get(self):
        ...
 
app = Application([
    (r'/user/(.*)', ProfileHandler, dict(database=database)),
    ])

此方法通常用来初始化参数(对象属性),很少使用

prepare()

预处理,即在执行对应请求方式的HTTP方法(如 get、pos t等)前先执行

ps: 不论以何种HTTP方式请求,都会执行此方法

代码示例

import json
 
class IndexHandler(RequestHandler):
    def prepare(self):
        if self.request.headers.get("Content-Type").startswith("application/json"):
            self.json_dict = json.loads(self.request.body)
        else:
            self.json_dict = None
 
    def post(self):
        if self.json_dict:
            for key, value in self.json_dict.items():
                self.write("<h3>%s</h3><p>%s</p>" % (key, value))
 
    def put(self):
        if self.json_dict:
            for key, value in self.json_dict.items():
                self.write("<h3>%s</h3><p>%s</p>" % (key, value))

HTTP方法

on_finish()

在请求处理结束后调用,即在调用HTTP方法后调用。通常该方法用来进行资源清理释放或处理日志等

注意:请尽量不要在此方法中进行响应输出

set_default_headers()

write_error()

调用顺序

测试代码

# coding:utf-8

import tornado.web
import tornado.ioloop
import tornado.httpserver
import tornado.options
from tornado.options import options
from tornado.web import RequestHandler


class IndexHandler(RequestHandler):

    def initialize(self):
        print("调用了initialize()")

    def prepare(self):
        print("调用了prepare()")

    def set_default_headers(self):
        print("调用了set_default_headers()")

    def write_error(self, status_code, **kwargs):
        print("调用了write_error()")

    def get(self):
        print("调用了get()")

    def post(self):
        print("调用了post()")
        self.send_error(200)  # 注意此出抛出了错误

    def on_finish(self):
        print("调用了on_finish()")


if __name__ == "__main__":
    tornado.options.parse_command_line()
    app = tornado.web.Application([
        (r"/", IndexHandler),
    ],
        debug=True)
    http_server = tornado.httpserver.HTTPServer(app)
    http_server.listen(8000)
    tornado.ioloop.IOLoop.current().start()

在正常情况未抛出错误时

调用顺序为:

在有错误抛出时

调用顺序为:

可看到   set_default_headers     initialize     prepare  这仨是雷打不动的最先执行

而  on_finish  必定最后执行,   set_default_headers  在出错的情况下回在 业务函数和  write_error  间在执行一次

Cookie 安全

Cookie是存储在客户端浏览器中的,很容易被篡改

Tornado提供了一种对Cookie进行简易加密签名的方法来防止Cookie被恶意篡改

使用安全Cookie需要为应用配置一个用来给Cookie进行混淆的秘钥cookie_secret

将其传递给Application的构造函数使其生效

写入配置

app = tornado.web.Application(
    [(r"/", IndexHandler),],
    cookie_secret = "2hcicVu+TqShDpfsjMWQLZ0Mkq5NPEWSk9fi0zsSt3A="
)

 

使用 cookie 

get_secure_cookie(name, value=None, max_age_days=31)

 

获取 cookie

 name  要获取的 cookie 名

 value  获取不到的默认值, 默认位 None

 max_age_day  过滤安全 cookie 的时间戳

set_secure_cookie(name, value, expires_days=30)

 

 name  要获取的 cookie 名

 value  获取不到的默认值, 默认位 None

 expires_days  设置浏览器中cookie的有效期

测试代码

# coding:utf-8

import tornado.web
import tornado.ioloop
import tornado.httpserver
import tornado.options
from tornado.web import RequestHandler


class IndexHandler(RequestHandler):
    def get(self):
        cookie = self.get_secure_cookie("count")
        count = int(cookie) + 1 if cookie else 1
        self.set_secure_cookie("count", str(count))
        self.write(
            '<html><head><title>Cookie计数器</title></head>'
            '<body><h1>您已访问本页%d次。</h1>' % count +
            '</body></html>'
        )


if __name__ == "__main__":
    tornado.options.parse_command_line()
    app = tornado.web.Application([
        (r"/", IndexHandler),
    ],
        debug=True,
        cookie_secret="2hcicVu+TqShDpfsjMWQLZ0Mkq5NPEWSk9fi0zsSt3A=")
    http_server = tornado.httpserver.HTTPServer(app)
    http_server.listen(8000)
    tornado.ioloop.IOLoop.current().start()

对比明文的保存。 这种机制的保存可以一定程度的具备安全性, 破解难度较高一些

如下是保存的  cookie 的具体内容

%222%7C1%3A0%7C10%3A1653532913%7C6%3A_count%7C4%3AMQ%3D%3D%7Cd22d828acb45404d252e2e9df76d56cf12e87ecc4d9c371e3a41c00a6d9b0f3b%22

 

其中的组成部分如下

 

用户验证

authenticated 装饰器

此装饰器装饰后的视图需要用户登录后才可以访问

class ProfileHandler(RequestHandler):
    @tornado.web.authenticated
    def get(self):
        self.write("这是我的个人主页。")

 

此装饰器是对于用户逻辑的验证则是通过   get_current_user  函数来实现

此函数若返回  True  或者 任何有意义的数据则表示验证通过

若返回  None  或者  False  则返回到  login_url  ( 因此需要设定  login_url  )

演示代码

# coding:utf-8

import tornado.web
import tornado.ioloop
import tornado.httpserver
import tornado.options
from tornado.web import RequestHandler


class LoginHandler(RequestHandler):
    def get(self):
        self.write("login ~~~~。")


class ProfileHandler(RequestHandler):
    def get_current_user(self):
        """在此完成用户的认证逻辑"""
        return True

    @tornado.web.authenticated
    def get(self):
        self.write("这是我的个人主页。")


if __name__ == "__main__":
    tornado.options.parse_command_line()
    app = tornado.web.Application([
        (r"/", ProfileHandler),
        (r"/login", LoginHandler),
    ],
        debug=True,
        login_url="/login",
    )
    http_server = tornado.httpserver.HTTPServer(app)
    http_server.listen(8000)
    tornado.ioloop.IOLoop.current().start()

 

听过调整  get_current_user  的返回值, 即可以实现登录态的判定, 以实现测试效果

异步处理

 

posted @ 2022-05-25 18:37  羊驼之歌  阅读(1280)  评论(0编辑  收藏  举报