zoukankan      html  css  js  c++  java
  • Torando 入门

    1. 前言

    Tornado 是使用 Python 编写的一个强大的、可拓展性的 Web 服务器/框架。与其他主流 Web 服务器框架有着明显区别:Tornado 支持异步非阻塞框架。同时它处理速度非常快,每秒钟可以处理数以千计的链接,是一个理想的 Web 框架。

    下载安装

    # pip安装
    pip3 install tornado
     
    # 源码安装
    tar xvzf tornado-4.4.1.tar.gz
    cd tornado-4.4.1
    python setup.py build
    sudo python setup.py install
    

    Tornado 主要模块

    # 主要模块
    web        # FriendFeed 使用的基础 Web 框架,包含了 Tornado 的大多数重要的功能
    escape     # XHTML, JSON, URL 的编码/解码方法
    database   # 对 MySQLdb 的简单封装,使其更容易使用
    template   # 基于 Python 的 web 模板系统
    httpclient # 非阻塞式 HTTP 客户端,它被设计用来和 web 及 httpserver 协同工作
    auth       # 第三方认证的实现(包括 Google、Facebook、Yahoo BBAuth、FriendFeed...)
    locale     # 针对本地化和翻译的支持
    options    # 命令行和配置文件解析工具,针对服务器环境做了优化
    
    # 底层模块
    httpserver # 服务于 web 模块的一个非常简单的 HTTP 服务器的实现
    iostream   # 对非阻塞式的 socket 的简单封装,以方便常用读写操作
    ioloop     # 核心的 I/O 循环
    

    2. 快速上手

    Tornado 请求生命周期

    • 程序启动:获取配置文件生成 URL 映射(根据映射找到对应的类处理请求),创建 socket 对象,将其添加到 epoll 中,然后循环监听 socket 对象变化。
    • 接收处理请求:接收客户端socket发送的请求(socket.accept),获取请求头信息,根据请求头获取请求 URL,然后根据路由映射关系匹配相关类。匹配成功就处理请求,处理完毕将响应返回给客户端,最后关闭 socket。

    2.1 牛刀小试

    一个简单的小示例:

    #!/usr/bin/env python
    # -*- coding:utf-8 -*-
    
    import tornado.ioloop
    import tornado.web
    
    
    class MainHandler(tornado.web.RequestHandler):
        # 请求方式(get、post、delete...)
        def get(self):
            self.write("Hello, world")
    
    # 路由映射
    application = tornado.web.Application([
        (r"/index", MainHandler),
    ])
    
    if __name__ == "__main__":
        application.listen(8888)    # 监听 8888 端口
        tornado.ioloop.IOLoop.instance().start()
    

    访问:http://127.0.0.1:8888/index,效果如下图所示:

    1.2 路由系统

    在 Django 中有 CBV 和 FBV 之分,url 可以对应函数也可以对应类,但是 Torando 一个 URL 对应一个类。

    处理请求、请求方法

    程序根据不同的 URL 正则匹配不同的类,并交付给 tornado.web.RequestHandler 的子类处理 子类处理。子类再根据请求方法调用不同的函数进行处理,最终将处理结果返回给浏览器。

    self.write("<h1>Hello, World</h1>")    # html代码直接写在浏览器客户端
    self.render("index.html")              # 返回html文件,调用render_string(),内部其实是打开并读取文件,返回内容
    self.redirect("http://www.baidu.com",permanent=False) # 跳转重定向,参数代表是否永久重定向
    
    name = self.get_argument("name")       # 获取客户端传入的参数值
    name = self.get_arguments("name")      # 获取多个值,类别形式
    file = self.request.files["filename"]  # 获取客户端上传的文件
    
    raise tornado.web.HTTPError(403)       # 返回错误信息给客户端
    

    重写 tornado.web.RequestHandlerself.initialize() 函数

    tornado.web.RequestHandler 构造函数 init() 中有一个 self.initialize 函数,它是一个 Tornado 框架提交的 钩子函数,用于子类初始化,为每个请求调用。

    在后面自定义 session 可以用到。

    1.3 模板系统

    1.3.1 静态文件及模板配置

    # 配置静态文件和模板文件
    settings = {
        'static_path': 'static',        # 静态文件目录名
        'static_url_prefix': '/static/',  # 静态文件 url 前缀,可以是其他
        'template_path': 'templates',
    
    }
    
    
    # 生成路由规则
    application = tornado.web.Application([
        # (r"/index", MainHandler),
        # (r'/login', LoginHandler),
    ], **settings)
    

    静态文件缓存的实现

    def get_content_version(cls, abspath):
        """Returns a version string for the resource at the given path.
    
            This class method may be overridden by subclasses.  The
            default implementation is a hash of the file's contents.
    
            .. versionadded:: 3.1
            """
        data = cls.get_content(abspath)
        hasher = hashlib.md5()
        if isinstance(data, bytes):
            hasher.update(data)
        else:
            for chunk in data:
                hasher.update(chunk)
        return hasher.hexdigest()
    

    1.3.2 模板语言

    Tornado 模板语言类似于 Django 的模板语言,很多语法都相似。如:控制语句都用 % % 包裹、表达语句都用 {{ }} 包裹等等。

    1、简单示例

    app.py

    #!/usr/bin/env python
    # -*- coding:utf-8 -*-
    
    import tornado.ioloop
    import tornado.web
    
    
    class MainHandler(tornado.web.RequestHandler):
        def get(self):
            self.render("index.html", list_info=[11, 22, 33])
    
    settings = {
        'static_path': 'static',        # 静态文件目录名
        'static_url_prefix': '/static/',  # 静态文件 url 前缀,可以是其他
        'template_path': 'templates',
    
    }
    
    application = tornado.web.Application([
        (r"/index", MainHandler),
    ], **settings)
    
    if __name__ == "__main__":
        application.listen(8888)
        tornado.ioloop.IOLoop.instance().start()
    

    index.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
        {{ list_info }}
    
        <p>{{ list_info[0] }}</p>
        {% for item in list_info %}
            <li>{{ item }}</li>
        {% end %}
    </body>
    </html>
    

    Tips:

    • Tornado 支持 if、for、while 和 try 等语句,以 {% end %} 结尾,区别于 Django
    • 取列表或元组之类中单独某个元素时,与 Python 取法类似:{{ list_info[0] }},Django:{{ list_info.0 }}
    • Tornado 也支持模板继承,通过 extendsblock 实现
    • 跨站请求伪造:form 表单提交时使用{% raw xsrf_form_html() %},类似于 Django 的 csrf_token

    Tornado 在模板中默认提供了一些函数、字段、类

    这些函数、字段或类可以直接拿来使用,不需要再定义:

    escape          # tornado.escape.xhtml_escape 的別名
    xhtml_escape    # tornado.escape.xhtml_escape 的別名
    url_escape      # tornado.escape.url_escape 的別名
    json_encode     # tornado.escape.json_encode 的別名
    squeeze         # tornado.escape.squeeze 的別名
    linkify         # tornado.escape.linkify 的別名
    datetime        #  Python 的 datetime 模组
    handler         # 当前的 RequestHandler 对象
    request         # handler.request 的別名
    current_user    # handler.current_user 的別名
    locale          # handler.locale 的別名
    _               # handler.locale.translate 的別名
    static_url      # for handler.static_url 的別名
    xsrf_form_html  # handler.xsrf_form_html 的別名
    
    

    比如 static_url() 函数可以用来找到静态文件:

    <link rel="stylesheet" href="{{ static_url('css/hj.css') }}">
    
    

    自定义函数

    Tornado 模板系统还支持自定义函数,可以像 Python 一样传入参数使用:

    app.py

    def func(name):
        return 'Hello, ' + name
    
    class MainHandler(tornado.web.RequestHandler):
        def get(self):
            # h = '<h1>raw 转义</h1>'
            # self.render("index.html", **{'h': '<h1>raw 转义</h1>', 'list_info': "[11, 22, 33]"})
    
            person_list = [
                {
                    'name': 'rose',
                    'age': 18,
                    'length': 170
                }
            ]
            self.render('index.html', person_list=person_list, func=func)
    
    
    {% for person in person_list %}
            <li>{{ func(person['name']) }}</li>
    {% end %}
    
    

    关于 Tornado 中转义

    后台带有 <、> 传到前端模板中会被转义为:&lt;、&gt; 等,要想不被转义,而输出的是原始字符串,可以使用以下三种方法:

    • 使用 {% raw 字符串 %}
    • 整个程序关闭转义功能:在 Application 构造函数中传递 autoescape=None 即可被关闭
    • 每个模板中关闭转义功能:在模板中添加 {% autoescape None %} 即可关闭

    对于已经关闭了转义功能的模板文件,若想对特殊字段转义,可以在模板文件中使用 escape() 函数:{{ escape(字符串) }}

    h = '<h1>raw 转义</h1>'
    
    self.render('index.html', h=h)
    
    # <p>{{ h }}</p>
    # <p>{% raw h %}</p>
    
    # {% autoescape None %}
    
    # {{ escape(h) }}
    
    

    Tips:

    • Firefox 中会直接弹出 alert 窗口
    • Chrome 中需要在 self.write() 前添加响应头 set_header("X-XSS-Protection", 0) 解决

    1.3.2 模板继承

    类似于 Django 模板继承,也是使用 extendsblock 来实现模板继承:

    母版 base.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
        {% block css %} {% end %}
    </head>
    <body>
        <div class="page-container">
            <h1>模板继承</h1>
        </div>
        
        {% block content %} {% end %}
    
        {% block js %} {% end %}
    </body>
    </html>
    
    

    子模板 index.html

    {% extends 'base.html' %}
    
    {% block content %}
        <h1>子模板</h1>
    
    {% end %}
    
    

    1.3.3 include 引入其他组件

    其他 HTML 组件

    <div>
        <p>模板继承</p>
        <ul>
            <li>extends</li>
            <li>include</li>
        </ul>
    </div>
    
    

    index.html

    {% extends 'base.html' %}
    
    {% block content %}
        <h1>子模板</h1>
        {% include 'test.html' %}
    {% end %}
    
    

    1.3.4 UIMothods 和 UIModules

    使用 UIMothodsUIModules,可以使得在模板文件中也可以调用执行 Python 的类和函数。分为以下几个步骤:

    • 定义两个 py 文件,在其中定义好要执行的函数或类
    • app.py 中注册上一步定义的 py 文件
    • 在模板中调用

    uimethods.py

    def tab(self):
        return 'UIMethod'
    
    

    uimodules.py

    from tornado.web import UIModule
    from tornado import escape
    
    class custom(UIModule):
    
        def render(self, *args, **kwargs):
            return escape.xhtml_escape('<h1>UIModules</h1>')
    
    

    app.py

    #!/usr/bin/env python
    # -*- coding:utf-8 -*-
    
    import tornado.ioloop
    import tornado.web
    import uimethods as mt
    import uimodules as md
    
    
    class MainHandler(tornado.web.RequestHandler):
        def get(self):
    
            self.render('index.html')
    
    settings = {
        'static_path': 'static',        # 静态文件目录名
        'static_url_prefix': '/static/',  # 静态文件 url 前缀,可以是其他
        'template_path': 'templates',
        'ui_methods': mt,
        'ui_modules': md
    }
    
    application = tornado.web.Application([
        (r"/index", MainHandler),
    ], **settings)
    
    
    if __name__ == "__main__":
        application.listen(8888)
        tornado.ioloop.IOLoop.instance().start()
    
    

    index.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    
    </head>
    <body>
          <h1>hello</h1>
          <p>{% module custom(123) %}</p>
          <p>{{ tab() }}</p>
    </body>
    </html>
    
    

    运行结果如下图:

    3. Cookie

    1、操作 cookie

    • get_cookie():获取 cookie
    • set_cookie():设置 cookie
    • clear_cookie():去除 cookie,一般用于登出设置
    class MainHandler(tornado.web.RequestHandler):
        def get(self):
            if not self.get_cookie("mycookie"):
                self.set_cookie("mycookie", "myvalue")
                self.write("您的Cookie尚未设置!")
            else:
                self.write("你的cookie已经设定好了!")
    
    

    2、加密 cookie

    所谓加密 cookie,即已经签名过防止伪造的 cookie,一般情况我们都会在 cookie 中存储登录用户的 ID 等个人信息。但是 cookie 也容易被恶意客户端伪造,从而攻陷服务端,所以对 cookie 进行必要的加密是很有必要的。

    Tornado 通过 set_secure_cookieget_secure_cookie 方法直接支持了这种功能。 要使用这些方法,你需要在创建应用时提供一个密钥,名字为 cookie_secret。 你可以把它作为一个关键词参数传入应用的设置中:

    class MainHandler(tornado.web.RequestHandler):
        def get(self):
            if not self.get_secure_cookie("mycookie"):
                self.set_secure_cookie("mycookie", "myvalue")
                self.write("您的Cookie尚未设置!")
            else:
                self.write("你的cookie已经设定好了!")
                 
    application = tornado.web.Application([
        (r"/", MainHandler),
    ], cookie_secret="61oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=")	# 提供的密匙
    
    

    签名 Cookie 的本质是:

    写cookie过程:

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

    读cookie过程:

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

    app.py

    # !/usr/bin/env python
    # -*- coding:utf-8 -*-
    
    import tornado.ioloop
    import tornado.web
    
    class BaseHandler(tornado.web.RequestHandler):
        def get_current_user(self):
            return self.get_secure_cookie("login_user")		# 获取 cookie
    
    
    class LoginHandler(BaseHandler):
        def get(self):
            self.render('login.html')
    
        def post(self):
            username = self.get_argument('user')
            password = self.get_argument('password')
            print(username, password)
            if username == 'rose' and password == '123':
                self.set_secure_cookie('login_user', username)		# 设置 cookie
                self.redirect('/')
            else:
                self.render('login.html', **{'status': '用户名或密码错误'})
    
    
    class WelcomeHandler(BaseHandler):
        @tornado.web.authenticated		# 用户认证装饰器
        def get(self):
            self.render('index.html', user=self.current_user)
    
    
    class LogoutHandler(BaseHandler):
        def get(self):
            if (self.get_argument("logout", None)):
                self.clear_cookie("login_user")
                self.redirect("/")
    
    
    settings = {
        'template_path': 'templates',
        'static_path': 'static',
        'static_url_prefix': '/static/',
        'cookie_secret': 'aiuasdhflashjdfoiuashdfiuh',
        "login_url": "/login",
        "xsrf_cookies": True,		# 相当于 Django 的 csrf_token
    }
    
    application = tornado.web.Application([
        (r'/', WelcomeHandler),
        (r"/login", LoginHandler),
        (r"/logout", LogoutHandler),
    ], **settings)
    
    if __name__ == "__main__":
        application.listen(8888)
        tornado.ioloop.IOLoop.instance().start()
    
    

    login.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
        <form action="/login" method="post">
            {% raw xsrf_form_html() %}
            <p><input type="text" name="user"></p>
            <p><input type="password" name="password"></p>
            <p><input type="submit" value="登录"></p>
        </form>
    </body>
    </html>
    
    

    index.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    
    </head>
    <body>
          <h3>Welcome back, {{ user }}</h3>
    </body>
    </html>
    
    

    3.2 Torando 自带认证功能 authenticated 装饰器

    authenticated 可以帮助我们认证当前用户是否登录,依赖于 login_urlcurrent_user 两个属性。

    • 当前用户未登录:current_user 为 False,否则为设置的 cookie
    • 对于未登录的用户:会被定位到 login_url 指定的 URL,非法 post 请求将返回 403 Forbidden HTTP 响应

    参考文章:tornado系列:用cookie进行用户验证

    Cookie 存储在浏览器端,因此也可以使用 JavaScript 操作 Cookie,下面是一个如何设置 Cookie 过期的小示例:

    /* 设置cookie,指定秒数过期  */
    /* 参数:
    domain   指定域名下的cookie
    path       域名下指定url中的cookie
    secure    https使用
    */
    
    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();
    }
    
    

    更多有关于 jQuery 操作 Cookie的方法:jQuery Cookie

    4. 跨站请求伪造

    Tornado 中跨站请求伪造(CSRF)类似于 Django,不过需要事先在 settings 配置好:

    app.py

    settings = {
        "xsrf_cookies": True,
    }
    application = tornado.web.Application([
        (r"/", MainHandler),
        (r"/login", LoginHandler),
    ], **settings)
    
    

    使用:index.html

    <form action="/login" method="post">
      {{ xsrf_form_html() }}			<!--生成一个这样的 input 标签:<input type="hidden" name="_xsrf" value="2|dde24a6d|ad9cb54babbe1bb9b43f4360867b2ff3|1559549174">-->
      <input type="text" name="user"/>
      <input type="submit" value="提交"/>
    </form>
    
    

    使用 Ajax 发送 post 请求时:

    /*获取本地 cookie,再携带 cookie 发送请求*/
    function getCookie(name) {
        var r = document.cookie.match("\b" + name + "=([^;]*)\b");
        return r ? r[1] : undefined;
    }
    
    jQuery.postJSON = function(url, args, callback) {
        args._xsrf = getCookie("_xsrf");
        $.ajax({url: url, data: $.param(args), dataType: "text", type: "POST",
            success: function(response) {
            callback(eval("(" + response + ")"));
        }});
    };
    
    

    5. 自定义 session

    session其实就是定义在服务器端用于保存用户回话的容器,其必须依赖 cookie 才能实现。下面我们来自定义一个内存级别 session,当然也可以将其存储到 reids 中。

    有关 cookies 的方法:

    get_cookie()        # 获取 cookie
    set_cookie()        # 设置 cookie
    
    

    tornado.web.RequestHandler 构造函数 init() 中有一个 self.initialize 函数,它是一个 Tornado 框架提交的 钩子函数,用于子类初始化,为每个请求调用。

    这里为了避免每个类都要定义 initialize(),我们定义了一个 BaseHandler(),要使用的类直接继承即可。

    app.py

    import tornado.ioloop
    import tornado.web
    # import tornado
    from session import Session
    
    
    class BaseHandler:
        def initialize(self):
            self.session = Session(self)
    
            super(BaseHandler, self).initialize()
    
    
    class MainHandler(BaseHandler, tornado.web.RequestHandler):
    
        def get(self, *args, **kwargs):
            temp = self.session.get_value('is_login')
            if temp:
                self.write("Hello, world")
            else:
                self.redirect('/login')
    
    
    class LoginHandler(BaseHandler, tornado.web.RequestHandler):
        """
        LoginHandler  和 MainHandler 都没有 init 构造方法,那就从它的父类中找
        它的父类中有一个 initialize() 方法,是一个钩子函数
        用于子类初始化,为每个请求调用,因此在执行调用这两个类时,会事先调用执行 initialize() 方法
        我们可以用来获取或设置用户的 cookies
        """
        def get(self, *args, **kwargs):
    
            self.render('login_session.html')
    
        def post(self, *args, **kwargs):
            user = self.get_body_argument('user')       # rose/123  获取表单中的值
            pwd = self.get_body_argument('password')
            print(user, pwd)
    
            if user == 'root' and pwd == '123':
                print(self.session.get_value('is_login'))
                self.session.set_value('is_login', True)
                self.redirect('/index')      # 重定向
            else:
                self.redirect('/login')
    
    # 配置静态文件和模板文件
    settings = {
        'static_path': 'static',        # 静态文件目录名
        'static_url_prefix': '/static/',  # 静态文件 url 前缀,可以是其他
        'template_path': 'templates',
    
    }
    
    
    # 生成路由规则
    application = tornado.web.Application([
        (r"/index", MainHandler),
        (r'/login', LoginHandler),
    ], **settings)
    
    
    if __name__ == "__main__":
        # 产生 socket 对象
        # 并将 socket 对象添加到 select 或 epoll(Linux)中,进行监听
        application.listen(8888)        # listen() 方法还可以指定 ip 和端口
    
        # 无限循环监听 socket 对象文件句柄是否发生变化
        tornado.ioloop.IOLoop.instance().start()
    
    

    session.py

    import uuid
    
    # 存放 session
    
    # container = {
    #     '97470544-215a-4837-b904-eeee4b6974fa': {'is_login': True},
    #     '随机字符串2': {'xxx': 'xxxxxxx'},
    # }
    
    
    class Session(object):
        container = {}
    
        def __init__(self, handler):
            """
            获取或设置用户 session
            :param handler: 为调用 Session() 的类的实例对象,如:LoginHandler() 的实例对象
            """
            # self.container = {}
    
            # 获取 cookie
            nid = handler.get_cookie('session_id')     # 相当于 self.get_cookie()
            print(Session.container)
            print('nid>>>>>>>>>>>>', nid)
            if nid:
                if nid in Session.container:
                    pass
                else:
                    nid = str(uuid.uuid4())
                    Session.container[nid] = {}
            else:
                nid = str(uuid.uuid4())
                Session.container[nid] = {}
    
            handler.set_cookie('session_id', nid, max_age=1000)
    
            self.nid = nid
            self.handler = handler
    
        def set_value(self, key, value):
            """
            设置 session
            :param key:
            :param value:
            :return:
            """
            Session.container[self.nid][key] = value       # container['97470544-215a-4837-b904-eeee4b6974fa']['is_login] = True
    
        def get_value(self, key):
            """
            获取 session
            :param key:
            :return:
            """
            # print(self.container)
            return Session.container[self.nid].get(key)        # container['97470544-215a-4837-b904-eeee4b6974fa'].get('is_login)
    
    

    使用 __getitem__、__setitem__、__delitem__ 进行改造

    在上面 Session() 我们定义了两个函数 get_key()set_value() 来获取和设置 cookie。下面来看看我们自定义的 session 框架和 Django 内置的有什么区别:

    # 自定制
    self.session.get_value('is_login')
    self.session.set_value('is_login', True)
    
    # Django
    sesiion['is_login'] = True
    session.get('is_login')
    
    

    要想实现和 Django 一样的效果,就需要用到面向对象中的 __getitem__、__setitem__、__delitem__ 三个方法,现在我们来修改下Session`:

    def __getitem__(self, item):
    
        return Session.container[self.nid].get(item)
    
    def __setitem__(self, key, value):
        Session.container[self.nid][key] = value
    
    def __delitem__(self, key):
        del Session.container[self.nid][key]
    
    

    将原来的 get_value()、set_value() 换成上面三个类的内置方法:

    • 当类对象进行获取操作时会触发 __getitem__
    • 当类对象进行修改操作时会触发 __setitem__
    • 当类对象进行删除操作时会触发 __delitem__

    下面再来修改下 index.py,改变其获取设置 cookie 的方式:

    class MainHandler(BaseHandler, tornado.web.RequestHandler):
    
        def get(self, *args, **kwargs):
            # temp = self.session.get_value('is_login')
    
            # 修改为这句
            temp = self.session['is_login'] 
            if temp:
                self.write("Hello, world")
            else:
                self.redirect('/login')
    
    
    
    class LoginHandler(BaseHandler, tornado.web.RequestHandler):
        
        ...
       
        def post(self, *args, **kwargs):
           ...
    
            if user == 'root' and pwd == '123':
                # print(self.session.get_value('is_login'))
                # self.session.set_value('is_login', True)
               
               # 修改为以下这句
               self.session['is_login'] = True
                self.redirect('/index')      # 重定向
            else:
                self.redirect('/login')
    
    

    将 session 存储到 redis 中

    import uuid
    import redis
    
    
    class RedisSession(object):
       # container = {
           #  '97470544-215a-4837-b904-eeee4b6974fa': {'is_login': True},
            #     '随机字符串2': {'xxx': 'xxxxxxx'},
       # }
    
        def __init__(self, handler):
            # 获取用户cookie,如果有,不操作,否则,给用户生成随即字符串
            # 写给用户
            # 保存在session
            r = redis.StrictRedis(host='192.168.21.128', port=6379, db=0)
            keys = r.keys()
    
    
            nid = handler.get_cookie('session_id')      # 465e0198-9ce5-4ae4-9768-d6d4803e7b86
            if nid:
                if nid in keys:
                    pass
                else:
                    nid = str(uuid.uuid4())
                    r.hset(nid, 'is_login', 1)
            else:
                nid = str(uuid.uuid4())
                r.hset(nid, 'is_login', 1)
    
            handler.set_cookie('session_id', nid, max_age=1000)
            # nid当前访问用户的随即字符串
            self.nid = nid
            # 封装了所有用户请求信息
            self.handler = handler
    
        def __getitem__(self, item):
    
            return RedisSession.container[self.nid].get(item)
    
        def __setitem__(self, key, value):
            RedisSession.container[self.nid][key] = value
    
        def __delitem__(self, key):
            del RedisSession.container[self.nid][key]
    
    

    6. 自定制 Form 表单验证

    学习过 Django 的朋友应该都知道,Django 内置有 Form 表单组件,可以用来生成表单,表单验证等等。而 tornado 没有自带 Form 表单验证,需要我们自定制。

    下面我们来模拟自定制一个简单的 Form 表单验证;

    • 在这里只定制 StringFieldEmailField 两种类型字段
    • 表单类型只有最简单的 input 文本框,当然还有更多的,如:select、textarea、checkbox 等,就需要做更多的定制

    app.py

    import tornado.ioloop
    import tornado.web
    import re
    
    
    class StringField:
        def __init__(self, name):
            self.regex = '^w+$'
            self.name = name
            self.error = ''
            self.value = ''
    
        def __str__(self):
            return "<input type='text' name='%s' value='%s'>" % (self.name, self.value)
    
    
    class EmailField:
        def __init__(self, name):
            self.regex = '^w+@.*$'
            self.name = name
            self.error = ''
            self.value = ''     # 编辑/修改表单时,保留原有值
    
        def __str__(self):
            return "<input type='text' name='%s' value='%s'>" % (self.name, self.value)
    
    
    class LoginForm:
        def __init__(self):
            self.user = StringField(name='user')
            self.email = EmailField(name='email')
    
        def is_valid(self, handler):
            cleaned_data = {}
            flag = True
    
            for k, v in self.__dict__.items():
                # k=user, v=<__main__.StringField object at 0x000001EC6BE33E80>
                # k=email v=  <__main__.EmailField object at 0x000001EC6BE33EB8>
                # user ^w+$、email ^w+@.*$
    
                temp = handler.get_body_argument(k)
                v.value = temp      # 赋值
    
                result = re.match(v.regex, temp)
                if result:
                    cleaned_data[k] = temp
                else:
                    v.error = '%s 错误' % k
                    flag = False
    
            return flag, cleaned_data           # True {'user': 'rose', 'email': 'john@qq.com'}
    
    
    class LoginHandler(BaseHandler, tornado.web.RequestHandler):
    
        def get(self, *args, **kwargs):
            obj = LoginForm()
    
            self.render('login.html', **{'obj': obj})
    
        def post(self, *args, **kwargs):
            obj = LoginForm()
            obj.is_valid(self)
    
            flag, cleaned_data = obj.is_valid(self)
            if flag:
                user = cleaned_data.get('user')
                email = cleaned_data.get('email')
                
                if user == 'root' and email== '123@qq.com':
                    self.redirect('/index')      # 重定向
            else:
                self.render('login.html', **{'obj': obj})
    
    

    前端 login.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
        <link rel="stylesheet" href="/static/css/test.css">
    </head>
    <body>
    <h1>Tornado 框架</h1>
    
    <form action="/login" method="post">
        <p>{% raw obj.user %} {{ obj.user.error }}</p>
        <p>{% raw obj.email %} {{ obj.email.error }}</p>
    
        <input type="submit" value="提交">
    </form>
    </body>
    </html>
    
    

    Tips: 千万不能忘记 raw !!!


    用到知识点

    • __str__:用来定制对象字符串显示形式
    • __dict__:类、类对象属性字典
    • 基于Python实现的支持多个WEB框架的 Form表单验证组件:Tyrion中文文档(含示例源码)

    7. 上传文件

    7.1 Form 表单上传

    index.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    
    </head>
    <body>
         <form id="my_form" name="form" action="/index" method="POST"  enctype="multipart/form-data" >
            <input name="file" id="my_file"  type="file" />
            <input type="submit" value="提交"  />
        </form>
    </body>
    </html>
    
    

    app.py

    #!/usr/bin/env python
    # -*- coding:utf-8 -*-
    
    import tornado.ioloop
    import tornado.web
    
    
    class MainHandler(tornado.web.RequestHandler):
        def get(self):
    
            self.render('index.html')
    
        def post(self, *args, **kwargs):
            file_metas = self.request.files["file"]
            print(file_metas)
            """file_metas = 
             [{'filename': '1.jpg', 'body': b'xffxd8xffxe0x00x10JFIFx00x01...x1fxffxd9', 
            'content_type': 'image/jpeg'}]
    
            """
            for meta in file_metas:
                file_name = meta['filename']
                with open(file_name, 'wb') as up:
                    up.write(meta['body'])
    
    settings = {
        'template_path': 'templates',
    }
    
    application = tornado.web.Application([
        (r"/index", MainHandler),
    ], **settings)
    
    
    if __name__ == "__main__":
        application.listen(8000)
        tornado.ioloop.IOLoop.instance().start()
    
    

    总结

    request.files["name"]:获取上传文件对象(保存有文件名、文件二进制信息、文件类型等信息)

    7.2 Ajax 上传

    1、XMLHttpRequest

    <input type="file" id="img" />
    <input type="button" onclick="UploadFile();" />
    <script>
        function UploadFile(){
            var fileObj = document.getElementById("img").files[0];
    
            var form = new FormData();
            form.append("k1", "v1");
            form.append("fff", fileObj);
    
            var xhr = new XMLHttpRequest();
            xhr.open("post", '/index', true);
            xhr.send(form);
        }
    </script>
    
    

    2、iframe

    <form id="my_form" name="form" action="/index" method="POST"  enctype="multipart/form-data" >
        <div id="main">
            <input name="fff" id="my_file"  type="file" />
            <input type="button" name="action" value="Upload" onclick="redirect()"/>
            <iframe id='my_iframe' name='my_iframe' src=""  class="hide"></iframe>
        </div>
    </form>
    
    <script>
        function redirect(){
            document.getElementById('my_iframe').onload = Testt;
            document.getElementById('my_form').target = 'my_iframe';
            document.getElementById('my_form').submit();
    
        }
    
        function Testt(ths){
            var t = $("#my_iframe").contents().find("body").text();
            console.log(t);
        }
    </script>
    
    

    3、jQuery

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    
    </head>
    <body>
    <input type="file" id="img"/>
    <input type="button" onclick="UploadFile();"/>
    
    
    <script src="{{ static_url('js/jquery-3.1.1.js') }}"></script>
    
    <script>
        function UploadFile() {
            var fileObj = $("#img")[0].files[0];
    //        console.log($("#img")[0].files[0]);
            var form = new FormData();
            form.append("k1", "v1");
            form.append("fff", fileObj);
    
            $.ajax({
                type: 'POST',
                url: '/index',
                data: form,
                processData: false,  // tell jQuery not to process the data
                contentType: false,  // tell jQuery not to set contentType
                success: function (arg) {
                    console.log(arg);
                }
            })
        }
    </script>
    </body>
    </html>
    
    

    4、app.py

    #!/usr/bin/env python
    # -*- coding:utf-8 -*-
    
    import tornado.ioloop
    import tornado.web
    
    
    class MainHandler(tornado.web.RequestHandler):
        def get(self):
    
            self.render('index.html')
    
        def post(self, *args, **kwargs):
            file_metas = self.request.files["fff"]
            # print(file_metas)
            for meta in file_metas:
                file_name = meta['filename']
                with open(file_name,'wb') as up:
                    up.write(meta['body'])
    
    settings = {
        'static_path': 'static',
        'static_url_prefix': '/static/',
        'template_path': 'templates',
    }
    
    application = tornado.web.Application([
        (r"/index", MainHandler),
    ], **settings)
    
    
    if __name__ == "__main__":
        application.listen(8000)
        tornado.ioloop.IOLoop.instance().start()
    
    

    8. 异步非阻塞

    Tornado 不仅仅是个同步 Web 框架,同时也是一个非常有名的异步非阻塞框架(与 Node.js 一样),下面我们就探讨下如何基本使用异步非阻塞。

    8.1 基本使用

    #!/usr/bin/env python
    # -*- coding:utf-8 -*-
    
    import tornado.ioloop
    import tornado.web
    from tornado import gen
    from tornado.concurrent import Future
    
    
    class AsyncHandler(tornado.web.RequestHandler):
        @gen.coroutine
        def get(self):
            future = Future()
            future.add_done_callback(self.doing)	# 请求成功,回调函数
            yield future
            # 或
            # tornado.ioloop.IOLoop.current().add_future(future,self.doing)
            # yield future
    
        def doing(self, *args, **kwargs):
            self.write('async')
            self.finish()
    
    application = tornado.web.Application([
        (r"/index", AsyncHandler),
    ])
    
    
    if __name__ == "__main__":
        application.listen(8000)
        tornado.ioloop.IOLoop.instance().start()
    
    

    访问:http://127.0.0.1:8000/index 时发现页面一直在转动,并未请求成功,连接也未断开。这是因为处理 get 请求的 函数被 @gen.coroutine 装饰(内部是一个协程),且 yield 了一个 Future 对象。该对象只有用户向其发送信号或者放置数据时,才会 “放行”,不然会一直等待。

    8.2 同步与异步非阻塞对比

    同步:

    class SyncHandler(tornado.web.RequestHandler):
    
        def get(self):
            self.doing()
            self.write('sync')
    
        def doing(self):
            time.sleep(10)
    
    

    异步:

    class AsyncHandler(tornado.web.RequestHandler):
        @gen.coroutine
        def get(self):
            future = Future()
            tornado.ioloop.IOLoop.current().add_timeout(time.time() + 5, self.doing)
            yield future
    
    
        def doing(self, *args, **kwargs):
            self.write('async')
            self.finish()
    
    

    8.3 httpclient类库

    使用 httpclient 类库用于发送 HTTP 请求,当浏览器向 Tornado 服务端发送请求时,其内部也会向别的地址发送 HTTP 请求。什么时候请求回来,就什么时候响应浏览器发送的请求。

    浏览器访问:http://127.0.0.1:8888/async,向服务器发起 get 请求,服务器内部通过 httpclient 向 Google 发送请求:

    import tornado.web
    import tornado.ioloop
    from tornado import gen
    from tornado import httpclient
    
    
    # 方式一:
    class AsyncHandler(tornado.web.RequestHandler):
        @gen.coroutine
        def get(self, *args, **kwargs):
            http = httpclient.AsyncHTTPClient()
            data = yield http.fetch("http://www.google.com")
            print('finished...', data)
            self.finish('请求成功~')
    
    
    # 方式二:
    # class AsyncHandler(tornado.web.RequestHandler):
    #     @gen.coroutine
    #     def get(self):
    #         http = httpclient.AsyncHTTPClient()
    #         yield http.fetch("http://www.google.com", self.done)
    #
    #     def done(self, response):
    #         print('finished...')
    #         self.finish('666')
    
    
    
    application = tornado.web.Application([
        (r"/async", AsyncHandler),
    ])
    
    if __name__ == "__main__":
        application.listen(8888)
        tornado.ioloop.IOLoop.instance().start()
    
    

    因为国内无法正常访问:http://www.google.com,所有一段时间后,得到:500: Internal Server Error 的结果。

  • 相关阅读:
    HDU 3835 R(N)
    HDU 2498 Digits
    HUST 1027 Enemy Target!
    【POJ 3714】 Raid
    【POJ 2965】 The Pilots Brothers' refrigerator
    【POJ 2054】 Color a Tree
    【POJ 1328】 Radar Installation
    【POJ 3190】 Stall Reservations
    【POJ 3614】 Sunscreen
    【BZOJ 3032】 七夕祭
  • 原文地址:https://www.cnblogs.com/midworld/p/11076015.html
Copyright © 2011-2022 走看看