Tornado

介绍

Tornado是使用Python开发的全栈式(full-stack)Web框架和异步网络库,最早由4名Google前软件工程师(布雷特·泰勒)2007创办的Friendfeed(一个社交聚合网站)开发而来的。通过使用非阻塞IO,Tornado可以处理数以万计的开放连接,是long polling、WebSockets和其他需要为用户维护长连接应用的理想选择。

目前最新版本6.1, 我们实际项目开发是使用的不可能是最新版本,所以在此我们在tornado基础阶段所学所用的版本为6.0.

文档:https://tornado-zh-cn.readthedocs.io/zh_CN/latest/

github:https://github.com/tornadoweb/tornado

特点

  • 开源的轻量级全栈式Web框架,提供了一整套完善的异步编码方案。

  • 高性能

    基于协程,底层就是基于asyncio来实现的完整的协程调度

    采用异步非阻塞IO处理方式,不依赖多进程或多线程 采用单进程单线程异步IO的网络模式,其高性能源于Tornado基于Linux的Epoll(UNIX为kqueue)的异步网络IO,具有出色的抗负载能力

    Tornado为了实现高并发和高性能,使用了一个IOLoop事件循环来处理socket的读写事件

  • WSGI全栈替代产品,Tornado把应用(Application)和服务器(Server)结合起来,既是WSGI应用也可以是WSGI服务,通俗来讲就是说,Tornado既是web服务器也是web框架,甚至可以通过Tornado替代nginx这些web服务器来运行Flask或者django框架

安装

1
2
mkvirtualenv tornado
pip install tornado==6.0.4

快速入门

运行项目

server.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
from tornado import ioloop
from tornado import web

class Home(web.RequestHandler):
    def get(self):
		# self.write 响应数据
        self.write("hello!")

def make_app():
    # Application是tornado web框架的核心应用类,是与服务器对应的接口,里面保存了路由映射表
    # handlers 设置路由列表
    return web.Application(handlers=[
        (r"/", Home),
    ])

if __name__ == "__main__":
    # 创建应用实例对象
    app = make_app()
    # 设置监听的端口和地址
    app.listen(8888)
    # ioloop,全局的tornado事件循环,是服务器的引擎核心,start表示创建IO事件循环,等待客户端连接
    ioloop.IOLoop.current().start()

img

终端运行

server.py,代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from tornado import ioloop,web
from tornado.options import define,options,parse_command_line

class Home(web.RequestHandler):
    def get(self):
        # self.write 响应数据
        self.write("hello!get")

urlpatterns =[
    (r"/", Home),
]

class Application(web.Application):
    def __init__(self,urlpatterns=None, *args,**kwargs):
        kwargs["handlers"] = urlpatterns
        super().__init__(*args,**kwargs)

if __name__ == "__main__":
    # 定义终端参数
    define(name="host",default="127.0.0.1", type=str)
    define(name="port",default=8000,type=int)

    # 解析终端启动命令,格式:python server.py --port=端口号  --host=监听地址
    parse_command_line()

    app = Application(urlpatterns)
    # 设置监听的端口和地址
    app.listen(address=options.host, port=options.port) # 获取解析到的参数值
    # ioloop,全局的tornado事件循环,是服务器的引擎核心,start表示创建IO事件循环,等待客户端连接
    ioloop.IOLoop.current().start()

调试模式

开启自动加载和调试模式,代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
from tornado import ioloop,web
from tornado.options import define,options,parse_command_line
# 当tornado启动debug模式以后会自动引入 autoreload模块,在保存编辑代码的时候自动重启项目
from tornado import autoreload

class Home(web.RequestHandler):
    def get(self):
        # self.write 响应数据
        self.write("hello!get")

urlpatterns =[
    (r"/", Home),
]

settings = {
    "debug": True
}

class Application(web.Application):
    def __init__(self,urlpatterns=None, *args,**kwargs):
        kwargs["handlers"] = urlpatterns
        super().__init__(*args,**kwargs)

if __name__ == "__main__":
    # 定义终端参数
    define(name="host",default="127.0.0.1", type=str)
    define(name="port",default=8000,type=int)

    # 解析终端启动命令,格式:python server.py --port=端口号  --host=监听地址
    parse_command_line()

    app = Application(urlpatterns,**settings)
    # 设置监听的端口和地址
    app.listen(address=options.host, port=options.port) # 获取解析到的参数值
    # ioloop,全局的tornado事件循环,是服务器的引擎核心,start表示创建IO事件循环,等待客户端连接
    ioloop.IOLoop.current().start()

路由拆分

代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from tornado import ioloop,web
from tornado.options import define,options,parse_command_line

# 类视图
class Home(web.RequestHandler):
    # 视图方法,默认提供 restful风格的类视图方法
    def get(self):
        # self.write 响应数据
        self.write("hello!get")

# 路由列表
urlpatterns =[
    (r"/", Home),
]

class Application(web.Application):
    def __init__(self,urlpatterns=None, *args,**kwargs):
        kwargs["handlers"] = urlpatterns
        super().__init__(*args,**kwargs)

if __name__ == "__main__":
    # 定义终端参数
    define(name="host",default="127.0.0.1", type=str)
    define(name="port",default=8000,type=int)

    # 解析终端启动命令,格式:python server.py --port=端口号  --host=监听地址
    parse_command_line()

    app = Application(urlpatterns)
    # 设置监听的端口和地址
    app.listen(address=options.host, port=options.port) # 获取解析到的参数值
    # ioloop,全局的tornado事件循环,是服务器的引擎核心,start表示创建IO事件循环,等待客户端连接
    ioloop.IOLoop.current().start()

视图编写

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from tornado import ioloop,web

class Home(web.RequestHandler):
    def get(self):
        # self.write 响应数据
        self.write("hello!get")

    def post(self):
        self.write("hello!post")

    def put(self):
        self.write("hello!put")

    def patch(self):
        self.write("hello!patch")

    def delete(self):
        self.write("hello!delete")

def make_app():
    return web.Application(handlers=[
        (r"/", Home),
    ])

if __name__ == "__main__":
    app = make_app()
    # 设置监听的端口和地址
    app.listen(address="127.0.0.1", port=8000)
    # ioloop,全局的tornado事件循环,是服务器的引擎核心,start表示创建IO事件循环,等待客户端连接
    ioloop.IOLoop.current().start()

多进程模式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
from tornado import ioloop
from tornado import web,httpserver
from tornado import autoreload
from tornado.options import define,options,parse_command_line

settings = {
    'debug' : False,
}

define("port", default=8888, type=int,help="设置监听端口号,默认为8888")

class Home(web.RequestHandler):
    def get(self):
        # self.write 响应数据
        self.write("hello!get")

    def post(self):
        self.write("hello!post")

    def put(self):
        self.write("hello!put")

    def patch(self):
        self.write("hello!patch")

    def delete(self):
        self.write("hello!delete")

# 路由列表
urls = [
    (r"/", Home),
]

if __name__ == "__main__":
    # 创建应用实例对象
    app = web.Application(urls,**settings)
    parse_command_line()
    server = httpserver.HTTPServer(app)
    # 设置监听的端口和地址
    server.bind(options.port)
    server.start(0) # 0表示进程=CPU核数+1
    # ioloop,全局的tornado事件循环,是服务器的引擎核心,start表示创建IO事件循环
    ioloop.IOLoop.current().start()

请求与响应

请求

tornado.httputil.HTTPServerRequest

server.py,代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
from tornado import ioloop,web

class Home(web.RequestHandler):
    def get(self):
        # print(self.request) # from tornado.httputil import HttpServerRequest的实例对象
        """
        HTTPServerRequest(protocol='http', host='127.0.0.1:8000', method='GET', uri='/', version='HTTP/1.1', remote_ip='127.0.0.1')
        """
        # print(self.request.protocol)  # http         协议
        # print(self.request.method)    # GET          http请求动作/请求方法
        # print(self.request.uri)       # /            请求的uri地址
        # print(self.request.remote_ip) # 127.0.0.1    发起请求的客户端地址

        # print(self.request.full_url())  # http://127.0.0.1:8000/    完整url地址
        # print(self.request.version)     # HTTP/1.1                  HTTP协议版本
        # print(self.request.headers) # 请求头 HTTPHeaders
        """
        Content-Type: text/plain
        User-Agent: PostmanRuntime/7.13.0
        Accept: */*
        Cache-Control: no-cache
        Postman-Token: f73f9e82-5b24-4e54-9318-3f314e547307
        Host: 127.0.0.1:8000
        Accept-Encoding: gzip, deflate
        Content-Length: 91
        Connection: keep-alive
        """
        # print(self.request.body)        # 请求体[原始数据]
        # print(self.request.host)        # 127.0.0.1:8000       地址端口
        # print(self.request.files)       # 上传文件
        # print(self.request.cookies)     # cookie信息

        # 接收参数
        # 例如,请求地址:
        # GET http://127.0.0.1:8000/?uid=1&uname=xiaoming
        print(self.request.query_arguments) # query_string 地址参数列表
        """打印效果:
        {'uid': [b'1'], 'uname': [b'xiaoming']}
        """
        print(self.request.body_arguments)  # body 请求体参数列表
        """打印效果:
        {}
        """
        print(self.request.request_time())  # 0.0004684925079345703    服务端接受请求到处理完成的时间[单位:秒]

        self.write("hello!get")

    def post(self):
        """post方法"""
        """
        例如,请求地址:
        请求行: POST    http://127.0.0.1:8000/?uid=1&uname=xiaoming
        请求体: username=xiaoming&password=123456
        """
        print(self.request.query_arguments) # 地址参数列表
        """打印效果:
        {'uid': [b'1'], 'uname': [b'xiaoming']}        
        """
        print(self.request.body_arguments)  # 请求体参数列表.不能接受json
        """打印效果:
        {'username': [b'xiaoming'], 'password': [b'123456']}
        """
        print(self.request.request_time())  # 0.0003886222839355469   请求处理时间

        self.write("hello!post")

def make_app():
    return web.Application(handlers=[
        (r"/", Home),
    ],debug=True)

