跳至主要內容

Tornado笔记

大约 13 分钟约 3821 字

Tornado框架简介

Tornado框架是一个强大的、可扩展的Web服务器。它在处理高并发的网络请求时显得很有优势,同时又足够轻量级。它还具有处理安全性、用户验证、社交网络以及与外部服务进行异步交互的特性。

安装Tornado框架很简单,只需要在venv虚拟环境中使用如下命令即可。

pip install tornado
安装Tornado框架
安装Tornado框架

Tornado框架主要分为四个部分。

  • Web框架。包括创建Web应用的RequestHandler类,还有很多其他支持的类。
  • HTTP(HTTPServer和AsyncHTTPClient)的客户端和服务器实现。
  • 异步网络库(IOLoop和IOStream),为HTTP组件提供构建模块,也可以用来实现其他协议。
  • 协程库(tornado.gen),允许以比链式回调更直接的方式编写异步代码。

一些Web开发的常见概念在本笔记中不再涉及,因为在Flask框架笔记和Django框架笔记都已提及。

第一个Tornado程序

import tornado.ioloop  # 主事件循环模块
import tornado.web  # Web框架模块

class MainHandler(tornado.web.RequestHandler):
    """GET请求"""

    def get(self):  # 处理get请求
        self.write('<h1>第一个Tornado程序</h1>')  # 输出字符串

def make_app():
    """创建Tornado应用"""
    return tornado.web.Application([(r'/', MainHandler)])  # 设置路由

if __name__ == '__main__':
    app = make_app()  # 创建Tornado应用
    app.listen(8888)  # 设置监听端口
    print('在端口8888上运行Tornado程序')  # 输出提示信息
    tornado.ioloop.IOLoop.current().start()  # 启动服务

运行此程序后,在浏览器中输入localhost:8888就可以看到创建的网页了。

运行结果
运行结果

路由

在上方代码中,于12行处配置了一个默认路由“/”,链接到MainHandler,由它的get()方法处理get请求。我们注意到,在handlers关键字参数中传递了一个列表作为参数。它实际上就是配置路由的参数。我们可以在这里面指定更多路由,如下。

tornado.web.Application(handlers=[
    ('/', MainHandler),  # 首页路由
    ('/login', LoginHandler),  # 登录路由
    ('/register', RegisterHandler)  # 注册路由
])

Application()还接受如下参数。

tornado.web.Application(handlers=None, default_host=None, transforms=None, **setting)

handlers参数以一个列表为参数,列表中的元素为一个二元组。第一个元素为一个正则表达式,tornado会根据路由匹配的正则表达式寻找对应的处理程序。第二个元素为一个RequestHandler类,它里面的get和post等方法就是处理对应请求的程序。第一个正则表达式不会匹配查询字符串和锚点。Tornado把这些正则表达式看作已经包含了行开始和结束的锚点(即如“/”的路由实际上为“^/%”)。

import tornado.ioloop  # 主事件循环模块
import tornado.web  # Web框架模块

class LoginHandler(tornado.web.RequestHandler):
    def get(self):
        self.write('<h1>登录页面</h1>')

class MainHandler(tornado.web.RequestHandler):
    """GET请求"""

    def get(self):
        self.write('<h1>首页</h1>')  # 输出字符串

def make_app():
    """创建Tornado应用"""
    return tornado.web.Application([
        (r'/', MainHandler),
        (r'/login', LoginHandler)
    ])  # 设置路由
    
if __name__ == '__main__':
    app = make_app()  # 创建Tornado应用
    app.listen(8888)  # 设置监听端口
    print('在端口8888上运行Tornado程序')  # 输出提示信息
    tornado.ioloop.IOLoop.current().start()  # 启动服务
运行结果
运行结果

HTTP方法

RequestHandler内置了很多方法,我们自定义的Handler类中使用的get方法就是重写了父类的方法。下面使用一个例子介绍如何处理POST请求。

import tornado.ioloop  # 主事件循环模块
import tornado.web  # Web框架模块

class LoginHandler(tornado.web.RequestHandler):
    def get(self):
        self.write('登录页面')

    def post(self):
        username = self.get_argument('username')
        password = self.get_argument('password')
        self.write(f'用户名:{username}, 密码:{password}')

class MainHandler(tornado.web.RequestHandler):
    """GET请求"""

    def get(self):
        self.write('<h1>首页</h1>')  # 输出字符串

def make_app():
    """创建Tornado应用"""
    return tornado.web.Application([
        (r'/', MainHandler),
        (r'/login', LoginHandler)
    ])  # 设置路由

