Tornado 基本操作

讲师的博客:
白话tornado源码系列5篇,主要是源码剖析暂时不需要知道那么多。只要看下第一篇就好:
https://www.cnblogs.com/wupeiqi/tag/Tornado/
Web框架之Tornado:
https://www.cnblogs.com/wupeiqi/p/5702910.html

Hello World

经典的 hello world 示例:

import tornado.web

# 视图
class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("Hello World.")

# 路由
application = tornado.web.Application([
    (r"/", MainHandler),
    (r"/hello", MainHandler),
])

if __name__ == '__main__':
    import tornado.ioloop
    application.listen(8000)
    tornado.ioloop.IOLoop.instance().start()

整个过程其实就是在创建一个socket服务端并监听8000端口。当请求到来时,根据请求中的url和请求方式(post、get或put等)来指定相应的类中的方法来处理本次请求。在上述示例中 url 在路由系统匹配到时,则服务器会给浏览器返回 Hello World ,否则返回 404: Not Found(tornado内部定义的值), 即完成一次http请求和响应。

模板引擎

Tornao中的模板语言和django中类似。模板引擎将模板文件载入内存,然后将数据嵌入其中,最终获取到一个完整的字符串,再将字符串返回给请求者。
不过还是有区别的。Tornado 的模板支持“控制语句”和“表达语句”,控制语句是使用 {% 和 %} 包起来的。例如 {% if len(items) > 2 %}。表达语句是使用 {{ 和 }} 包起来的,例如 {{ items[0] }} 。
控制语句和对应的 Python 语句的格式基本完全相同。支持 if、for、while 和 try,这些语句逻辑结束的位置需要用 {% end %} 做标记。还通过 extends 和 block 语句实现了模板继承。这些在 template 模块的代码文档中有着详细的描述。在使用模板前需要在setting中设置模板路径:"template_path" : "tpl"
使用模板引擎的简单示例,后端代码:

import tornado.web

class IndexHandler(tornado.web.RequestHandler):
    def get(self):
        self.render("index.html", k1='v1', k2='v2')  # k1和k2是传给模板引擎处理的内容

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

if __name__ == '__main__':
    import tornado.ioloop
    application.listen(8000)
    tornado.ioloop.IOLoop.instance().start()

前端代码,模板语言的使用:

<body>
<h1>Hello World</h1>
<h3>{{ k1 }}</h3>
{% if k2 == 'v2' %}
    <h3>k2 == v2</h3>
{% else %}}
    <h2>k2 != v2</h2>
{% end %}
</body>

加载配置

上面的前端代码,最好是统一保存在某个目录里,比如新建个tpl目录来存放。把html文件移过去之后,现在render()方法就找不到这个文件了。当然可以改一下参数,把目录名加进去。不过推荐的做法是把tpl目录加到配置里去,对上面的代码进去修改,加入配置信息:

class IndexHandler(tornado.web.RequestHandler):
    def get(self):
        self.render("index.html", k1='v1', k2='v2')

# 配置就是个key-value的字段
settings = {
    'template_path': 'tpl'
}

application = tornado.web.Application([
    (r"/index", IndexHandler),
], **settings)  # application加载配置信息

POST

先准备好如下的页面,在输入框里填入要搜索的关键字,提交后就跳转到搜索引擎搜索的结果:

<body>
<form method="POST" action="/baidu">
    <input type="text" name="wd" />
    <input type="submit" value="百度一下" />
</form>
</body>

后端的代码:

import tornado.web

class SearchHandler(tornado.web.RequestHandler):
    def get(self):
        self.render("baidu.html")

    def post(self):
        wd = self.get_argument('wd')
        print(wd)
        self.redirect('https://www.baidu.com/s?wd=%s' % wd)

settings = {
    'template_path': 'tpl'
}

application = tornado.web.Application([
    (r"/baidu", SearchHandler),
], **settings)

if __name__ == '__main__':
    import tornado.ioloop
    application.listen(8000)
    tornado.ioloop.IOLoop.instance().start()