if __name__ == "__main__":
    app = make_app()
    # 设置监听的端口和地址
    app.listen(address="127.0.0.1", port=8000)
    # ioloop,全局的tornado事件循环,是服务器的引擎核心,start表示创建IO事件循环,等待客户端连接
    ioloop.IOLoop.current().start()

接收查询字符串

server.py,代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
from tornado import ioloop,web

class Home(web.RequestHandler):
    def get(self):
        """获取参数-查询字符串"""
        """
        请求行:GET http://127.0.0.1:8000/?uid=1&uname=xiaoming&lve=swimming&lve=boll&lve=shopping
        请求体:uname=xiaohong&lve=music
        """

        # print(self.request.query)
        """获取原始的query_string查询字符串数据
        uid=1&uname=xiaoming&lve=swimming&lve=boll&lve=shopping
        """

        # print(self.request.query_arguments)
        # print(self.request.arguments["uname"][0].decode())
        """获取解析后的query_string查询字符串数据
        {'uid': [b'1'], 'uname': [b'xiaoming'], 'lve': [b'swimming', b'boll', b'shopping']}
        xiaoming
        """
        print(self.get_query_argument("uname1",default=None))
        """获取查询字符串中指定参数的值[单个值,默认是最后一个]
        xiaoming
        """
        # print(self.get_query_arguments("lve"))
        """获取查询字符串中指定参数的值[多个值,总是以列表格式返回]
        ['swimming', 'boll', 'shopping']
        """

        # print(self.get_argument("uname"))
        """获取请求参数中指定参数的单个值,不区分请求体和查询字符串的数据,当查询字符串和请求体参数名一致时,请求体会覆盖查询字符串的内容
        xiaohong
        """
        # print(self.get_arguments("lve"))
        """获取请求参数中指定参数的多个值[列表格式],不区分请求体和查询字符串的数据
        ['swimming', 'boll', 'shopping', 'music']
        """
        self.write("hello!get")

def make_app():
    return web.Application(handlers=[
        (r"/", Home),
    ],debug=True)

if __name__ == "__main__":
    app = make_app()
    # 设置监听的端口和地址
    app.listen(address="127.0.0.1", port=8000)
    # ioloop,全局的tornado事件循环,是服务器的引擎核心,start表示创建IO事件循环,等待客户端连接
    ioloop.IOLoop.current().start()

浏览器:http://127.0.0.1:8888/?name=xiaoming&name=xiaohong

接收请求体

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
from tornado import ioloop,web

class Home(web.RequestHandler):
    def post(self):
        """接受请求体数据"""
        """
        请求行:GET http://127.0.0.1:8000/?uid=1&uname=xiaoming&lve=swimming&lve=boll&lve=shopping
        请求体:uname=xiaohong&lve=music&lve=game
        """
        # print(self.request.arguments)
        """获取请求参数,不区分查询字符串或请求体
        {'uid': [b'1'], 'uname': [b'xiaoming', b'xiaohong'], 'lve': [b'swimming', b'boll', b'shopping', b'music']}
        """

        # print(self.request.body)
        """获取请求体原始内容,包括普通的文本数据或者上传文件内容
        b'----------------------------601718903670717038003781\r\nContent-Disposition: form-data; name="uname"\r\n\r\nxiaohong\r\n----------------------------601718903670717038003781\r\nContent-Disposition: form-data; name="lve"\r\n\r\nmusic\r\n----------------------------601718903670717038003781\r\nContent-Disposition: form-data; name="log"; filename="java_error_in_PYCHARM_2557.log"\r\nContent-Type: text/plain\r\n\r\n#\n# A fatal error has been detected by the Java Runtime Environment:\n#\n#  SIGSEGV (0xb) at pc=0x00007f5fe3d1a0a6, pid=2557, tid=2643\n#\n# JRE version: OpenJDK Runtime Environment (11.0.4+10) (build 11.0.4+10-b304.77)\n# Java VM: OpenJDK 64-Bit Server VM (11.0.4+10-b304.77, mixed mode, tiered, compressed oops, concurrent mark sweep gc, linux-amd64)\n# Problematic frame:\n# C  [libc.so.6+0x430a6]\n#\n# Core dump will be written. Default location: Core dumps may be processed with "/usr/share/apport/apport %p %s %c %d %P %E" (or dumping to /home/moluo/core.2557)\n#\n# If you would like to submit a bug report, please visit:\n#   http://bugreport.java.com/bugreport/crash.jsp\n# The crash happened outside the Java Virtual Machine in native code.\n# See problematic frame for where to report the bug.\n#\n\n---------------  S U M M A R Y ------------\n\nCommand Line: -Xms128m -Xmx750m -XX:ReservedCodeCacheSize=240m -XX:+UseConcMarkSweepGC -XX:SoftRefLRUPolicyMSPerMB=50 -ea -XX:CICompilerCount=2 -Dsun.io.useCanonPrefixCache=false -Djava.net.preferIPv4Stack=true -Djdk.http.auth.tunneling.disabledSchemes="" -XX:+HeapDumpOnOutOfMemoryError -XX:-OmitStackTraceInFastThrow -Djdk.attach.allowAttachSelf -Dkotlinx.coroutines.debug=off -Djdk.module.illegalAccess.silent=true -Dawt.useSystemAAFontSettings=lcd -Dsun.java2d.renderer=sun.java2d.marlin.MarlinRenderingEngine -Dsun.tools.attach.tmp.only=true -javaagent:/opt/pycharm-2019.2.4/jetbrains-agent.jar -XX:ErrorFile=/home/moluo/java_error_in_PYCHARM_%p.log -XX:HeapDumpPath=/home/moluo/java_error_in_PYCHARM.hprof -Didea.paths.selector=PyCharm2019.2 -Djb.vmOptionsFile=/home/moluo/.PyCharm2019.2/config/pycharm64.vmoptions -Didea.platform.prefix=Python com.intellij.idea.Main\n\nHost: Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz, 4 cores, 3G, \r\n----------------------------601718903670717038003781--\r\n'
        """

        # print(self.request.body_arguments)
        """获取请求体数据
        {'uname': [b'xiaohong'], 'lve': [b'music']}
        """
        print(self.get_argument("uname"))
        """获取指定请求参数的单个值,不区分查询字符串或请求体数据
        xiaohong
        """
        print(self.get_arguments("uname"))
        """获取指定请求参数的多个值,不区分查询字符串或请求体数据
        ['xiaoming', 'xiaohong']
        """

        print(self.get_body_argument("lve"))
        """获取请求体的指定参数的单个值
        game
        """
        print(self.get_body_arguments("lve"))
        """获取请求体的指定参数的多个值
        ['music', 'game']
        """
        self.write("hello!get")

def make_app():
    return web.Application(handlers=[
        (r"/", Home),
    ],debug=True)

if __name__ == "__main__":
    app = make_app()
    # 设置监听的端口和地址
    app.listen(address="127.0.0.1", port=8000)
    # ioloop,全局的tornado事件循环,是服务器的引擎核心,start表示创建IO事件循环,等待客户端连接
    ioloop.IOLoop.current().start()

接收路由参数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from tornado import ioloop,web

class Home(web.RequestHandler):
    def get(self,mobile):
        print(mobile)
        self.write("hello,get")

class Index(web.RequestHandler):
    def get(self,uid,category):
        print(f"uid={uid},category={category}")
        self.write("hello,get")

def make_app():
    return web.Application(handlers=[
        (r"/home/(\d{11})", Home), # 路由的位置参数传递[非命名参数,不绑定传参]
        (r"/index/(?P<uid>\d+)/(?P<category>\d+)", Index), # 命名参数,绑定传参
    ],debug=True)

if __name__ == "__main__":
    app = make_app()
    # 设置监听的端口和地址
    app.listen(address="127.0.0.1", port=8000)
    # ioloop,全局的tornado事件循环,是服务器的引擎核心,start表示创建IO事件循环,等待客户端连接
    ioloop.IOLoop.current().start()

响应

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
from tornado import ioloop
from tornado import web
from tornado import autoreload
from tornado.options import define, options, parse_command_line

settings = {
    'debug': True,
}

define("port", default=8888, type=int, help="设置监听端口号,默认为8888")

from datetime import datetime
class Home(web.RequestHandler):
    def set_default_headers(self):
        self.set_header("time", int(datetime.now().timestamp()))

    def get(self):
        # self.write("<h1>hello</h1>") # 响应html文本信息
        self.write({"message":"hello get"}) # 响应json数据
        self.set_header("Content-Type","text/json; charset=gbk")
        self.add_header("Company","OldboyEdu") # 自定义响应头
        self.set_cookie("name","xiaohui") # 设置cookie

    def post(self):
        self.write({"message": "hello post"})  # 响应json数据

    def put(self):
        self.clear_header("time")
        # self.set_status(404,"Not Found")
        # self.send_error(500,reason="服务器炸了!")
        self.send_error(404, msg="服务器炸了!", info="快报警")

    def write_error(self, status_code, **kwargs):
        self.write("<h1>完蛋啦...</h1>")
        self.write("<p>错误信息:%s</p>" % kwargs["msg"])
        self.write("<p>错误描述:%s</p>" % kwargs["info"])

    def patch(self):
        # 页面跳转
        self.redirect("http://www.baidu.com")

# 设置路由列表
urls = [
    (r"/", Home),
]

if __name__ == "__main__":
    # 创建应用实例对象
    parse_command_line()
    app = web.Application(urls, **settings)
    # 设置监听的端口和地址
    app.listen(options.port)
    # ioloop,全局的tornado事件循环,是服务器的引擎核心,start表示创建IO事件循环
    ioloop.IOLoop.current().start()

基本使用

设置cookie self.set_cookie(name, value)

获取cookie self.get_cookie(name)

server.py,代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
from tornado import ioloop,web
from datetime import datetime

class Home(web.RequestHandler):
    def get(self):
        # 通过set_cookie设置cookie信息,没有设置expire参数,则默认当前cookie为会话期
        self.set_cookie("uname","xiaoming")
        # 设置cookie可以通过expire设置有效期,单位: 秒
        self.set_cookie("token","abcdef",expires=datetime.now().timestamp()+10)
        self.write("hello,home")