if __name__ == '__main__':
    app = make_app()  # 创建Tornado应用
    app.listen(8888)  # 设置监听端口
    print('在端口8888上运行Tornado程序')  # 输出提示信息
    tornado.ioloop.IOLoop.current().start()  # 启动服务

在浏览器中直接访问login路由。

运行结果
运行结果

使用cURL工具测试登录功能。

运行结果
运行结果

模板

Tornado的模板存放在tornado.template模块中,使用模板可以简化Web开发。

渲染模板

使用模板时,需要先在应用中设置template_path路径,然后使用render()函数渲染模板。

首先先创建一个main.py文件。

import os.path

import tornado.ioloop
import tornado.web

class LoginHandler(tornado.web.RequestHandler):
    def get(self):
        self.render('login.html')   # 渲染模板文件

    def post(self):
        username = self.get_argument('username')
        password = self.get_argument('password')
        self.write(f'用户名:{username},密码:{password}')

def make_app():
    return tornado.web.Application(handlers=[
        ('/login', LoginHandler)
    ], template_path=os.path.join(os.path.dirname(__file__), 'templates'))  # 创建模板

if __name__ == '__main__':
    app = make_app()
    app.listen(8888)
    print('开始监听8888端口')
    tornado.ioloop.IOLoop.current().start()

然后在main.py同级目录下创建一个templates文件夹,在里面创建login.html文件,代码如下。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录页面</title>
</head>
<body>
<form action="/login" method="post">
    <label for="username">用户名:</label><input type="text" name="username" id="username">
    <label for="password">密码:</label><input type="password" name="password" id="password">
    <input type="submit" value="登录">
</form>
</body>
</html>

然后运行main.pyopen in new window,现在浏览器访问127.0.0.1:8888/login,看到如下的页面。

运行结果
运行结果

然后输入用户名和密码之后点击登录,出现如下图的提示。

运行结果
运行结果

模板语法

Tornado支持在HTML中嵌入控制语句和表达式。控制语句的格式与Jinja2的语法类似,需要包含在“{%”和“%}”之间,例如“{% if bool %}”。表达式则要包含在{{}}之间,如{{ item[0] }}。控制语句和Python的语句类似,支持if、for、while、try,且需要用“{% end %}”结束,如下。

<html>
    <head>
        <title>{{ title }}</title>
    </head>
    <body>
        <ul>
            {% for item in list %}
            <li>{{ item }}</li>
            {% end %}
        </ul>
    </body>
</html>

提供静态文件

当编写网页时,用到js和css等静态文件时,可以使用Tornado提供的一种简便的使用方式引用静态资源。

第一步就是在Application的构造函数中传递一个叫做static_path的关键字参数,它用于引用所有静态文件的根目录。第二步就是在HTML模板中使用Tornado提供的static_path函数生成静态文件的URL。具体请看下面的例子。

首先创建一个main.py文件。

import os.path
import tornado.ioloop
import tornado.web

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

    def post(self):
        username = self.get_argument('username')
        password = self.get_argument('password')
        self.write(f'用户名:{username},密码:{password}')

def make_app():
    return tornado.web.Application(handlers=[
        ('/login', MainHandler)
    ],
        static_path=os.path.join(os.path.dirname(__file__), "static"),
        template_path=os.path.join(os.path.dirname(__file__), 'templates'))

if __name__ == '__main__':
    app = make_app()
    app.listen(8888)
    print('开始监听8888端口')
    tornado.ioloop.IOLoop.current().start()

然后创建模板文件,代码如下。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录页面</title>
    <link rel="stylesheet" href="{{ static_url('style.css')}}"><script src="{{ static_url('main.js')}}"></script>
</head>
<body>
<form action="/login" method="post">
    <label for="username">用户名:</label><input type="text" name="username" id="username">
    <label for="password">密码:</label><input type="password" name="password" id="password">
    <input type="submit" value="登录">
</form>
</body>
</html>

其中引用样式表的那行,使用了Tornado内置框架的static_url函数,然后参数就是文件名或者路径名。

这个style.css位于和main.py同级目录下的static文件夹内。

异步与协程

Tornado是一款可以定制的非阻塞式的异步加载框架,为了充分利用Tornado的特性,需要先理解几个概念。

基本概念

  1. 阻塞

程序未得到所需资源而处于挂起的状态成为阻塞。

程序在等待某个操作完成期间,自身无法干别的事情,则称该程序在该操作上是阻塞的。最典型的例子就是Python使用input函数时,在未输入内容时下方的代码都无法执行,这就是阻塞。

  1. 非阻塞