上面的示例,post请求最后是用redirect()返回的,这个是页面的跳转。
获取提交的参数的方法有这些:

class LoginHandler(tornado.web.RequestHandler):
    def post(self):
        # 获取URL中以GET形式传递的数据
        self.get_query_argument()
        self.get_query_arguments()
        # 获取请求体中以POST形式传递的数据
        self.get_body_argument()
        self.get_body_arguments()
        # 从上面2个里都尝试获取
        self.get_argument()
        self.get_arguments()

静态文件(图片)

静态文件是给用户直接下载的,所以应该单独存放,并且在配置里注册对应的目录。配置的写法:

settings = {
    'template_path': 'tpl',  # 模板
    'static_path': 'imgs',  # 静态文件
}

现在可以根据配置里的名称去创建一个新的文件夹 static 用来存放静态文件。然后放张图片进去。
这里故意不用static作为静态文件文件夹的名称,这里只是注册文件夹,但是前端引用的时候,无论你的静态文件放在那里,都是用 static/文件名称 。
加一个img标签到html里,然后验证一下效果。注意src里用的是static,而不是文件夹真正的名称:

<img src="static/test.jpg" />

这里前端引用的是必须用static,不过这个名字也是可以自定义的:

settings = {
    'template_path': 'tpl',  # 模板
    'static_path': 'imgs',  # 静态文件
    'static_url_prefix': '/statics/',  # 注意两边都要有斜杠/
}

其他操作

self.request.cookies : 获取cookies
self.set_cookie() : 设置cookie
self.request.headers : 获取请求头
self.set_header() : 设置响应头,如果出现同一个响应头,会覆盖
self.add_header() : 设置响应头,如果出现同一个响应头,则追加

Tornado 没有提供 session ,所以要用的话,得另外写。同样的,缓存也没有。

进阶操作

最基本的就是上面那些了,这里再补充一点别的。

自定义UIMethod以及UIModule

这个就是模板引擎里的自定义函数。
UIMethod 自定义的是个函数,UIModule 自定义的是个类。

定义
把自定义的函数和自定义的类单独写在文件里:

# ui_methods.py
def test1(self):  # 这里的self不能去掉
    return "TEST1"

def test2(self):
    return "TEST2"

# ui_module.py
from tornado.web import UIModule
from tornado import escape

class Test(UIModule):
    def render(self, *args, **kwargs):
        return escape.xhtml_escape('<h3>UI Module TEST</h3>')

注册
写一个完整的服务,这里加上注册的代码。先导入上面的文件,然后分别在settings里注册:

import tornado.web
import ui_methods as mt
import ui_modules as md

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.render('ui.html')

settings = {
    'template_path': 'tpl',  # 模板
    'static_path': 'static',  # 静态文件,这里不重要
    'static_url_prefix': '/statics/',  # 注意两边都要有斜杠/
    'ui_methods': mt,
    'ui_modules': md,
}

application = tornado.web.Application([
    (r"/ui", MainHandler),
], **settings)

if __name__ == '__main__':
    import tornado.ioloop
    application.listen(8000)
    tornado.ioloop.IOLoop.instance().start()

使用
这里只需要看明白前端调用的方法就可以了

<body>
<h1>UI Method</h1>
<p>{{ test1() }}</p>
<p>{{ test2() }}</p>
<h1>UI Module</h1>
{% module Test() %}
</body>

UIModule里的方法
render 方法返回的内容就是调用模板的位置显示的内容:

class Test(UIModule):

    def javascript_files(self):
        pass

    def embedded_javascript(self):
        pass

    def css_files(self):
        pass

    def embedded_css(self):
        pass

    def render(self, *args, **kwargs):
        return escape.xhtml_escape('<h3>UI Module TEST</h3>')

javascript的方法会在body的尾部插入script标签,插入js代码
css的方法则会在head里插入style标签,设置css
files就是直接引入文件,进行设置
embedded就是插入返回的字符串作为设置

CSRF

首先在settings里启用csrf:

settings = {
    "xsrf_cookies": True,
}

在 form 中使用