class Index(web.RequestHandler):
    def get(self):
        # 原始的所有cookie信息[一行一个cookie]
        print(self.request.cookies)
        # # 原始的单个cookie信息
        print(self.request.cookies.get("token",None))

        # 获取指定名称的cooie
        print(self.get_cookie("uname",None))
        print(self.get_cookie("token",None))

        self.write("hello,index")

def make_app():
    return web.Application(
        handlers=[
            (r"/set_cookie", Home),
            (r"/get_cookie", Index),
        ],
        debug=True,
    )


if __name__ == '__main__':
    app = make_app()
    app.listen(port=8000)
    ioloop.IOLoop.current().start()

加密使用

设置加密cookie self.set_secure_cookie(name,value)

获取cookie self.get_secure_cookie(name)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
from tornado import ioloop,web
from datetime import datetime

class Home(web.RequestHandler):
    def get(self):
        # 加密cookie,必须先设置cookie_secret加密秘钥
        self.set_secure_cookie("name", "xiaoming", expires=int(datetime.now().timestamp()) + 30)
        self.set_secure_cookie("age", "16")
        self.write("hello,home")

class Index(web.RequestHandler):
    def get(self):
        # 获取到的数据是bytes类型
        age = self.get_secure_cookie("age")
        name = self.get_secure_cookie("name")
        print( f"name={name}" )
        print( f"age={age}" )

        if age:
            print( f"age={age.decode()}" )

        if name:
            print( f"name={name.decode()}" )

        self.write("hello,index")

def make_app():
    return web.Application(
        handlers=[
            (r"/set_cookie", Home),
            (r"/get_cookie", Index),
        ],
        debug=True,
        # base64.b64encode(uuid.uuid4().bytes+uuid.uuid4().bytes)
        cookie_secret="WO+JNAJ3QZyOe4SMVXZpXAt3uG9hoU0UokoCBeYn1Y4="
    )


if __name__ == '__main__':
    app = make_app()
    app.listen(port=8000)
    ioloop.IOLoop.current().start()

刪除和清空

1
2
删除cookie      self.clear_cookie(name)
清空cookie      self.clear_all_cookie()
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
from tornado import ioloop,web
from datetime import datetime

class Home(web.RequestHandler):
    def get(self):
        # 加密cookie,必须先设置cookie_secret加密秘钥
        self.set_secure_cookie("name", "xiaoming", expires=int(datetime.now().timestamp()) + 3600)
        self.set_secure_cookie("age", "16")
        # 通过set_cookie设置cookie信息,没有设置expire参数,则默认当前cookie为会话期
        self.set_cookie("uname","xiaoming")
        # 设置cookie可以通过expire设置有效期,单位: 秒
        self.set_cookie("token","abcdef",expires=datetime.now().timestamp()+3600)
        self.write("hello,home")

class Index(web.RequestHandler):
    def get(self):
        print(self.get_cookie("uname",None))
        print(self.get_cookie("token",None))
        print(self.get_secure_cookie("age"))
        print(self.get_secure_cookie("name"))

        self.write("hello,index")

class Main(web.RequestHandler):
    def get(self):
        # 删除指定cookie
        self.clear_cookie("uname")
        self.set_cookie("token","",expires=datetime.now().timestamp() - 3600)

        # 清空客户端所有cookie[当前服务器]
        self.clear_all_cookies()
        
        self.write("hello,main")

def make_app():
    return web.Application(
        handlers=[
            (r"/set_cookie", Home),
            (r"/get_cookie", Index),
            (r"/del_cookie", Main),
        ],
        debug=True,
        # base64.b64encode(uuid.uuid4().bytes+uuid.uuid4().bytes)
        cookie_secret="WO+JNAJ3QZyOe4SMVXZpXAt3uG9hoU0UokoCBeYn1Y4="
    )


if __name__ == '__main__':
    app = make_app()
    app.listen(port=8000)
    ioloop.IOLoop.current().start()

注意:

  1. 2021年1月以后,Google为首的浏览器宣布不再建议和支持web开发使用cookie.所以我们可以采用其他的方案来代替cookie技术.
  2. tornado没有提供session操作,如果需要使用到session可以自己实现或者引入第三方模块。
  3. cookie的删除和清空操作, 是不管是否加密的.

静态文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import os
from tornado import ioloop,web

settings = {
    "debug":True,
    # 静态文件保存路径
    # "static_path": os.path.join(os.path.dirname(__file__), 'static'),
    # 静态文件url地址前缀
    # "static_url_prefix":"/static/", # 必须前后有斜杠
    # 提供静态文件访问支持的视图类
    # "static_handler_class": web.StaticFileHandler,
}

class Home(web.RequestHandler):
    def get(self):
        self.write("hello,home")

def make_app():
    return web.Application(
        handlers=[
            (r"/", Home),
            # 也可以不需要在配置中填写,直接在路由中进行指定设置
            (r"/static/(.*)", web.StaticFileHandler, {"path": os.path.join(os.path.dirname(__file__), 'static')}),
        ],
        **settings,
    )


if __name__ == '__main__':
    app = make_app()
    app.listen(port=8000)
    ioloop.IOLoop.current().start()

页面响应

加载template模板

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import os
from tornado import ioloop,web

settings = {
    "debug":True,
    # html模板目录
    "template_path": os.path.join(os.path.dirname(__file__), 'templates'),
}

class Home(web.RequestHandler):
    def get(self):
        data = {
            "title": "大标题",
        }
        self.render("home.html",**data)

def make_app():
    return web.Application(
        handlers=[
            (r"/", Home),
            # 也可以不需要在配置中填写,直接在路由中进行指定设置
            (r"/static/(.*)", web.StaticFileHandler, {"path": os.path.join(os.path.dirname(__file__), 'static')}),
        ],
        **settings,
    )


if __name__ == '__main__':
    app = make_app()
    app.listen(port=8000)
    ioloop.IOLoop.current().start()

templates/home.html,代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>Home.get</h1>
    <p>{{title}}</p>
    
    tornado默认内置了一套非常强大的模板引擎<br>
    这套模板引擎是基于jinja2模板引擎的基础上进行了改造而成的。<br>
    当然jinja2是基于django的DTL模板引擎基础上改造而成的。<br>
    所以flask和tornado进行比较的时候,从来不提tornado抄袭模板引擎这个事,反而会和django去比较模板引擎的问题。
</body>
</html>

路由进阶

tornado中的路由参数最终是由tornado.routing.Rule进行实例转换

路由语法和参数

在路由列表的路由成员中,我们一共可以设置4个参数

路由的解析流程

Application(handlers=路由列表)

_ApplicationRouter(self, handlers)

super(_ApplicationRouter, self).__init__(rules)

super(ReversibleRuleRouter, self).__init__(rules)

RuleRouter.add_rules(rules)

Rule(matcher=r"/uri路径", target=视图类, target_kwargs={“参数名”:“参数值”,…}, name=“路由别名”)

URLSpec(pattern=r"/uri路径",, handler=视图类,kwargs={“参数名”:“参数值”,…},name=“路由别名”)

url(pattern=r"/uri路径",, handler=视图类,kwargs={“参数名”:“参数值”,…},name=“路由别名”)

所有的路由列表最终保存的地方就是 RuleRouter.rules路由列表中

server.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from tornado import ioloop,web
from tornado.web import url
class Home(web.RequestHandler):
    # initialize 视图初始化的时候会自动调动,钩子方法,可以在当前方法的参数列表中接受来自路由的附加参数
    def initialize(self, proxy):
        self.proxy = proxy

    def get(self):
        print(self.proxy)
        # 对路由别名进行 反解析
        print("uri路径:%s" % self.reverse_url("myhome"))  # 对路由别名进行 反解析
        self.write("Home.get")

    def post(self):
        print(self.proxy)

if __name__ == '__main__':
    app = web.Application(handlers=[
            # 路由的第三个参数,主要是提供给视图initialize的附带参数
            # (r"/", Home,{"proxy": 10086}),
            url(r"/", Home,{"proxy": 10086},"myhome"),
        ],
        debug=True
    )
    app.listen(port=8000)
    ioloop.IOLoop.current().start()

视图进阶

视图中内置的钩子方法

在tornado提供的视图类中,我们除了可以编写客户端http请求对应名称的视图方法和初始化方法initialize以外,还提供了一个预处理方法prepareon_finish,prepare方法会在http请求方法执行之前先执行,on_finish会在http响应完成时进行。

server.py,代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import json
from tornado import ioloop,web
class Home(web.RequestHandler):
    def initialize(self, proxy):
        self.proxy = proxy

    def prepare(self):
        """预处理"""
        self.json_arguments = {}
        self.is_json = False
        content_type = self.request.headers.get("content-type")
        # print(content_type)
        if content_type == "application/json":
            """json数据"""
            self.is_json = True
            self.json_arguments = json.loads(self.request.body)
        elif content_type == "application/xml":
            """xml数据"""
            print("采用lxml的模块,编写xpath路径提取标签名作为参数,标签内容作为参数值")


    def post(self):
        if self.is_json:
            print(self.json_arguments)

        self.write("Home.post")
        self.write("Home.post")
        self.finish() # 立刻冲刷暂存区中的数据返回客户端
        print("程序继续执行!!!")
        self.write("Home.post") # 这里两个输出方法就不会被返回给客户端,而是清洗掉了
        self.write("Home.post")

    def on_finish(self):
        """视图处理数据完成以后,自动执行的钩子方法"""
        # 记录日志信息等等
        print("视图执行完成以后的收尾工作")

if __name__ == '__main__':
    app = web.Application(handlers=[
            (r"/", Home,{"proxy": 10086}),
        ],
        debug=True
    )
    app.listen(port=8000)
    ioloop.IOLoop.current().start()
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# postman作为客户端调试时发送数据的测试信息:
# xml数据
<xml version="1.0" encoding="utf-8">
   <username>xiaoming</username>
   <password>123456</password>
</xml>

# json数据
{
	"username":"xiaoming",
	"password":"123456"
}

视图中内置钩子的调用顺序

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
from tornado import ioloop
from tornado import web
from tornado.httpserver import HTTPServer
from tornado.options import define, options, parse_command_line
from tornado.web import url
settings = {
    'debug': True,
}

define("port", default=8888, type=int, help="设置监听端口号,默认为8888")