程序在等待某操作过程中可以继续干别的事情,则称该程序在该操作上是非阻塞的。阻塞是无处不在的,而非阻塞并不是无处不在的。它只有程序封装的级别可以囊括独立的子程序单元时它才可能存在非阻塞状态。非阻塞的存在是因为某个操作因为是阻塞状态时会很浪费时间,效率低下,才将它变为非阻塞的。

  1. 同步

不同程序单元为了完成某个任务,在执行过程中需要靠某种通信方式来协调一致。如购物商场中更新商品库存,就需要用“行锁”作为通信信号,让不同的更新请求强制排队顺序执行。

  1. 异步

不同任务在完成某一任务时不需要反复通过通信确认一致就称为异步。不相关的程序之间为异步的。如爬虫爬取网页,调度程序调用下载程序时,即可调度其他任务,无需与该任务保持一致;不同网页的下载、保存等操作都是无关的。

  1. 并发

并发描述的是程序的组织结构。程序要被设计成多个可独立执行的子程序,以最大化利用有限的CPU资源,是多个任务几乎同时执行。

  1. 并行

并行描述的是程序的执行状态。指多个任务同时执行,以利用有限的资源加速完成某个任务。并发是一种程序执行方式,使用它可以大大加快程序的执行效率,但它不是必须的。

asyncio模块

asyncio模块是Python用来编写并发代码的内置库,使用async/wait语法。Python有很多高性能异步库都是基于asyncio开发的,包括网络和网站服务,数据库连接库、分布式任务队列。asyncio往往是构建IO密集型和高层级结构化网络代码的最佳选择。

asyncio提供了一组高层级API,用于以下任务执行。

  • 并发地执行Python协程,并对其执行过程实现完全控制。
  • 执行网络IO和IPC。
  • 控制子进程。
  • 通过队列实现分布式任务。
  • 同步并发代码。

此外,还有一些低级API用以支持库和框架的开发者实现。

  • 创建和管理事件循环,以提供异步API,用于网络化、运行子进程、处理OS信号等。
  • 使用transports实现高效率协议。
  • 通过async/await语法桥接基于回调的库和代码。

下面通过一个例子介绍asyncio的基本使用。

import asyncio

async def worker_1():
    print('函数worker_1开始运行')
    await asyncio.sleep(1)
    print('函数worker_1执行完毕')

async def worker_2():
    print('函数worker_2开始执行')
    await asyncio.sleep(2)
    print('函数worker_2执行完毕')

async def main():
    task1 = asyncio.create_task(worker_1())
    task2 = asyncio.create_task(worker_2())
    print('await之前')
    await task1
    print('已经await task1')
    await task2
    print('已经await task2')

if __name__ == '__main__':
    asyncio.run(main())

上述代码的执行步骤如下:

  1. if main部分开始执行。asyncio.run()先执行,程序进入main()函数,开启事件循环。
  2. task1和task2被创建,并进入事件循环,等待运行,输出“await之前”。
  3. await task1被执行,用户选择从当前的主任务中切出,时间调度器开始调度worker_1。
  4. worker_1开始执行,运行到await asyncio.sleep(1)时,从当前任务切出,时间调度器开始调度worker_2。
  5. worker_2开始执行,运行到await asyncio.sleep(2)时,从当前任务切出,事件调度器开始在这个时候暂停调度。
  6. 1s后,worker_1的sleep完成,事件调度器将控制权重新传给task_1,输出“函数worker_1执行完毕”。task1完成任务,从事件调度器中退出。
  7. await task1完成后,事件调度器将控制权重新传给主任务,输出“已经await task1”,然后在await task2处继续等待;2s后,worker_2的sleep完成,事件调度器将控制权重新传给task2,输出“函数worker_2执行完毕”,task2完成任务,从事件循环中退出。
  8. 主任务输出“已经await task2”,协程全任务结束,事件循环结束。

输出结果如下。

Tornado框架的gen模块

Tornado框架使用Tornado.gen实现基于生成器的协程程序。协程程序提供了一种在异步环境中工作比链接回调更简单的方法。使用协程的代码在技术上是异步的,但它是作为单个生成器而不是单独的函数集合编写的。

例如,一个基于协程的处理程序代码如下。

from tornado import gen

class GenAsyncHandler(RequestHandler):
    @gen.coroutine
    def get(self):
        http_client = AsyncHTTPClient()
        response = yield http_client.fetch('https://example.com')
        do_something_with_response(response)
        self.render('template.html')