<form action="/new_message" method="post">
  {% raw xsrf_form_html() %}
  <input type="text" name="message"/>
  <input type="submit" value="提交"/>
</form>

{{ xsrf_form_html() }} 能够输出完整的input标签,但是直接用回被解析为字符串,带着标签的信息作为字符串显示出来。所以上面用的是 {% raw xsrf_form_html() %} 。

在 Ajax 中使用
Ajax使用时,本质上就是去获取本地的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 + ")"));
    }});
};

上传文件

先准备一个form表单上传文件的html页面:

<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    <title>上传文件</title>
</head>
<body>
    <form id="my_form" name="form" action="/index" method="POST"  enctype="multipart/form-data" >
        <input name="fff" id="my_file"  type="file" />
        <input type="submit" value="提交"  />
    </form>
</body>
</html>

接收上传文件:

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 = {
    'template_path': 'template',
}

application = tornado.web.Application([
    (r"/index", MainHandler),
], **settings)

if __name__ == '__main__':
    import tornado.ioloop
    application.listen(8000)
    tornado.ioloop.IOLoop.instance().start()

上传文件还可以用Ajax,另外还有借助iframe标签实现的伪Ajax的实现,略...

异步非阻塞

异步非阻塞IO,高并发高性能是tornado的特点,所以这小节很重要。但是具体内容也没搞明白,只能尽量先记一些。
首先要引入下面的2个模块:

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

    def doing(self,*args, **kwargs):
        self.write('async')
        self.finish()

当发送GET请求时,由于方法被@gen.coroutine装饰且yield 一个Future对象,那么Tornado会等待,等待用户向future对象中放置数据或者发送信号,如果获取到数据或信号之后,就开始执行doing方法。
这里发送请求后,永远也不会返回,就是按上面说的Tornado一直在等待。等待调用了 future.set_result(result) 这个方法。之后就会调用回调函数,而set_result方法里传递进去的参数,可以通过 future.result() 获取到。大致就是这么的用法,但是没有个使用示例有点不好理解。

Future类
Future类位于tornado源码的concurrent模块中。下面是Future类里的一部分代码作为分析之用:

class Future(object):
    def done(self):
        return self._done

    def result(self, timeout=None):
        self._clear_tb_log()
        if self._result is not None:
            return self._result
        if self._exc_info is not None:
            raise_exc_info(self._exc_info)
        self._check_done()
        return self._result

    def add_done_callback(self, fn):
        if self._done:
            fn(self)
        else:
            self._callbacks.append(fn)

    def set_result(self, result):
        self._result = result
        self._set_done()

    def _set_done(self):
        self._done = True
        for cb in self._callbacks:
            try:
                cb(self)
            except Exception:
                app_log.exception('exception calling callback %r for %r',
                                  cb, self)
        self._callbacks = None

Future类重要成员函数:

  • def done(self) : Future的_result成员是否被设置
  • def result(self, timeout=None) : 获取Future对象的结果
  • def add_done_callback(self, fn) : 添加一个回调函数fn给Future对象。如果这个Future对象已经done,则直接执行fn,否则将fn加入到Future类的一个成员列表中保存。
  • def_set_done(self) : 一个内部函数,主要是遍历列表,逐个调用列表中的callback函数,也就是前面 add_done_calback 加如来的。
  • def set_result(self, result) : 给Future对象设置result,并且调用_set_done。也就是说,当Future对象获得result后,所有add_done_callback加入的回调函数就会执行。

这里最终就是希望 future 调用 set_result ,然后就是执行回调函数。

自定义异步非阻塞Web框架

这节主要是想以源码的方式展示分析tornado是怎么实现异步非阻塞的。代码应该不是超的源码,只是借鉴了思路,做了很多简化。
下面是实现异步非阻塞的代码,主要是 select+socket :
https://www.cnblogs.com/wupeiqi/p/6536518.html

什么场景考虑使用Tornado

复杂的应用还是用django来开发。
如果要开发一个API的功能,或者其他的简单的工具、应用,也不用操作数据库。就可以用tornado或者是其他简单的框架。就不需要用django了。
可以选择的简单的框架还有Flask。