class Home(web.RequestHandler):
    def initialize(self):
        print("initialize执行了")

    def prepare(self):
        print("prepare执行了")


    def set_default_headers(self):
        print("set_default_headers执行了")

    def get(self):
        self.write("hello,get")
        print("视图http方法执行了")
        # self.send_error(200,msg="注意:丢炸弹了")  # 此处抛出错误

    def write_error(self, status_code, **info):
        print("write_error执行了,msg=%s" % info["msg"])

    def on_finish(self):
        print("on_finish执行了")

# 设置路由列表
urls = [
    (r"/", Home),
]

if __name__ == "__main__":
    # 创建应用实例对象
    parse_command_line()
    app = web.Application(urls, **settings)
    server = HTTPServer(app)
    # 设置监听的端口和地址
    server.listen(options.port)
    server.start(1)
    ioloop.IOLoop.current().start()

当视图中没有任何异常时,执行顺序:

set_defautl_headers()
initialize()
prepare()
视图http方法()
on_finish()

当视图中抛出异常错误时,执行顺序:

set_default_headers()
initialize()
prepare()
视图http方法()
set_default_headers()
write_error()
on_finish()

冲刷缓存

在前面的学习中,我们使用了self.write()来完成数据的响应。

事实上,在tornado提供的视图操作中,视图基类(web.RequestHandler)中提供了一个 _write_buffer列表用于暂时缓存提供给客户端的数据, 这个 _write_buffer就是输出缓冲区(输出暂存区)

self.write()本质上来说是将chunk数据块写到输出缓冲区(_write_buffer)中。所以才出现在视图中多次调用self.write()输出数据的情况,因为self.write()根本没有输出数据,而是把数据写入到了输出缓冲区里面. 如果没有其他操作干预的情况下,则视图方法处理完成以后,会自动执行finished()方法把输出缓冲区_write_buffer中所有的数据冲刷出来响应给客户端。

除了self.write()方法以外,tornado还提供了2个方法用于在视图中冲刷缓存数据到客户端的。

self.flush() 立刻把数据从输出缓冲区冲刷出去。

self.finish()立刻把数据从输出缓冲区冲刷出去。但是与self.flush()不同的是, self.finish()执行了以后, 后面的所有输出调用都不在支持.也就不能返回给客户端。

server.py,代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from tornado import ioloop
from tornado import web
import time
class Home(web.RequestHandler):

    def get(self):
        # 提供超大文件的读取和下载功能
        self.set_header("Content-Type","image/jpg")
        with open("./static/7.jpg","rb") as f:
            while True:
                content = f.read(1024)
                if not content:
                    break
                self.write(content)
                # time.sleep(1)
                self.flush()  # 立刻冲刷缓冲区的数据返回给客户端

if __name__ == "__main__":
    app = web.Application([
        (r"/", Home),
    ], debug=True)
    # 设置监听的端口和地址
    app.listen(port=8000)
    ioloop.IOLoop.current().start()

用户认证

tornado提供了装饰器tornado.web.authenticated与视图内置方法get_current_user允许我们轻松的实现定制性的用户认证功能。

装饰器authenticated依赖于请求处理类中的self.current_user属性来进行判断用户是否通过认证,如果self.current_user值为假(None、False、0、““等),任何GET或HEAD请求都将把访客重定向到settings配置中login_url设置的URL,而非法用户的POST请求将返回HTTPError(403)异常, Forbidden。

server.py,代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
from tornado import ioloop
from tornado import web
from tornado.web import authenticated


class Login(web.RequestHandler):
    def get(self):
        self.write("登录视图!")

class View(web.RequestHandler):
    def get_current_user(self):
        """登录认证方法"""
        username = self.get_argument("username", "")
        password = self.get_argument("password", "")
        # 此处肯定通过token或者数据库查询用户信息
        if username == "root" and password == "123":
            return username

class Home(View):
    @authenticated
    def get(self):
        print(self.current_user)
        self.write("Home.get")

    @authenticated
    def post(self):
        self.write("Home.post")

settings = {
    "login_url": r"/login"
}

if __name__ == "__main__":
    app = web.Application([
        (r"/", Home),
        (r"/login", Login),
    ], debug=True,**settings)
    # 设置监听的端口和地址
    app.listen(port=8000)
    ioloop.IOLoop.current().start()

先访问:http://127.0.0.1:8000/?username=root&password=123

再访问:http://127.0.0.1:8000/

模板语法

基本语法

变量、表达式与自定义函数

server.py,代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
from tornado import ioloop
from tornado import web

import os,time
settings = {
    'debug': True,
    # 静态文件保存路径
    "static_path": os.path.join(os.path.dirname(__file__), 'static'),
    # 静态文件url地址前缀
    "static_url_prefix":"/static/", # 必须前后有斜杠
    "template_path": os.path.join(os.path.dirname(__file__), 'templates'),
}


class HttpRequest(web.RequestHandler):
    def initialize(self):
        self.data = {}

    def render(self, template_name, **kwargs):
        super().render(template_name,**self.data)

from datetime import datetime
def money_format(data):
    return "¥" + ("%.2f" % data)

class Home(HttpRequest):
    def get(self):
        # 变量
        self.data["title"] = "大标题"
        self.data["book_list"] = [
            {"id":1,"title": "代码之髓", "price":38.8888},
            {"id":2,"title": "算法", "price": 99.8},
            {"id":3,"title": "操作系统", "price": 59.0}
        ]
        self.data["address"] = ["北京市","昌平区","百沙路"]
        self.data["datetime"] = datetime
        self.data["money_format"] = money_format
        self.render("1.html")

# 设置路由列表
urls = [
    (r"/", Home),
]

if __name__ == "__main__":
    app = web.Application(urls, **settings)
    app.listen(port=8000)
    ioloop.IOLoop.current().start()

