Tornado简单教程
1. Tornado
1.1 特点
非阻塞式和基于Linux的Epoll(UNIX为kqueue)的异步网络IO
异步非阻塞IO处理方式,单进程单线程异步IO的网络模型,可以编写异步非阻塞的程序
非常适合开发长轮询、WebSocket和需要与每个用户建立持久连接的应用
既是WebServer也是WebFramework
1.2 结构
Web 框架 (包括用来创建 Web 应用程序的 RequestHandler 类, 还有很多其它支持的类).
HTTP 客户端和服务器的实现 (HTTPServer 和 AsyncHTTPClient).
异步网络库 (IOLoop 和 IOStream), 对 HTTP 的实现提供构建模块, 还可以用来实现其他协议.
协程库 (tornado.gen) 让用户通过更直接的方法来实现异步编程, 而不是通过回调的方式.
1.3 三个底层核心模块
httpserver 服务于web模块的一个简单的HTTP服务器的实现
Tornado的HTTPConnection类用来处理HTTP请求,包括读取HTTP请求头、读取POST传递的数据,调用用户自定义的处理方法,以及把响应数据写给客户端的socket。
`iostream` 对非阻塞式的`socket`的封装以便于常见读写操作 为了在处理请求时实现对`socket`的**异步读写**,Tornado实现了`IOStream`类用来处理`socket`的异步读写。 `ioloop` 核心的I/O循环 Tornado为了实现**高并发和高性能**,使用了一个`IOLoop`事件循环来处理`socket`的读写事件,`IOLoop`事件循环是**基于Linux的`epoll`模型**,可以高效地响应网络事件,这是Tornado高效的基础保证。
2.安装
2.1 安装python3
略过
2.2 安装tornado
pip install tornado
2.3 编写简单server
#! /usr/bin/python # encoding:utf-8 # 导入Tornado模块 import tornado.ioloop #核心IO循环模块 import tornado.httpserver #异步非阻塞HTTP服务器模块 import tornado.web #Web框架模块 import tornado.options #解析终端参数模块 #从终端模块中导出define模块用于读取参数,导出options模块用于设置默认参数 from tornado.options import define, options # 定义端口用于指定HTTP服务监听的端口 # 如果命令行中带有port同名参数则会称为全局tornado.options的属性,若没有则使用define定义。 define("port", type=int, default=8000, help="run on the given port") # 创建请求处理器 # 当处理请求时会进行实例化并调用HTTP请求对应的方法 class IndexHandler(tornado.web.RequestHandler): # 定义get方法对HTTP的GET请求做出响应 def get(self): # 从querystring查询字符串中获取id参数的值,若无则默认为0. id = self.get_argument("id", 0) # write方法将字符串写入HTTP响应 self.write("hello world id = " + str(id)) # 创建路由表 urls = [(r"/", IndexHandler),] # 定义服务器 def main(): # 解析命令行参数 tornado.options.parse_command_line() # 创建应用实例 app = tornado.web.Application(urls) # 监听端口 app.listen(options.port) # 创建IOLoop实例并启动 tornado.ioloop.IOLoop.current().start() # 应用运行入口,解析命令行参数 if __name__ == "__main__": # 启动服务器 main()
测试结果:
2.4 运行流程
1.Tornado中Application应用类是Handler处理器的集合
Application类的__init__初始化函数原型
# 原型 def __init__(self, handlers=None, default_host="", transforms=None, wsgi=False, **settings):
2.Tornado的HTTPServer会负责解析用户的HTTPRequest,构造一个request对象。并交给RequestHandler处理,Request的解析是一个规划化的流程,针对Request的处理函数RequestHandler是被自定义的重点部分。
3.由于HTTP是工作在TCP协议之上的,HTTPServer其实是TCPServer的派生类,常规socket编程中启动一个TCPServer有三个必备步骤:
a) 创建socket
b) 绑定指定地址的端口
c) 执行监听
TCPServer类的实现借鉴UNIX/Linux中的Socket机制,也必然存在上述步骤,这几个步骤都是在HTTPServer.listen()
函数调用时完成的。
server.listen(options.port)
listen
函数的参数是端口号,端口定义可通过define
来定义。
from tornado.options import define, options define("port", default=8888, help="run on the given port", type=int)
define函数是OptionParser类的成员,定义在tornado/options.py文件中,机制于parse_command_line()类似。define定义端口port或,port变量会被存放在options对象的directory成员中,因此可直接使用options.port访问。
4.当使用server.listen(options.port)后,服务器就会在端口上启动一个服务,并开始监听客户端的连接。对于常规的Socket操作,listen之后的操作应该是accept。
在Tornado中accept操作是这样的:
tornado.ioloop.IOLoop.current().start()
IOLoop是什么呢?IOLoop于TCPServer之间的关系其实很简单。例如使用C语言编写TCP服务器时,编写完create-bind-listen三段式之后,都需要编写accept/recv/send处理客户端请求。通常会写一个无限循环,不断调用accept来响应客户端连接,其实这个无线循环就是Tornado中的IOLoop。
IOLoop会负责accept这一步,对于recv/send操作通常也是在一个循环中进行的,也可以抽象成IOLoop。
最后,简单梳理下整个流程:当我们使用在客户端浏览器地址栏中输入http://127.0.0.1:8000?id=1000时,浏览器首先会连接服务器 ,将HTTP请求发送到HTTPServer中,HTTPServer会先解析请求parse request,然后将请求request交给第一个匹配到的处理器Handler。处理器Handler会负责组织数据并调用发送API将数据发送到客户端。
3.核心组件
Tornado的Web服务器通常包含四大组件
3.1 ioloop实例
tornado.ioloop是全局Tornado的IO事件循环,是服务器的引擎核心。
tornado.ioloop是核心IO循环模块,封装了Linux的epoll和BSD的kqueue,是Tornado高性能处理的核心。
tornado.ioloop.IOLoop.current()返回当前线程的IOLoop实例对象
tornado.ioloop.IOLoop.current().start() 用于启动IOLoop实例对象的IO循环并开启监听
# 加载Tornado核心IO事件循环模块 import tornado.ioloop # 默认Tornado的ioloop实例 tornado.ioloop.IOLoop.current()
3.2 app实例
app
实例代表了一个完成的后端应用,它会挂接一个服务端套接字端口并对外提供服务,一个ioloop
事件循环实例中可以包含多个app
实例。
# 创建应用实例 app = tornado.web.Application(urls) # 监听端口 app.listen(options.port)
3.3 urls路由表
路由表用于将指定URL规则和处理器Handler挂接起来形成路由映射表,当请求到来时会根据请求的访问URL查询路由映射表来查询对应业务的处理器Handler
。
urls = [(r"/", MainHandler),]
3.4 handler类
handler
类代表着业务逻辑,在进行服务端开发时也就是在编写处理器,用以服务客户端请求。
# 当处理请求时会进行实例化并调用HTTP请求对应的方法 class MainHandler(tornado.web.RequestHandler): # 定义get方法对HTTP的GET请求做出响应 def get(self): # 从querystring查询字符串中获取id参数的值,若无则默认为0. id = self.get_argument("id", 0) # write方法将字符串写入HTTP响应 self.write("hello world id = " + id)
四大组件的关系
- 一个IO事件循环ioloop可以包含多个应用app,即可以管理多个服务端口
- 一个应用app可以包含一个路由表urls
- 一个路由表urls可以包含多个处理器Handler
ioloop是服务的引擎核心是发动机,负责接收和响应客户端请求,负责驱动业务处理器handler的运行,负责服务器内部定时任务的执行。同一个ioloop实例会运行在一个单线程环境下。
ioloopioloop是服务的引擎核心是发动机,负责接收和响应客户端请求,负责驱动业务处理器handler的运行,负责服务器内部定时任务的执行。同一个ioloop实例会运行在一个单线程环境下。
当一个请求到来时,IO事件循环ioloop会读取请求并解包形成 一个HTTP请求对象,并找到该套接字上对应应用app的路由表urls,通过请求对象的URL查询路由表中挂接的处理器Handler,然后执行处理器Handler。handler处理器执行后会返回一个对象,ioloop负责将对象包装成HTTP响应对象并序列化发送给客户端。
4.异步
一个简单的同步函数:
from tornado.httpclient import HTTPClient def synchronous_fetch(url): http_client = HTTPClient() response = http_client.fetch(url) return response.body
这时同样的函数但是被通过回调参数方式的异步方法重写了:
from tornado.httpclient import AsyncHTTPClient def asynchronous_fetch(url, callback): http_client = AsyncHTTPClient() def handle_response(response): callback(response.body) http_client.fetch(url, callback=handle_response)
再一次 通过 Future
替代回调函数:
from tornado.concurrent import Future def async_fetch_future(url): http_client = AsyncHTTPClient() my_future = Future() fetch_future = http_client.fetch(url) fetch_future.add_done_callback( lambda f: my_future.set_result(f.result())) return my_future
原始的 Future 版本十分复杂, 但是 Futures 是 Tornado 中推荐使用的一种做法, 因为它有两个主要的优势. 错误处理时通过 Future.result 函数可以简单的抛出一个异常 (不同于某些传统的基于回调方式接口的 一对一的错误处理方式), 而且 Futures 对于携程兼容的很好.
5.协程
Tornado 中推荐用 协程 来编写异步代码. 协程使用 Python 中的关键字 yield 来替代链式回调来实现挂起和继续程序的执行(像在 gevent 中使用的轻量级线程合作的方法有时也称作协程, 但是在 Tornado 中所有协程使用异步函数来实现的明确的上下文切换).
协程和异步编程的代码一样简单, 而且不用浪费额外的线程, . 它们还可以减少上下文切换 让并发更简单 .
from tornado import gen @gen.coroutine def fetch_coroutine(url): http_client = AsyncHTTPClient() response = yield http_client.fetch(url) # 在 Python 3.3 之前的版本中, 从生成器函数 # 返回一个值是不允许的,你必须用 # raise gen.Return(response.body)来代替 return response.body
6.async 和 await
从 Tornado 4.3 开始, 在协程基础上你可以使用这些来代替 yield. 简单的通过使用 async def foo() 来代替 @gen.coroutine 装饰器, 用 await 来代替 yield. 文档的剩余部分还是使用 yield 来兼容旧版本的 Python, 但是 async 和 await 在可用时将会运行的更快:
async def fetch_coroutine(url): http_client = AsyncHTTPClient() response = await http_client.fetch(url) return response.body
await 关键字并不像 yield 更加通用. 例如, 在一个基于 yield 的协程中你可以生成一个列表的 Futures, 但是在原生的协程中你必须给列表报装 tornado.gen.multi. 你也可以使用 tornado.gen.convert_yielded 将使用 yield 的任何东西转换成用 await 工作的形式.
虽然原生的协程不依赖于某种特定的框架 (例如. 它并没有使用像 tornado.gen.coroutine 或者 asyncio.coroutine 装饰器), 不是所有的协程都和其它程序兼容.这里有一个 协程运行器 在第一个协程被调用时进行选择, 然后被所有直接调用 await 的协程库共享. Tornado 协程运行器设计时就时多用途且可以接受任何框架的 awaitable 对象. 其它协程运行器可能会有更多的限制(例如, asyncio 协程运行器不能接收其它框架的协程). 由于这个原因, 我们推荐你使用 Tornado 的协程运行器来兼容任何框架的协程. 在 Tornado 协程运行器中调用一个已经用了asyncio协程运行器的协程,只需要用 tornado.platform.asyncio.to_asyncio_future 适配器.
6.1 如何工作的
一个含有 yield 的函数时一个 生成器 . 所有生成器都是异步的; 调用它时将会返回一个对象而不是将函数运行完成. @gen.coroutine 修饰器通过 yield 表达式通过产生一个 Future 对象和生成器进行通信.
装饰器从生成器接收一个 Future 对象, 等待 (非阻塞的) Future 完成, 然后 “解开” Future 将结果像 yield 语句一样返回给生成器. 大多数异步代码从不直接接触到 Future 类, 除非 Future 立即通过异步函数返回给 yield 表达式.
6.2 怎样调用
协程在一般情况下不抛出异常: 在 Future 被生成时将会把异常报装进来. 这意味着正确的调用协程十分的重要, 否则你可能忽略很多错误:
@gen.coroutine def divide(x, y): return x / y
近乎所有情况中, 任何一个调用协程自身的函数必须时协程, 通过利用关键字 yield
来调用. 当你在覆盖了父类中的方法, 请查阅文档来判断协程是否被支持 ( 文档中应该写到那个方法 “可能是一个协程” 或者 “可能返回一个 Future
”):
@gen.coroutine def good_call(): # yield will unwrap the Future returned by divide() and raise # the exception. yield divide(1, 0)
有时你并不想等待一个协程的返回值. 在这种情况下我们推荐你使用 IOLoop.spawn_callback
, 这意味着 IOLoop
负责调用. 如果它失败了, IOLoop
会在日志中记录调用栈:
# The IOLoop will catch the exception and print a stack trace in # the logs. Note that this doesn't look like a normal call, since # we pass the function object to be called by the IOLoop. IOLoop.current().spawn_callback(divide, 1, 0)
7.协程模式
7.1 结合 callbacks
为了使用回调来代替 Future
与异步代码进行交互, 讲这个调用报装在 Task
中. 这将会在你生成的 Future
对象中添加一个回调参数:
@gen.coroutine def call_task(): # Note that there are no parens on some_function. # This will be translated by Task into # some_function(other_args, callback=callback) yield gen.Task(some_function, other_args)
7.2 调用阻塞函数
在协程中调用阻塞函数的最简单方法时通过使用 ThreadPoolExecutor
, 这将返回与协程兼容的 Futures
thread_pool = ThreadPoolExecutor(4) @gen.coroutine def call_blocking(): yield thread_pool.submit(blocking_func, args)
7.3 交叉存取技术
有时保存一个 Future
比立刻yield它更有用, 你可以在等待它之前执行其他操作:
@gen.coroutine def get(self): fetch_future = self.fetch_next_chunk() while True: chunk = yield fetch_future if chunk is None: break self.write(chunk) fetch_future = self.fetch_next_chunk() yield self.flush()
7.4 循环
因为在Python中无法使用 for
或者 while
循环 yield
迭代器, 并且捕获yield的返回结果. 相反, 你需要将循环和访问结果区分开来, 这是一个 Motor 的例子:
import motor db = motor.MotorClient().test @gen.coroutine def loop_example(collection): cursor = db.collection.find() while (yield cursor.fetch_next): doc = cursor.next_object()
7.5 在后台运行
PeriodicCallback
和通常的协程不同. 相反, 协程中 通过使用 tornado.gen.sleep
可以包含 while True:
循环:
@gen.coroutine def minute_loop(): while True: yield do_something() yield gen.sleep(60) # Coroutines that loop forever are generally started with # spawn_callback(). IOLoop.current().spawn_callback(minute_loop)
有时可能会遇到一些复杂的循环. 例如, 上一个循环每 60+N
秒运行一次, 其中 N
时 do_something()
的耗时.为了精确运行 60 秒,使用上面的交叉模式:
@gen.coroutine def minute_loop2(): while True: nxt = gen.sleep(60) # Start the clock. yield do_something() # Run while the clock is ticking. yield nxt # Wait for the timer to run out.
8.Tornado数据操作介绍
有两种方式可以操作数据库。
第一种:第一种其实感觉用起来并没有那么舒服,是一个模块,对数据库支持没有非常完善。
第二种:数据库模块,我们自己写的数据库模块来代替Tornado提供的原生方式。
#!/usr/bin/env python # -*- coding: utf-8 -*- # @Time : 2023/5/29 19:19 # @Author : 李泽雄 # @BoKeYuan : 小家电维修 # @File : tornado_sql.py # @Version : Python 3.10.10 # @Project : python3 # @Software : PyCharm """ Mysql数据库操作 """ #import pymysql as MySQLdb import logging import pymysql pymysql.install_as_MySQLdb() logger = logging.getLogger(__name__) class MysqlServer(object): """连接Mysql数据服务器 """ def __init__(self, db_config): try: self._db_config = db_config self._conn = self.__get_conn() self._cursor = self._conn.cursor() logger.info(u"connected the db") except Exception: self.close() logger.exception(u"connect db failed!") def __get_conn(self): db_config = self._db_config connection = pymysql.connect(host=db_config['HOST'], port=db_config['PORT'], user=db_config['USERNAME'], passwd=db_config['PASSWORD'], db=db_config['DB'], charset="utf8") connection.ping(True) return connection def ensure_cursor(self): if not self._cursor: if not self._conn: self._conn = self.__get_conn() self._cursor = self._conn.cursor() def run_sql(self, sql): self.ensure_cursor() self._cursor.execute(sql) #commit只对innodb生效,不加commit的话,修改数据库记录的操作不会生效。而如果是myisam引擎的话,不需要commit即可生效 self._conn.commit() return self._cursor.fetchall() def execute_sql(self, sql): self.ensure_cursor() self._cursor.execute(sql) self._conn.commit() def run_sql_fetchone(self, sql): self.ensure_cursor() self._cursor.execute(sql) return self._cursor.fetchone() def close(self): if self._cursor: self._cursor.close() if self._conn: self._conn.close() logger.info(u"closed the db connection")
参考转载: https://blog.csdn.net/HNUPCJ/article/details/115643693