Tornado - 基本入门

初识 Tornado

什么是 Tornado

全称 Tornado Web Server,是一种 Web 服务器软件的开源版本。

特点

  • 作为 Web 框架,是一个轻量级的 Web 框架,类似于另一个 Python web 框架 Web.py,其拥有异步非阻塞 IO 的处理方式
  • 作为 Web 服务器,Tornado 有较为出色的抗负载能力,官方用 nginx 反向代理的方式部 Tornado 和其它 Python web 应用框架进行对比,结果最大浏览量超过第二名近 40%

使用场景

  1. 用户量大,高并发
  2. 大量大 HTTP 持久连接
    • 使用同一个 TCP 连接来发送和接收多个 HTTP 请求 / 应答,而不是为每一个新的请求 / 应答打开新的连接的方法
    • 对于 HTTP 1.0,可以在请求的包头 (Header)中添加 Connection:Keep-Alive=
    • 对于 HITP 1.1,所有的连接默认都是持久连接

C10K

上面的高并发问题,通常用 C10K 这一概念来描述。C10K——Concurrentlyhandling ten thousand connections,即并发 10000 个连接。对于单台服务器而言,根本无法承担,而采用多台服务器分布式又意味着高昂的成本

性能

Tornado 在设计之初就考虑到了性能因素,旨在解决 C10K 问题,这样的设计使得其成为一个拥有非常高性能的解决方案(服务器与框架的集合体)

Tornado 与 Django 对比

Django

  1. Django 是走大而全的方向,注重的是高效开发,它最出名的是其全自动化的管理后台:只需要使用起 ORM,做简单的对象定义,它就能自动生成数据库结构、以及全功能的管理后台
  2. Django 提供的方便,也意味着 Django 内置的 ORM 跟框架内的其他模块耦合程度高,应用程序必须使用 Django 内置的 ORM,否则就不能享受到框架内提供的种种基于其 ORM 的便利
  3. 特点
    • session 功能
    • 后台管理
    • ORM

Tornado

  1. Tornado 走的是少而精的方向,注重的是性能优越,它最出名的是异步非阻塞的设计方式
  2. 特点
    • HTTP 服务器
    • 异步编程
    • WebSockets

安装 Tornado

  1. 安装

    1
    pip install tornado==5.1.1
  2. 测试

    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
    # tornado的基础web框架
    import tornado.web
    # tornado的核心IO循环模块,封装了Linux的epoll和BSD的kqueue,是tornado高效的基础
    import tornado.ioloop


    # 类比Django中的视图,一个业务处理类
    class IndexHandler(tornado.web.RequestHandler):
    # 处理get请求的,不能处理post请求
    def get(self, *args, **kwargs):
    # 对应http请求的方法,给浏览器响应信息
    self.write('sunck is a good man!')


    if __name__ == '__main__':
    # 实例化一个app对象
    # Application:是tornado web框架的核心应用类,是与服务器的接口
    # 里面保存了路由映射表,有一个listen方法用来创建一个http服务器的实例,并绑定了端口
    app = tornado.web.Application([
    (r'/', IndexHandler)
    ])
    # 绑定监听端口
    # 注意:此时服务器并没有开启监听
    app.listen(8000)
    # IOLoop.current():返回当前线程的IOloop实例
    # IOLoop.start():启动IOLoop实例的I/O循环,同时开启了监听
    tornado.ioloop.IOLoop.current().start()
  3. 说明

    • Tornado 应该运行在类 Unix 平台,在线上部署时为了最佳的性能和扩展性,仅推荐 Linux 和 BSD(因为充分利用 Linux 的 epoll 工具和 BSD 的 kqueue 工具,是 Tornado 不依靠多进程 / 多线程而达到高性能的原因)。
    • 对于 Mac OS x,虽然也是衍生自 BSD 并且支持 kqueue,但是其网络性能通常不太给力,因此仅推荐用于开发
    • 对于 Windows,Tornado 官方没有提供配置支持,但是也可以运行起来,不过仅推荐在开发中使用