templates/index.html,代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    {# 模板注释 #}
    <h1>{{ title }}</h1>
    <p>{{ book_list[0]["id"] }} {{ book_list[0]["title"] }} {{ money_format(book_list[0]["price"]) }}</p>
    <p>{{ "-".join(address) }}</p>
    <p>当前时间戳: {{ int(datetime.now().timestamp()) }}</p>
    <p>获取当前客户端的http请求方法: {{ request.method }}</p>
</body>
</html>

流程控制

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 判断
{% if ... %}
{% elif ... %}
{% else ... %}
{% end %}

# 遍历
{% for ... in ... %}
{% end %}

# 循环
{% while ... %}
{% end %}

server.py,代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
from tornado import ioloop
from tornado import web

import os,time
settings = {
    'debug': True,
    # 静态文件保存路径
    "static_path": os.path.join(os.path.dirname(__file__), 'static'),
    # 静态文件url地址前缀
    "static_url_prefix":"/static/", # 必须前后有斜杠
    "template_path": os.path.join(os.path.dirname(__file__), 'templates'),
}


class HttpRequest(web.RequestHandler):
    def initialize(self):
        self.data = {}

    def render(self, template_name, **kwargs):
        super().render(template_name,**self.data)

def money_format(data):
    return "¥" + ("%.2f" % data)

class Home(HttpRequest):
    def get(self):
        self.data["book_list"] = [
            {"id":10,"title": "代码之髓", "price":38.8888},
            {"id":11,"title": "算法", "price": 99.8},
            {"id":12,"title": "操作系统", "price": 59.0}
        ]
        self.data["money_format"] = money_format

        self.render("2.html")

# 设置路由列表
urls = [
    (r"/", Home),
]

if __name__ == "__main__":
    app = web.Application(urls, **settings)
    app.listen(port=8000)
    ioloop.IOLoop.current().start()

templates/2.html,代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
    table,tr,td,th{
        border-collapse: collapse;
        border: 1px solid #aaa;
        padding: 4px;
    }
    .tr-low{
        background: beige;
    }
    </style>
</head>
<body>
    <table border="1" width="600">
        <tr>
            <th>序号</th>
            <th>id</th>
            <th>title</th>
            <th>price</th>
        </tr>
        {% for key,book in enumerate(book_list) %}
            {% if book["price"] < 50 %}
            <tr class="tr-low">
            {% else %}
            <tr>
            {% end %}
                <td>{{ key + 1 }}</td>
                <td>{{ book["id"] }}</td>
                <td>{{ book["title"] }}</td>
                <td>{{ book["price"] }}</td>
            </tr>
        {% end %}
    </table>
</body>
</html>

内置标签或函数

内置标签

内置标签,也叫内置语句

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# 导包
{% from ... import ... %}
{% import ... %}
# 加载其他模板
{% include ... %}
# 输出原始数据
{% raw ... %}

# 语句/局部变量
{% set 变量名=变量值 %}

# 异常处理
{% try %}...{% except %}...{% else %}...{% finally %}...{% end %}
# 模板继承
{% extends "filename.html" %}
{% block 模板块名称} {% end %}

代码1,server.py,代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
from tornado import ioloop
from tornado import web

import os,time
settings = {
    'debug': True,
    # 静态文件保存路径
    "static_path": os.path.join(os.path.dirname(__file__), 'static'),
    # 静态文件url地址前缀
    "static_url_prefix":"/static/", # 必须前后有斜杠
    "template_path": os.path.join(os.path.dirname(__file__), 'templates'),
}


class HttpRequest(web.RequestHandler):
    def initialize(self):
        self.data = {}
        self.data["title"] = "默认的公共头部标题"

    def render(self, template_name, **kwargs):
        super().render(template_name,**self.data)

class Home(HttpRequest):
    def get(self):
        # self.data["title"]="我的标题"
        self.data["content"] = '<a href="http://www.baidu.com">百度</a>'
        self.render("3.html")

# 设置路由列表
urls = [
    (r"/", Home),
]

if __name__ == "__main__":
    app = web.Application(urls, **settings)
    app.listen(port=8000)
    ioloop.IOLoop.current().start()

templates/3.html,代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>

</head>
<body>
    {% include "header.html" %}
    {% from datetime import datetime %}
    {{ datetime.now().timestamp() }}
    <p>{{ content }}</p>
    <p>{% raw content %}</p>
    {% set uname="小明" %}
    <p>{{ uname }}</p>
</body>
</html>

代码2,server.py,代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from tornado import ioloop
from tornado import web

import os,time
settings = {
    'debug': True,
    # 静态文件保存路径
    "static_path": os.path.join(os.path.dirname(__file__), 'static'),
    # 静态文件url地址前缀
    "static_url_prefix":"/static/", # 必须前后有斜杠
    "template_path": os.path.join(os.path.dirname(__file__), 'templates'),
}


class HttpRequest(web.RequestHandler):
    def initialize(self):
        self.data = {}
        self.data["title"] = "默认的公共头部标题"

    def render(self, template_name, **kwargs):
        super().render(template_name,**self.data)

class Home(HttpRequest):
    def get(self):
        self.render("4.html")

# 设置路由列表
urls = [
    (r"/", Home),
]

if __name__ == "__main__":
    app = web.Application(urls, **settings)
    app.listen(port=8000)
    ioloop.IOLoop.current().start()

templates/4.html,代码:

1
2
3
4
5
{% extends "main.html" %}
{% block title %} 4.html的网页标题 {% end %}
{% block content %}
    4.html的内容
{% end %}

templates/main.html,代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{% block title %}{{ title }}{% end %}</title>
</head>
<body>
    {% block content %}
    main.html的默认内容
    {% end %}
</body>
</html>

内置函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 输出转义数据,tornado在配置中允许通过autoescape=None设置全局转义
{{ escape(text) }} # 等同于 {% raw text %}
# 静态文件存储路径
{{ static_url("style.css") }}
# 路由反解析
reverse_url("路由别名")

# CSRF防范机制,CSRF也叫XSRF
# tornado开启csrf必须在配置中进行设置 xsrf_cookies = True
# 补充:
# 在前后端分离项目中,客户端可以通过cookie来读取XSRFToken,cookie名称为_xsrf,请求头必须名称:X-XSRFToken
# 在视图方法中可以通过 self.xsrf_token 来获取 XSRFTokentoken
{% module xsrf_form_html() %}

server.py,代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
from tornado import ioloop
from tornado import web

import os,time
settings = {
    'debug': True,
    # 静态文件保存路径
    "static_path": os.path.join(os.path.dirname(__file__), 'static'),
    # 静态文件url地址前缀
    "static_url_prefix":"/static/", # 必须前后有斜杠
    "template_path": os.path.join(os.path.dirname(__file__), 'templates'),
    # 开启 xsrf,当开启了csrf以后,
    # 视图类中的_execute()方法会自动执行验证csrf_token的操作
    # self.check_xsrf_cookie()
    "xsrf_cookies": True
}

class HttpRequest(web.RequestHandler):
    def initialize(self):
        self.data = {}
        self.data["title"] = "默认的公共头部标题"
        # 传递xsrf_token
        self.data["xsrf_token"] = self.xsrf_token

    def render(self, template_name, **kwargs):
        super().render(template_name,**self.data)

class Home(HttpRequest):
    def get(self):
        self.data["text"] = '<a href="http://www.oldboyedu.com">老男孩</a>'
        self.render("5.html")

import json
class Login(HttpRequest):
    def prepare(self):
        self.json_argument = {}
        if self.request.method.lower() == "post":
            if self.request.headers.get("Content-Type") == "application/json":
                self.json_argument = json.loads(self.request.body)

    def post(self):
        print(self.json_argument.get("username"))
        print(self.json_argument.get("password"))
        self.write("user.login")

# 设置路由列表
urls = [
    (r"/", Home,{},'home'),
    (r"/user/login", Login,{},'login'),
]

if __name__ == "__main__":
    app = web.Application(urls, **settings)
    app.listen(port=8000)
    ioloop.IOLoop.current().start()

templates/5.html,代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <link rel="stylesheet" href="{{ static_url("main.css") }}">
</head>
<body>
    <p>{{ escape(text) }}</p>
    <p>{{ text }}</p>
    <p>{{ reverse_url("login") }}</p>
{#    <form method="post" action="{{ reverse_url("login") }}">#}
        {% module xsrf_form_html() %}
        username: <input type="text" name="username"><br><br>
        password: <input type="password" name="password"><br><br>
        <input type="submit" value="登录">
{#    </form>#}

    <script>
        var username = document.querySelector("input[name=username]");
        var password = document.querySelector("input[name=password]");
        var submit = document.querySelector("input[type=submit]");
        var _xsrf = '{{ xsrf_token }}';
        submit.addEventListener("click", ()=>{
            // 原生ajax写法
            xhr = new XMLHttpRequest();
            xhr.open("POST",'{{ reverse_url("login") }}');
            xhr.setRequestHeader("X-Xsrftoken",_xsrf);
            xhr.setRequestHeader("Content-Type","application/json");
            data = JSON.stringify({
                "username":username.value,
                "password":password.value,
            });
            xhr.send(data);
            // 监听ajax的执行状态
            xhr.addEventListener("readystatechange",()=>{
                if(xhr.statechange == 4 && xhr.status==200){
                    console.log(xhr.responseText );
                }
            });
        })
    </script>

</body>
</html>

内置变量

1
2
# 客户端请求对象
request

数据库

与Django框架相比,Tornado没有自带ORM,对于数据库需要自己去适配。我们使用MySQL数据库。

在Tornado3.0版本以前提供tornado.database模块用来操作MySQL数据库,而从3.0版本开始,此模块就被独立出来,作为torndb包单独提供。torndb只是对MySQLdb的简单封装,不支持Python 3。所以如果在当前版本中使用torndb进行数据库操作,需要修改源代码,所以在此,我们使用pymysql。

项目中如果要使用ORM,可以使用SQLAlchemy,但开发中,很少有人这么使用.

同时,tornado强大的地方在于其异步非阻塞,所以我们后面关于数据库操作,不管是mysql, mongodb还是redis基本都是异步读写操作。

MySQL

pip install pymysql

mysql.py,代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import pymysql

class MySQL(object):
    def __init__(self, host, user, pwd, name):
        self.host = host
        self.user = user
        self.pwd = pwd
        self.name = name
        self.data = None
        self.last_sql = None

    def connect(self):
        self.db = pymysql.Connect(host=self.host, user=self.user, passwd=self.pwd, db=self.name)
        self.cursor = self.db.cursor(cursor = pymysql.cursors.DictCursor)

    def close(self):
        self.cursor.close()
        self.db.close()

    def get_one(self, sql):
        try:
            self.connect()
            self.cursor.execute(sql)
            res = self.cursor.fetchone()
            self.data = res
            self.last_sql = sql
            self.close()
        except Exception as e:
            print("错误:%s" % e)

        # 链式模式
        return self

    def get_all(self, sql):
        try:
            self.connect()
            self.cursor.execute(sql)
            res = self.cursor.fetchall()
            self.last_sql = sql
            self.data = res
            self.close()
        except Exception as e:
            print("错误:%s" % e)
        return self

    def insert(self, sql):
        return self.execute(sql)

    def update(self, sql):
        return self.execute(sql)

    def delete(self, sql):
        return self.execute(sql)

    def execute(self, sql):
        try:
            self.connect()
            self.last_sql = sql
            count = self.cursor.execute(sql)
            self.db.commit()
            self.data = count
            self.close()
        except Exception as e:
            print("错误:%s" % e)
            self.db.rollback()
        return self

server.py,代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
from tornado import ioloop
from tornado import web
from mysql import MySQL
from tornado.web import Union,unicode_type,escape,utf8
settings = {
    'debug': True,
}
mysql = {
    "host": "127.0.0.1",
    "user": "root",
    "pwd": "123",
    "db": "student"
}
db = MySQL(mysql["host"], mysql["user"], mysql["pwd"], mysql["db"])

class HttpRequest(web.RequestHandler):
    # 默认情况下,tornado和flask以及django一样的,不能直接把列表数据转换成json结构
    def write(self, chunk: Union[str, bytes, dict, list]) -> None:
        if self._finished:
            raise RuntimeError("Cannot write() after finish()")
        if not isinstance(chunk, (bytes, unicode_type, dict, list)):
            message = "write() only accepts bytes, unicode, and dict objects"
            # if isinstance(chunk, list):
            #     message += (
            #         ". Lists not accepted for security reasons; see "
            #         + "http://www.tornadoweb.org/en/stable/web.html#tornado.web.RequestHandler.write"  # noqa: E501
            #     )
            raise TypeError(message)
        if isinstance(chunk, (dict, list)):
            chunk = escape.json_encode(chunk)
            self.set_header("Content-Type", "application/json; charset=UTF-8")
        chunk = utf8(chunk)
        self._write_buffer.append(chunk)

class Home(HttpRequest):
    def initialize(self):
        self.db = db

    def get(self):
        """查询一条数据"""
        # data = self.db.get_one("select * from tb_student where id = 10").data
        """查询多条数据"""
        # data = self.db.get_all("select * from tb_student where id > 5").data
        """添加一条数据"""
        # username = "xiaobai"
        # password = "123456"
        # nickname = "小白"
        # age = 16
        # sex = 1
        # email = "xiaobai@xiaobai.com"
        # sql = "INSERT INTO tb_student (username,password,nickname,age,sex,email) VALUES ('%s','%s','%s','%s','%s','%s');" % \
        #       (username,password,nickname, age,sex,email)
        # data = self.db.insert(sql).data
        # print(self.db.last_sql)
        # print(data)

        """添加多条"""
        # student_list = [
        #     {"username":"xiaohui1","password":"123456","nickname":"小辉1","age":16,"sex":1,"email":"xiaohuo1@qq.com"},
        #     {"username":"xiaohui2","password":"123456","nickname":"小辉2","age":16,"sex":1,"email":"xiaohuo2@qq.com"},
        #     {"username":"xiaohui3","password":"123456","nickname":"小辉3","age":16,"sex":1,"email":"xiaohuo3@qq.com"},
        #     {"username":"xiaohui4","password":"123456","nickname":"小辉4","age":16,"sex":1,"email":"xiaohuo4@qq.com"},
        # ]
        # table = "tb_student"
        #
        # fields = ",".join( student_list[0].keys() )
        # sql = "INSERT INTO %s (%s) VALUES " % (table,fields)
        #
        # for student in student_list:
        #     sql += "('%s','%s','%s','%s','%s','%s')," % \
        #            (student["username"],student["password"],student["nickname"],student["age"],student["sex"],student["email"])
        # sql = sql[:-1]
        # data = self.db.insert(sql).data
        # print(data)


        """更新"""
        # nickname = "小辉"
        # sql = "UPDATE tb_student set nickname='%s' WHERE id = 3" % (nickname)
        # data = self.db.update(sql).data
        # print(data)

        """删除"""
        # sql = "DELETE FROM tb_student WHERE id = 16"
        # data = self.db.delete(sql).data
        # print(data)

        self.write("Home.get")

# 设置路由列表
urls = [
    (r"/", Home),
]

if __name__ == "__main__":
    app = web.Application(urls, **settings)
    app.listen(port=8000)
    ioloop.IOLoop.current().start()

数据库建库语句:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 建库
create database student charset=utf8mb4;
# 切换数据库
use student;
# 建表
create table tb_student(
id int primary key auto_increment comment "主键",
username varchar(20) not null comment "姓名",
nickname varchar(20) not null comment "昵称",
password varchar(255) not null comment "密码",
age tinyint default 0 comment "年龄",
sex tinyint default 0 comment "性别",
email varchar(255) null comment "邮箱"
) comment="学生信息表";
# 测试数据
insert tb_student (username,nickname,password,age,sex,email) values
("xiaoming1","小明1号","123456",17,1,"xiaoming@qq.com"),
("xiaoming2","小明2号","123456",17,1,"xiaoming@qq.com"),
("xiaoming3","小明3号","123456",17,1,"xiaoming@qq.com"),
("xiaoming4","小明4号","123456",17,1,"xiaoming@qq.com"),
("xiaoming5","小明5号","123456",17,1,"xiaoming@qq.com"),
("xiaoming6","小明6号","123456",17,1,"xiaoming@qq.com"),
("xiaoming7","小明7号","123456",17,1,"xiaoming@qq.com"),
("xiaoming8","小明8号","123456",17,1,"xiaoming@qq.com"),
("xiaoming9","小明9号","123456",17,1,"xiaoming@qq.com"),
("xiaoming10","小明10号","123456",17,1,"xiaoming@qq.com"),
("xiaoming11","小明11号","123456",17,1,"xiaoming@qq.com");

同步和异步

概念

同步是指代在程序执行多个任务时,按部就班的依次执行,必须上一个任务执行完有了结果以后,才会执行下一个任务。

异步是指代在程序执行多个任务时,没有先后依序,可以同时执行,所以在执行上一个任务时不会等待结果,直接执行下一个任务。一般最终在下一个任务中通过状态的改变或者通知、回调的方式来获取上一个任务的执行结果。

同步

server.py,代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import time
def client_A():
    """模拟客户端A"""
    print('开始处理请求1-1')
    time.sleep(5)
    print('完成处理请求1-2')


def client_B():
    """模拟客户端B"""
    print('开始处理请求2-1')
    print('完成处理请求2-2')


def tornado():
    """模拟tornado框架"""
    client_A()
    client_B()


if __name__ == "__main__":
    tornado()

异步

server.py,代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from threading import Thread
from time import sleep

def async(func):
    def wrapper(*args, **kwargs):
        thread = Thread(target=func, args=args, kwargs=kwargs)
        thread.start()
    return wrapper

@async
def funcA():
    sleep(5)
    print("funcA执行了")

def funcB():
    print("funcB执行了")

def tornado():
    funcA()
    funcB()

if __name__ == "__main__":
    tornado()

协程

要理解什么是协程(Coroutine),必须先清晰迭代器生成器的概念。

迭代器

迭代器就是一个对象,一个可迭代的对象,是可以被for循环遍历输出的对象。当然专业的说,就是实现了迭代器协议的对象。

任何一个对象,只要类中实现了``iter()`就是一个可迭代对象(iterable)。

任何一个对象,只要类中实现了__iter__()__next__()就是一个迭代器(iterator)。

迭代器一定是可迭代对象,可迭代对象不一定是迭代器。

要了解迭代器,我们先编写一个代码来看看python提供的可迭代对象。常见的有:str,list ,tuple,dict,set,文件对象。

迭代器是惰性执行的,可以节省内存,不能反复, 只能向下取值。

server.py,代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 可迭代对象
# arr = [4,5,6,7]
# arr = "abcd"
# print(dir(arr))
# for item in arr:
#     print(item)

# 不可迭代对象
# num = 123
# print(dir(num))
# for item in num: # TypeError: 'int' object is not iterable
#     print(item)

# 自定义可迭代对象
class Colors(object):
    def __init__(self):
        self.data = ["红色", "橙色", "紫色", "黄色"]

    def __iter__(self):
        # __iter__ 必须有返回值,并且只能返回迭代器对象
        return self.data.__iter__()

colors = Colors()
print(dir(colors))
for item in colors:
    print(item)

查看一个对象是否是可迭代对象或迭代器:

1
2
3
4
5
6
from collections import Iterable, Iterator
data = [1,2,3,4]
print(isinstance(data,Iterable)) # True       # 查看是不是可迭代对象
print(isinstance(data,Iterator)) # False      # 查看是不是迭代器
print(isinstance(data.__iter__(),Iterator))   # True,
# 所有的迭代对象都有一个__iter__方法,该方法的作用就是返回一个迭代器对象

接下来,动手编写一个迭代器。

server.py,代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Num(object):
    def __init__(self,max):
        self.max = max
        self.current = 0

    def __next__(self):
        # print("current=",self.current)
        if self.current >= self.max:
            raise StopIteration

        self.current += 1
        return self.current

    def __iter__(self):
        return self

num = Num(3) # 迭代器
# print(dir(num))
# for的内部本质上就是不断调用了迭代器的__next__(),
# 并在遇到StopIteration异常以后,终止程序的执行
# for item in num:
#     print(item)

while True:
    try:
        print(num.__next__())
    except StopIteration:
        break

__iter__() 方法返回一个特殊的迭代器对象, 这个迭代器对象实现了 __next__() 方法并通过 StopIteration 异常标识迭代的完成。

__next__() 方法返回下一个迭代器对象。

StopIteration 异常用于标识迭代的完成,防止出现无限循环,在 __next__() 方法中可以设置在完成指定循环次数后触发 StopIteration 异常来结束迭代。

生成器

在 Python 中,使用了 yield 的函数被称为生成器函数。

生成器函数执行以后的返回结果就是生成器generator),是一种特殊的迭代器。生成器只能用于迭代操作。

yield 是一个python内置的关键字,它的作用有一部分类似return,可以返回函数的执行结果。但是不同的是,return 会终止函数的执行,yield 不会终止生成器函数的执行。两者都会返回一个结果,但return只能一次给函数的调用处返回值,而yield是可以多次给next()方法返回值,而且yield还可以接受外界send()方法的传值。所以,更准确的来说,yield是暂停程序的执行权并记录了程序当前的运行状态的标记符.同时也是生成器函数外界和内部进行数据传递的通道

server.py,代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def func():
    for item in [4,5,6]:
        return item

def gen1():
    for item in [4,5,6]:
        yield item

def gen2():
    key = 0
    print(">>>>> 嘟嘟,开车了")
    while True:
        food = yield "第%s次" % key
        print('接收了,%s'% food)
        key +=1

f = func()
print(f)
g1 = gen1()
print(g1)
for item in g1:
    print(item)

g2 = gen1()
print(g2)
print(next(g2))
print(next(g2))
print(g2.__next__())
# print(next(g2))

g3 = gen2()
g3.send(None) # g3.__next__() 预激活生成器,让生成器内部执行到第一个yield位置,否则无法通过send传递数据给内部的yield
for item in ["苹果","芒果"]:
    print(g3.send(item))

使用生成器可以让代码量更少,内存使用更加高效节约。

所以在工作中针对海量数据查询,大文件的读取加载,都可以考虑使用生成器来完成。因为一次性读取大文件或海量数据必然需要存放内容,而往往读取的内容大于内存则可能导致内存不足,而使用生成器可以像挤牙膏一样,一次读取一部分数据通过yield返回,每次yield返回的数据都是保存在同一块内存中的,所以比较起来肯定比一次性读取大文件内容来说,内存的占用更少。

yield 和 yield from

yield from 也叫委派生成器.委派生成器的作用主要是用于多个生成器之间进行嵌套调用时使用的.

server.py,代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def gen1():
    a = 0
    while True:
        # print("+++++++")
        a = yield a**2

def gen2(gen):
    yield from gen
    # a = 0
    # b = 1
    # gen.send(None)
    # while True:
    #     # print("-------")
    #     b = yield a
    #     a = gen.send(b)

if __name__ == '__main__':
    g1 = gen1()
    g2 = gen2(g1)
    g2.send(None)
    for i in range(5):
        # print(">>>> %s" % i)
        print(g2.send(i))
基于生成器来实现协程异步

这也是协程的实现原理,任务交替切换执行(遇到IO操作时进行判断任务切换才有使用价值, 当前此处我们使用的生成器实现的协程,是无法判断当前任务是否是遇到IO的,我们通过第三方模块: geventlet来实现判断是否遇到IO操作.)。

server.py,代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import time
def gen1():
    while True:
        print("--1")
        yield
        print("--2")
        time.sleep(1)

def gen2():
    while True:
        print("--3")
        yield
        print("--4")
        time.sleep(1)

if __name__ == "__main__":
    g1 = gen1()
    g2 = gen2()
    for i in range(3):
        next(g1)
        print("主程序!")
        next(g2)

异步操作数据库

MySQL

针对MySQL的异步操作,一般基于asyncio和aiomysql。其中,aiomysql 底层依赖于 pymysql,所以 aiomysql 并没有单独实现相应的连接驱动,而是在 pymysql 之上进行了封装。

pip install aiomysql
pymysql
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import asyncio
import aiomysql
settings = {
    "host":"127.0.0.1",
    "port": 3306,
    "db": "student",
    "password":"123",
    "user": "root",
}
async def main():
    pool = await aiomysql.create_pool(host=settings["host"], port=settings["port"],
                                      user=settings["user"], password=settings["password"],
                                      db=settings["db"], charset="utf8mb4")
    async with pool.acquire() as conn:
        async with conn.cursor() as cur:

            await cur.execute("SELECT id, username, nickname, email from tb_student")
            data = await cur.fetchone()
            
    pool.close()
    await pool.wait_closed()

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())
SQLAlchemy
1
 pip install sqlalchemy

server.py,代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import asyncio
import sqlalchemy as sa
import aiomysql
from aiomysql.sa import create_engine
settings = {
    "host":"127.0.0.1",
    "port": 3306,
    "db": "student",
    "password":"123",
    "user": "root",
}

metadata = sa.MetaData()
tbl = sa.Table('tb_student', metadata,
               sa.Column('id', sa.Integer, primary_key=True),
               sa.Column('username', sa.String(255)),
               sa.Column('nickname', sa.String(255)),
               sa.Column('password', sa.String(255),nullable=False),
               sa.Column('age', sa.Integer(),default=0,nullable=True),
               sa.Column('sex', sa.Boolean(),default=False,nullable=True),
               sa.Column('email', sa.String(255),default=None,nullable=True),
       )

async def main(loop):
    engine = await create_engine(user=settings["user"],
                                 db=settings["db"],
                                 host=settings["host"],
                                 password=settings["password"],
                                 port=settings["port"],
                                 charset="utf8mb4",
                                 loop=loop)
    async with engine.acquire() as conn:
        # 添加操作
        await conn.execute(tbl.insert().values(username='xiaohong',nickname="小红",password="123456",age=12,sex=False,email="xiaohong@qq.comn"))
        await conn.execute(tbl.insert().values(username='xiaolan',nickname="小兰",password="123456",age=13,sex=False,email="xiaolan@qq.comn"))
        # 查询操作[python3.8以后]
        ret = await conn.execute(tbl.select())
        async for row in ret:
            print(row)

    engine.close()
    await engine.wait_closed()

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main(loop))
tornado框架
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
from tornado import web,ioloop
import aiomysql,json

settings = {
    "debug": True,
    "mysql":{
        "host": "127.0.0.1",
        "port": 3306,
        "db": "student",
        "password": "123",
        "user": "root",
    }
}

class Home(web.RequestHandler):
    def prepare(self):
        # 读取配置中的数据库链接信息
        self.db = self.application.settings["mysql"]

        self.json_argument = {}
        if self.request.headers.get("Content-Type").lower() == "application/json":
            self.json_argument = json.loads(self.request.body)

    async def get(self):
        pool = await aiomysql.create_pool(host=self.db["host"], port=self.db["port"],
                                          user=self.db["user"], password=self.db["password"],
                                          db=self.db["db"], charset="utf8")
        async with pool.acquire() as conn:
            async with conn.cursor() as cur:
                await cur.execute("SELECT id, username, nickname, email, password, age from tb_student")
                print(cur.description)
                data = await cur.fetchone()
                print(data)

        pool.close()
        await pool.wait_closed()
        self.write('ok')

    async def post(self, *args, **kwargs):
        id = self.json_argument.get("id", "")
        username = self.json_argument.get("username", "")
        nickname = self.json_argument.get("nickname", "")
        password = self.json_argument.get("password", "")
        email = self.json_argument.get("email", "")
        age = self.json_argument.get("age", "")
        sex = int( self.json_argument.get("sex", 0) )

        async with aiomysql.create_pool(host=self.db["host"], port=self.db["port"],
                                        user=self.db["user"], password=self.db["password"],
                                        db=self.db["db"], charset="utf8mb4") as pool:
            async with pool.acquire() as conn:
                async with conn.cursor() as cur:
                    if not id:
                        sql = "INSERT INTO tb_student (username, nickname, password, email,age,sex) VALUES('{}','{}','{}','{}',{},{})".format(
                                username, nickname, password, email,age, sex)

                    else:
                        sql = "update tb_student set username='{}', nickname='{}', password='{}', email='{}', age={}, sex={} WHERE id={}".format(
                                username, nickname, password, email,age,sex, id)
                    print(sql)
                    await cur.execute(sql)
                    print(cur.description)
                    await conn.commit()

        self.write("ok")

def make_app():
    return web.Application(handlers=[
        ("/",Home),
    ],**settings)

if __name__ == '__main__':
    app = make_app()
    app.listen(8000)
    ioloop.IOLoop.current().start()

Redis

1
pip install aioredis

代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
from tornado import web,ioloop
import json,aioredis

settings = {
    "debug": True,
    "redis": "redis://:@127.0.0.1:6379/0",
}

class Home(web.RequestHandler):
    def prepare(self):
        self.json_argument = {}
        if self.request.method.lower() in ('post','put','patch'):
            if self.request.headers.get("Content-Type").lower() == "application/json":
                self.json_argument = json.loads(self.request.body)

    async def get(self):
        # 1. 直接使用通用执行方法execute操作任意命令,
        # 参数1: 命令名称(不区分大小写),
        # 后面就是命令的相关操作,按位置填写
        # redis = await aioredis.create_connection(self.application.settings["redis"])
        # await redis.execute("setex", "name", 30, "xiaoming",)
        # data = await redis.execute("get", "name")

        # 2. 也可以通过命令模式使用兼容写法,和之前操作pyredis一样的写法
        redis = await aioredis.create_redis(self.application.settings["redis"])
        await redis.setex("name",30,"xiaoming")
        data = await redis.get("name")
        print(data)
        self.write('ok')

def make_app():
    return web.Application(handlers=[
        ("/",Home),
    ],**settings)

if __name__ == '__main__':
    app = make_app()
    app.listen(8000)
    ioloop.IOLoop.current().start()

MongoDB

motor模块是对pymongo的异步封装.当然,github上也有一个叫motorengine的异步MongoDB的ORM模块,但是并不靠谱。

1
pip install motor

server.py,代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
from tornado import web,ioloop
import json
from motor import motor_tornado
settings = {
    "debug": True,
    "mongo": 'mongodb://localhost:27017'
}

class Home(web.RequestHandler):
    def prepare(self):
        self.json_argument = {}
        if self.request.method.lower() in ('post','put','patch'):
            if self.request.headers.get("Content-Type").lower() == "application/json":
                self.json_argument = json.loads(self.request.body)
        client = motor_tornado.MotorClient(self.application.settings["mongo"])
        self.student_list = client['mofang']['student_list']

    async def post(self):
        print( """添加一条数据""" )
        document = {"username": "xiaoming", "nickname": "小明","age":18}
        result = await self.student_list.insert_one(document)
        print(result.inserted_id)  # 成功则返回_id或主键
        print(result.acknowledged)  # 语句执行成功则返回True

        print("""添加多条数据""")
        document_list = [
            {"username": "xiaoming1", "nickname": "小明1号", "age": 15},
            {"username": "xiaoming2", "nickname": "小明2号", "age": 14},
            {"username": "xiaoming3", "nickname": "小明3号", "age": 18},
        ]
        result = await self.student_list.insert_many(document_list)
        print(result.inserted_ids)  # 成功则返回_id或主键列表
        print(result.acknowledged) # 语句执行成功则返回True
        self.write('ok')

    async def get(self):
        # query = {"username":"xiaoming"}
        # print("""获取一条数据""")
        # result = await self.student_list.find_one(query,{"_id":0})
        # print(result)
        # print("""获取多条数据""")
        # result = self.student_list.find(query,{"_id":0})
        #
        # async for document in result:
        #     print(document)

        print("""分页查询""")
        query = {}
        page = int(self.get_query_argument("page",default=1))
        per_page = 5
        result = self.student_list.find(query).limit(per_page).skip((page - 1) * per_page)
        data = {}
        data["count"] = await self.student_list.count_documents(query)
        data["items"] = []
        async for document in result:
            data["items"].append(document)
        print(data)
        self.write("ok")

    async def put(self):
        query = {"username": "xiaoming3"}
        updater = {"$set":{"nickname":"小光"}}
        result = await self.student_list.update_one(query,updater)
        print(result.modified_count)

    async def delete(self):
        query = {"username": "xiaoming"}
        result = await self.student_list.delete_one(query)
        # result = await self.student_list.delete_many(query)
        print(result.acknowledged)

def make_app():
    return web.Application(handlers=[
        ("/",Home),
    ],**settings)

if __name__ == '__main__':
    app = make_app()
    app.listen(8000)
    ioloop.IOLoop.current().start()

Tornado的协程

Tornado的异步编程也主要体现在网络IO的异步上,即异步Web请求。

异步Web请求客户端

Tornado提供了一个异步Web请求客户端tornado.httpclient.AsyncHTTPClient用来进行异步Web请求。

fetch(request)

用于执行一个web请求request,并异步返回一个tornado.httpclient.HTTPResponse响应。

request可以是一个url,也可以是一个tornado.httpclient.HTTPRequest对象。如果是url地址,fetch方法内部会自己构造一个HTTPRequest对象。

HTTPRequest

HTTP请求类,HTTPRequest的构造函数可以接收众多构造参数,最常用的如下:

  • url (string) – 要访问的url,此参数必传,除此之外均为可选参数
  • method (string) – HTTP访问方式,如“GET”或“POST”,默认为GET方式
  • headers (HTTPHeaders or dict) – 附加的HTTP协议头
  • body – HTTP请求的请求体

HTTPResponse

HTTP响应类,其常用属性如下:

  • code: HTTP状态码,如 200 或 404
  • reason: 状态码描述信息
  • body: 响应体字符串
  • error: 异常(可有可无)

基于gen.coroutine的协程异步

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from tornado import web,httpclient,gen,ioloop
import json
class Home(web.RequestHandler):
    @gen.coroutine
    def get(self):
        http = httpclient.AsyncHTTPClient()
        ip = "123.112.18.111"
        response = yield http.fetch("http://ip-api.com/json/%s?lang=zh-CN" % ip)
        if response.error:
            self.send_error(500)
        else:
            data = json.loads(response.body)
            if 'success' == data["status"]:
                self.write("国家:%s 省份: %s 城市: %s" % (data["country"], data["regionName"], data["city"]))
            else:
                self.write("查询IP信息错误")

# 设置路由列表
urls = [
    (r"/", Home),
]

if __name__ == "__main__":
    # 创建应用实例对象
    app = web.Application(urls, debug=True)
    # 设置监听的端口和地址
    app.listen(port=8888)
    # ioloop,全局的tornado事件循环,是服务器的引擎核心,start表示创建IO事件循环
    ioloop.IOLoop.current().start()

将异步Web请求单独抽取出来

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
from tornado import web,httpclient,gen,ioloop
import json


class Home(web.RequestHandler):

    @gen.coroutine
    def get(self):
        ip = "123.112.18.111"
        data = yield self.get_ip_info(ip)
        if data["status"] == 'success':
            self.write("国家:%s 省份: %s 城市: %s" % (data["country"], data["regionName"], data["city"]))
        else:
            self.write("查询IP信息错误")

    @gen.coroutine
    def get_ip_info(self,ip):
        http = httpclient.AsyncHTTPClient()
        response = yield http.fetch("http://ip-api.com/json/%s?lang=zh-CN" % ip)
        if response.error:
            rep = {"status": "fail"}
        else:
            rep = json.loads(response.body)

        raise gen.Return(rep)
        # 此处需要注意,生成器函数中是不能直接return返回数据的,否则出错,
        # 所以我们需要再此通过tornado 封装的异常对象gen.Return(rep)把结果进行抛出,让调用处进捕获

urls = [
    ("/",Home)
]

if __name__ == "__main__":
    app = web.Application(urls, debug=True)
    app.listen(port=8888)
    ioloop.IOLoop.current().start()

并行协程

Tornado可以同时并行执行多个协程异步,并发的异步可以使用列表或字典,如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
from tornado import web,httpclient,gen,ioloop
import json
class Home(web.RequestHandler):
    @gen.coroutine
    def get(self):
        ips = ["123.112.18.111",
               "112.112.233.89",
               "119.112.23.3",
               "120.223.70.76"]
        rep1, rep2 = yield [self.get_ip_info(ips[0]), self.get_ip_info(ips[1])]
        self.write_response(ips[0], rep1)
        self.write_response(ips[1], rep2)
        rep_dict = yield dict(rep3=self.get_ip_info(ips[2]), rep4=self.get_ip_info(ips[3]))
        self.write_response(ips[2], rep_dict['rep3'])
        self.write_response(ips[3], rep_dict['rep4'])

    def write_response(self,ip, rep):
        if 'success' == rep["status"]:
            self.write("IP:%s 国家:%s 省份: %s 城市: %s<br>" % (ip,rep["country"], rep["regionName"], rep["city"]))
        else:
            self.write("查询IP信息错误<br>")
    @gen.coroutine
    def get_ip_info(self, ip):
        http = httpclient.AsyncHTTPClient()
        response = yield http.fetch("http://ip-api.com/json/%s?lang=zh-CN" % ip)
        if response.error:
            rep = {"status":"fail"}
        else:
            rep = json.loads(response.body)
        raise gen.Return(rep)  # 此处需要注意

# 设置路由列表
urls = [
    (r"/", Home),
]

if __name__ == "__main__":
    # 创建应用实例对象
    app = web.Application(urls, debug=True)
    # 设置监听的端口和地址
    app.listen(port=8888)
    # ioloop,全局的tornado事件循环,是服务器的引擎核心,start表示创建IO事件循环
    ioloop.IOLoop.current().start()

改进版本,server.py,代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
from tornado import web,httpclient,gen,ioloop
import json


class Home(web.RequestHandler):

    @gen.coroutine
    def get(self):
        ips = ["123.112.18.111",
               "112.112.233.89",
               "119.112.23.3",
               "120.223.70.76"]
        
        # response_dict = yield {ip:self.get_ip_info(ip) for ip in ips}
        # for ip,response in response_dict.items():
        #     self.write_response(ip,response)

        for ip in ips:
            response = yield self.get_ip_info(ip)
            self.write_response(ip,response)

    def write_response(self, ip, rep):
        if rep["status"] == 'success':
            self.write("IP:%s 国家:%s 省份: %s 城市: %s<br>" % (ip, rep["country"], rep["regionName"], rep["city"]))
        else:
            self.write("查询IP信息错误<br>")

    @gen.coroutine
    def get_ip_info(self,ip):
        http = httpclient.AsyncHTTPClient()
        response = yield http.fetch("http://ip-api.com/json/%s?lang=zh-CN" % ip)
        if response.error:
            rep = {"status": "fail"}
        else:
            rep = json.loads(response.body)

        raise gen.Return(rep)
        # 此处需要注意,生成器函数中是不能直接return返回数据的,否则出错,
        # 所以我们需要再此通过tornado 封装的异常对象gen.Return(rep)把结果进行抛出,让调用处进捕获

urls = [
    ("/",Home)
]

if __name__ == "__main__":
    app = web.Application(urls, debug=True)
    app.listen(port=8888)
    ioloop.IOLoop.current().start()

Tornado的WebSocket

WebSocket是HTML5规范中新提出的客户端-服务器全双工通信协议,协议本身使用新的ws://URL格式。

WebSocket 是独立的、创建在 TCP 上的协议,和 HTTP 的唯一关联是使用 HTTP 协议与服务端进行三次握手以后,使用http的101状态码进行协议升级,使用的 TCP 端口是80,可以用于绕过大多数防火墙的限制。

WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端直接向客户端主动推送数据而不需要客户端进行再次请求,两者之间可以创建持久性连接,并允许数据进行双向通信。

Tornado提供支持WebSocket的模块是tornado.websocket,其中提供了一个WebSocketHandler类用来处理通讯。

常用方法

open()

当一个WebSocket连接建立后被调用。

on_message(message)

当客户端发送消息message过来时被调用,注意此方法必须被重写

on_close()

当WebSocket连接关闭后被调用。

write_message(message, binary=False)

向客户端发送消息messagea,message可以是字符串或字典(字典会被转为json字符串)。若binary为False,则message以utf8编码发送;二进制模式(binary=True)时,可发送任何字节码。

close()

关闭WebSocket连接。

check_origin(origin)

判断源origin,对于符合条件(返回判断结果为True)的请求源origin允许其连接,否则返回403。可以重写此方法来解决WebSocket的跨域请求(如始终return True)。

快速使用

server.py,代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from tornado import web,ioloop
from tornado.websocket import WebSocketHandler

class Index(web.RequestHandler):
    def get(self):
        self.render("templates/index.html")

class Home(WebSocketHandler):
    def open(self):
        # 
        self.write_message("欢迎来到socket.html")

    def on_message(self, message):
        print("接收数据:%s" % message)

    def on_close(self):
        print("socket连接断开")

    def check_origin(self, origin):
        return True  # 允许WebSocket的跨域请求

# 设置路由列表
urls = [
    (r"/", Index),
    (r"/home", Home),
]

if __name__ == "__main__":
    # 创建应用实例对象
    app = web.Application(urls, debug=True)
    # 设置监听的端口和地址
    app.listen(port=8888)
    # ioloop,全局的tornado事件循环,是服务器的引擎核心,start表示创建IO事件循环
    ioloop.IOLoop.current().start()

tempales/index.html,代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
<div id="content"></div>
<script>
    var content = document.querySelector("#content");
    var ws = new WebSocket("ws://127.0.0.1:8888/home"); // 新建一个ws连接
    ws.onopen = function() {  // 连接建立好后的回调
       ws.send("Hello, world");  // 向建立的连接发送消息
    };
    ws.onmessage = function (evt) {  // 收到服务器发送的消息后执行的回调
       content.innerHTML+=evt.data+"<br>";  // 接收的消息内容在事件参数evt的data属性中
    };
</script>
</body>
</html>

案例:聊天室

server.py,代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
from tornado import ioloop,web,httpserver,websocket,options
from datetime import datetime


class Index(web.RequestHandler):
    def get(self):
        self.render("templates/chat.html")

class Chat(websocket.WebSocketHandler):
    users = []
    def open(self):
        if self not in self.users:
            self.users.append(self)  # 建立连接后保存客户端的socket连接对象到users容器中
            key = list(self.users).index(self)
            for user in self.users:  # 向已在线用户发送消息
                user.write_message(
                "[%s]-%02d-[%s]-登录" % (self.request.remote_ip, key, datetime.now().strftime("%H:%M:%S")))

    def on_message(self, message):
        key = self.users.index(self)
        for user in self.users:  # 向在线用户广播消息
            user.write_message("[%s]-%02d-[%s]-发送:%s" % (
            self.request.remote_ip, key, datetime.now().strftime("%H:%M:%S"), message))

    def on_close(self):
        key = self.users.index(self)
        self.users.remove(self)  # 用户关闭连接后从容器中移除用户
        for user in self.users:
            user.write_message(
                "[%s]-%02d-[%s]-退出" % (self.request.remote_ip, key, datetime.now().strftime("%H:%M:%S")))

    def check_origin(self, origin):
        return True  # 允许WebSocket的跨域请求


urls = [
    (r"/", Index),
    (r"/chat", Chat),
]

if __name__ == '__main__':
    app = web.Application(urls,debug=True)
    server = httpserver.HTTPServer(app)
    server.listen(port=8080,address="0.0.0.0")
    server.start(1)
    ioloop.IOLoop.current().start()

templates/chat.html,代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>聊天室</title>
</head>
<body>
    <div>
        <textarea id="msg"></textarea>
        <button onclick="sendMsg()">发送</button>
    </div>
    <div id="content" style="height:500px;overflow:auto;"></div>
    <script>
        var ws = new WebSocket("ws://127.0.0.1:8888/chat");
        ws.onmessage = function(message) {
            console.log("接收数据:", message);
            content.innerHTML +="<p>" + message.data + "</p>";
        };

        function sendMsg() {
            console.log("发送数据:", msg.value);
            ws.send(msg.value);
            msg.value = "";
        }
    </script>
</body>
</html>

快速上手框架

1. Sanic
	中文文档:https://www.osgeo.cn/sanic/index.html
	官方地址:https://sanicframework.org/en/

孟浩鑫,汪伟,赵万里,

2. FastAPI
	官方中文文档:https://fastapi.tiangolo.com/zh/
	官方文档:https://fastapi.tiangolo.com/

俞昭志,韩瑞啸,孙健,宋健

3. Django3.0
	官网:http://djangoproject.com

张光旭,孙开玺,王永飞,王之国,安晓东,李泽鑫

图书管理的功能,学生管理的功能。