上述代码中,@gen.coroutine是异步生成器的装饰器,带有此修饰符的函数将返回Future对象。

还可以生成其他可扩展对象的列表和字典,这些对象将同时启动并并行运行,结果列表或字典将在完成后返回。如:

@gen.coroutine
def get(self):
    http_client = AsyncHTTPClient()
    response1, response2 = yield [http_client.fetch(url1),
                                 http_client.fetch(url2)]
    response_dict = yield dict(response3=http_client.fetch(url3),
                              response4=http_client.fetch(url4))
    response3 = response_dict['response3']
    response4 = response_dict['response4']

此外,如果引发异常,可以使用gen.Return()将其值参数作为协程的结果返回,如下:

@gen.coroutine
def fetch_json(url):
	response = yield AsyncHTTPClient().fetch(url)
    raise gen.Return(json_decode(response.body))

操作MySQL数据库

Tornado-MySQL是对PyMySQL异步化的一个第三方库,使用它可以在Tornado框架中操作MySQL数据库。

使用pip install Tornado-MySQL安装Tornado-MySQL库。

若MySQL版本高于8.0,需要打开tornado-mysql库下的charset.py文件,新增如下代码才能连接数据库。

# Connect to MySQL8.0+
_charsets.add(Charset(244, 'utf8mb4', 'utf8mb4_german2_ci', ''))
_charsets.add(Charset(245, 'utf8mb4', 'utf8mb4_croatian_ci', ''))
_charsets.add(Charset(246, 'utf8mb4', 'utf8mb4_unicode_520_ci', ''))
_charsets.add(Charset(247, 'utf8mb4', 'utf8mb4_vietnamese_ci', ''))
_charsets.add(Charset(248, 'gb18030', 'gb18030_chinese_ci', 'Yes'))
_charsets.add(Charset(249, 'gb18030', 'gb18030_bin', ''))
_charsets.add(Charset(250, 'gb18030', 'gb18030_unicode_520_ci', ''))
_charsets.add(Charset(255, 'utf8mb4', 'utf8mb4_0900_ai_ci', ''))

我在实际运行中没有解决这个问题,应该是Tornado-MySQL客户端版本太低导致的,若实在不行也可以使用PyMySQL连接MySQL。

使用Tornado-MySQL连接MySQL的步骤如下。

  • 首先导入Tornado-MySQL库,使用tornado.gen创建基于生成器的协程程序,在协程程序中调用tornado_mysql.connect()函数连接MySQL数据库,代码如下。
from tornado import gen, ioloop
import tornado_mysql


@gen.coroutine
def main():
    conn = yield tornado_mysql.connect(host='127.0.0.1', port=3306, user='root', password='db', db='mysql')
    cursor = conn.cursor()
    yield cursor.execute('SELECT Host, User FROM user')
    for row in cursor:
        print(row)
    cursor.close()
    conn.close()

ioloop.IOLoop.current().run_sync(main)

其他关于MySQL的步骤与PyMySQL类似,这里不再赘述。

操作Redis数据库

为了充分利用Tornado框架的异步效果,一般与非关系型数据库结合使用,使用最广泛的就是Redis数据库。

使用Tornado-Redis就可以连接Redis数据库。该库为Tornado框架可用的异步Redis客户端,它是一个第三方库,可以使用pip install Tornado-Redis安装。需要注意,Tornado 5.0.2以上版本与Tornado-Redis不兼容,会提示“ImportError: cannot import name 'stack_context' from tornado”。

Tornado-Redis有两种连接方式。

  1. 普通连接方式

首先导入tornadoredis,然后调用Client()方法连接客户端。示例代码如下:

import tornadoredis

c = tornadoredis.Client(host='localhost', port=6379)
c.set('name', 'qi1')

若运行上述代码中出现如下错误:

TypeError: __init__() got an unexpected keyword argument 'io_loop'

需要在虚拟环境中找到Tornado-MySQL安装路径,然后修改tornadoredis下的client.pyopen in new window,找到如下代码:

self.__stream = IOStream(sock, io_loop=self.io_loop)

去掉后面的关键字参数即可。

  1. 连接池方式

首先导入tornadoredis,然后调用tornadoredis的ConnectionPool()方法连接客户端,示例代码如下:

import tornadoredis

CONNECTION_POOL = tornadoredis.ConnectionPool(max_connections=100, wait_for_available=True)
c = tornadoredis.Client(host='localhost', port=6379, connection_pool=CONNECTION_POOL)
c.set('age', 18)
上次编辑于:
贡献者: QI