Tornado 高效的原理

工作原理

httpserver

httpserver 对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import tornado.web
import tornado.ioloop
# 引入httpserver模块
import tornado.httpserver


class IndexHandler(tornado.web.RequestHandler):
def get(self, *args, **kwargs):
self.write('sunck is a good man!')


if __name__ == '__main__':
app = tornado.web.Application([
(r'/', IndexHandler)
])
# app.listen(8000)
# 实例化一个HTTP服务器对象
httpServer = tornado.httpserver.HTTPServer(app)
# 绑定端口
httpServer.listen(8000)

tornado.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
    import tornado.web
    import tornado.ioloop
    # 引入httpserver模块
    import tornado.httpserver


    class IndexHandler(tornado.web.RequestHandler):
    def get(self, *args, **kwargs):
    self.write('sunck is a good man!')


    if __name__ == '__main__':
    app = tornado.web.Application([
    (r'/', IndexHandler)
    ])
    httpServer = tornado.httpserver.HTTPServer(app)
    # httpServer.listen(8000)
    httpServer.bind(8000)
    httpServer.start(4)

    tornado.ioloop.IOLoop.current().start()
  • 说明

    • httpServer.bind(port):将服务器绑定到指定的的端口
    • httpServer.start(num)
      • 默认开启一个进程;
      • 值大于 0,创建对应个数子进程
      • 值为 None 或者小于等于 0,开启对应机器硬件的 CPU 核心数个子进程

补充说明

  • app.listen(port):只能在单进程模式中使用
  • 多进程
    • 虽然 tornado 给我们提供了一次性启动多个进程的方式,但是由于一些问题,不建议使用上面方式启动多进程,而是手动启动多个进程,并且还能绑定不同的端口
    • 问题
      1. 每个子进程都会从父进程中复制一份 IOLoop 的实例,如果在创建子进程前修改了 IOLoop,会影响所有的子进程
      2. 所有的进程都是由一个命令启动的,无法做到在不停止服务的情况下修改代码
      3. 所有进程共享一个端口,想要分别监控很困难

options

  • tornado 为我们提供了一个 tornado.options 模块

  • 作用:全局参数的定义、存储、转换

  • 基础方法与属性

    • tornado.options.define()

      • 原型

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        tornado.options.define(
        name: str,
        default: Any = None,
        type: type = None,
        help: str = None,
        metavar: str = None,
        multiple: bool = False,
        group: str = None,
        callback: Callable[[Any], None] = None,
        )
      • 功能:用来定义 options 选项变量的方法

      • 参数

        • name:选项变量名,必须保证其唯一性,否则会报 options xxx already define in ...
        • default:设置选项变量的默认值,默认为 None
        • type
          • 设置选项变量的类型,从命令行或配置文件导入参数时 tornado 会根据类型转换输入的值,转换不成会报错,可以是 str、float、int、datetime、timedelta
          • 如果没有设置 type,会根据 default 的值进行转换
          • 如果 default 没有设置,那么不进行转换
        • multiple:设置选项变量是否可以为多个值,默认为 false
        • help:选项变量的帮助提示信息
      • 示例

        1
        2
        tornado.options.define('port', default=8000, type=int)
        tornado.options.define('list', default=[], type=str)
    • tornado.options.options

      • 全局的 options 对象,所有定义的选项都会作为该对象的属性
  • 获取参数的方法(3 种)

    1. tornado.options.parse_command_line()

      • 作用:转换命令行参数,

      • 示例

        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
        import tornado.web
        import tornado.ioloop
        import tornado.httpserver
        import tornado.options

        # 定义两个参数
        tornado.options.define('port', default=8000, type=int)
        tornado.options.define('list', default=[], type=str, multiple=True)


        class IndexHandler(tornado.web.RequestHandler):
        def get(self, *args, **kwargs):
        self.write('sunck is a good man!')


        if __name__ == '__main__':
        # 转换命令行参数,并保存到tornado.options.options
        tornado.options.parse_command_line()
        print('list =', tornado.options.options.list)
        app = tornado.web.Application([
        (r'/', IndexHandler)
        ])
        httpServer = tornado.httpserver.HTTPServer(app)
        # 使用变量的值
        httpServer.bind(tornado.options.options.port)
        httpServer.start(1)

        tornado.ioloop.IOLoop.current().start()
      • 启动

        1
        python server04.py --port=8848 --list=good,nice,handsome,cool
    2. tornado.options.parse_config_file(path)

      • 作用:从配置文件导入参数

      • 示例

        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
        import tornado.web
        import tornado.ioloop
        import tornado.httpserver
        import tornado.options

        # 定义两个参数
        tornado.options.define('port', default=8000, type=int)
        tornado.options.define('list', default=[], type=str, multiple=True)


        class IndexHandler(tornado.web.RequestHandler):
        def get(self, *args, **kwargs):
        self.write('sunck is a good man!')


        if __name__ == '__main__':
        # 转换命令行参数,并保存到tornado.options.options
        tornado.options.parse_config_file('config')
        print('list =', tornado.options.options.list)
        app = tornado.web.Application([
        (r'/', IndexHandler)
        ])
        httpServer = tornado.httpserver.HTTPServer(app)
        # 使用变量的值
        httpServer.bind(tornado.options.options.port)
        httpServer.start(1)

        tornado.ioloop.IOLoop.current().start()
      • 需要创建一个名为 config 的普通文件

        1
        2
        port = 7000
        list = ['good', 'nice', 'handsome']

        说明:

        1. 书写格式仍需要按照 python 的语法要求
        2. 不支持字典类型
      • 启动

        1
        python server05.py
    3. 最终版本

      • 示例

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


        class IndexHandler(tornado.web.RequestHandler):
        def get(self, *args, **kwargs):
        self.write('sunck is a good man!')


        if __name__ == '__main__':
        print('list =', config.options['list'])
        app = tornado.web.Application([
        (r'/', IndexHandler)
        ])
        httpServer = tornado.httpserver.HTTPServer(app)
        # 使用变量的值
        httpServer.bind(config.options['port'])
        httpServer.start(1)

        tornado.ioloop.IOLoop.current().start()
      • 需要创建一个名为 config.py 的普通文件

        1
        2
        3
        4
        5
        # 参数
        options = {
        'port': 8080,
        'list': ['good', 'nice', 'handsome']
        }
  • 日志

    • 当我们在大马中使用 parse_command_line() 或者 parse_config_file(path) 方法时,tornado 会默认开启 logging 模块功能,向屏幕终端输出一些打印信息

    • 关闭日志(2 种方法)

      1. 在第一行加入 tornado.options.options.logging = None

        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
        import tornado.web
        import tornado.ioloop
        import tornado.httpserver
        import tornado.options

        # 定义两个参数
        tornado.options.define('port', default=8000, type=int)
        tornado.options.define('list', default=[], type=str, multiple=True)


        class IndexHandler(tornado.web.RequestHandler):
        def get(self, *args, **kwargs):
        self.write('sunck is a good man!')


        if __name__ == '__main__':
        # 转换命令行参数,并保存到tornado.options.options
        tornado.options.options.logging = None
        tornado.options.parse_config_file('config')
        print('list =', tornado.options.options.list)
        app = tornado.web.Application([
        (r'/', IndexHandler)
        ])
        httpServer = tornado.httpserver.HTTPServer(app)
        # 使用变量的值
        httpServer.bind(tornado.options.options.port)
        httpServer.start(1)

        tornado.ioloop.IOLoop.current().start()
      2. 黑屏终端

        1
        python server04.py --port=8848 --list=good,nice,handsome,cool --logging=none