0%

Tornado 快速入门

logo

基础接口使用示例

GET 方法执行顺序:

  1. set_default_headers()
  2. initialize()
  3. prepare()
  4. get()
  5. 客户端收到响应了
  6. on_finish():一些清理工作

POST 方法执行顺序:

  1. set_default_headers()
  2. initialize()
  3. prepare()
  4. post(): 这里人为的让它抛出一个错误 self.send_error(400)
  5. set_default_headers(): 这里为什么又调了一次?因为在上一步 post 时有些数据可能改变了,而且最后抛出错误了,所以恢复成默认的 header。
  6. write_error()
  7. 客户端收到响应了
  8. on_finish():一些清理工作

常用方法扎堆演示:

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
import tornado.web  # 基础 web 框架模块
import tornado.ioloop # 核心 IOLoop 模块


class IndexHandler(tornado.web.RequestHandler):

def get(self):
print(self.request)
# 可以执行多次 write,内容会保存在缓冲区内。当 get 函数执行完会执行一个 self.finish() 才会发给客户端
self.write('1')
self.write('2')
self.write('3')

# 自定义头部
self.set_header("content-Type", "application/json; charset=UTF-8")

# 标准状态码,不用设置 reason
self.set_status(44)

# 非标准状态码,设置了 reason
self.set_status(210, "itcast error")

# 抛出一个错误,后面的都不执行了,类似 raise。使用 write_error 可以定义返回的错误页面。
self.send_error(404, content="出现 404 错误")

# 告知浏览器跳转到 url
self.redirect("/")

# 可以使用这个函数,为这个 handler 的所有方法添加默认的头部信息
def set_default_headers(self):
self.set_header("Content-type","application/json")

# 用来处理 send_error 抛出的错误信息并返回给浏览器错误信息页面。可以重写此方法来定制自己的错误显示页面。
def write_error(self, status_code, **kwargs):
self.write(f'<h1> 出错了,程序员 GG 正在赶过来!{kwargs}</h1>')


class PyHandler(tornado.web.RequestHandler):
# 对应每个请求的处理类 Handler 在构造一个实例后首先执行 initialize 方法。路由映射中的第三个字典型参数会作为该方法的命名参数传递。
def initialize(self, sub):
self.sub = sub

# 预处理,即在执行对应请求方式的 HTTP 方法 (如 get、post 等) 前先执行,注意:不论以何种 HTTP 方式请求,都会执行 prepare 方法。
# 例如做 json 判断
def prepare(self):
if self.request.headers.get("Content-Type", "").startswith("application/json"):
self.json_dict = json.loads(self.request.body)
else:
self.json_dict = None

def get(self):
self.write(self.sub)

if __name__ == '__main__':
'''
Application: 是 tornado web 框架的核心应用类,是与服务器对应的接口
'''

# 注册路由
app = tornado.web.Application(
[
(r'/', IndexHandler),
(r'/py', PyHandler, {'sub': 'pythonHandler'}) # 可以指定一些初始化参数
]
)

'''仅仅绑定端口,并未开始监听'''
app.listen(8000) # 注意:这样开始的是单进程

'''
IOLoop.current: 返回当前线程的 IOLoop 实例
IOLoop.start(): 启动 IOLoop 实例的循环,同时开启了监听

epoll 可以想象成一个容器
IOLoop 是对系统级 epoll 的封装,负责往 epoll 中添加 socket 并不断循环监视 epoll

流程:
IOLoop 启动后,epoll 内会有一个 Socket 监听在 8080 端口
当客户端请求到来时,会产生一个与客户端连接的 Socket,也放在 epoll 中
在 tornado.web.Application 中一个列表保存了路由信息,当客户端发来数据时,IOloop 会把 socket 拿来,去做路由匹配,找到对应的 handler 来处理
'''
tornado.ioloop.IOLoop.current().start()

Settings

我们在创建 tornado.web.Application 对象时,可以传入一些配置参数,比如:

  • debug: 设置 tornado 是否工作在调试模式,默认为 False 即工作在生产模式。

当设置 debug=rue 后,tornado 会工作在调试开发模式,在此种模式下,tornado 为方便我们开发而提供了几种特性:

  • 自动重启:tornado 应用会监控我们的源代码文件,当有改动保存后便会重后程序,这可以减少我们手动重启程序的次数。需要注意的是,一旦我们保存的更改有错误,自动重会导致程序报错而退出,从而需要我们保存修正错误后手动重启程序。这一特性也可单独通过 autoreload=true 开启;
  • 取消缓存编译的模板:可以单独通过 compiled_template_cache=False 来开启;
  • 取消缓存静态文件 hash 值:单独通过 static_hash_cache=False 来开启;
  • 提供追踪信息:当 RequestHandler 或者其子类抛出一个异常而未被捕获后,会生成一个包含追踪信息的页面,可以单独通过 serve_traceback=true 来设置。

使用 debug 参数的方法:

1
2
import tornado.web
app = tornado.web.Application([], debug=True)

Http 服务器

Tornado 是可以单独创建 http 服务器的

以下做的事,等同于上面的 app.listen(8000)

1
2
3
4
5
6
7
# 引入 httpserver 模块
import tornado.httpserver

# 实例化一个 http 服务器对象,需要指定 app,因为路由规则保存在 app 中。
httpserver = tornado.httpserver.HTTPServer(app)
# 绑定端口
httpserver.listen(8000) # 绑定并监听

多进程

Tornado 默认是单进程的,现在介绍多进程的开启方法

注意:虽然 tornado 给我们提供了一次性启动多个进程的方式,但是由于一些问题,不建议使用下面的方式启动多进程,而是手动启动多个进程,并且还能绑定不同的端口。

不建议使用的原因是:

  1. 每个子进程都会从父进程中复制一份 IOLoop 的实例,如果在创建子进程前修改了 IOLoop,会影响所有的子进程;
  2. 由于所有的进程都是由同一个命令启动的,无法做到在不停止服务的情况下修改某个进程的代码;
  3. 所有进程共享一个端口,想要分别监控很困难。

如果要使用,建议使用默认的单进程模式,然后手动启动多进程,并绑定在不同的监听端口上:

1
2
3
4
5
~]> python3 tornado.py
~]> python3 tornado.py
~]> python3 tornado.py
...
...

多进程的启动方法:

1
2
3
4
5
6
7
8
9
10
11
12
# app.listen(8000)  # 之前这样的启动是单进程

httpserver = tornado.httpserver.HTTPServer(app)
httpserver.bind(8000) # 只绑定端口
httpserver.start(Num)
'''
默认开启 1 个进程
当 Num 大于 0,创建对应个数个子进程
当 Num == None 或 Num <=0,开启对应硬件机器的 cup 核心数个子进程
'''

tornado.ioloop.IOLoop.current().start()

安全

tornado中的安全cookies、csrf校验与用户认证

Options 全局参数

  • 作用:全局参数的定义、存储、转换;
  • 功能:用来定义 options 选项变量的方法。

先定义,再引用。

定义

1
2
3
import tornado.options

tornado.options.define(name, default, type, help, metavar, multiple, group, callback)

参数解析:

  • name: 选项变量名,必须保证其唯一性,否则会报 Option 'xxx' already define in...
  • default: 设置选项变量的默认值,默认为 None
  • type: 设置选项变量的类型,从命令行或配置文件导入参数时 tornado 会根据类型转换输入的值,转换不成会报错;
    • 如果没有设置 type,会根据 default 的值进行转换;
    • 如果 default 没有设置,那么不进行转换。
  • multiple: 设置选项变量是否可以为多个值, 默认为 False

示例:

1
2
tornado.options.define("port", default=8000, type=int)
tornado.options.define("list", default=[], type=str, multiple=True)

传参和引用

tornado.options.options: 全局的 options 对象,所有定义的选项变量都会作为该对象的属性。

传参,有两种方式:

  • 从命令行;
  • 从配置文件。

从配置文件传参数,新建一个配置文件 config

1
2
3
~]> vim config.txt
port = 7000
list ["good", "nice", "handsome"]

从命令行传参数:

1
2
~]> python server.py --port=9000 --list=good,nice,handsome,cool
list = ['good', nice','handsome','cool]

引用,上边定义的 portlist 参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 从命令行引用,这句会从命令行转换参数,并保存到 tornado.options.optrons
tornado.options.parse_command_line()

# 从配置文件引用,建议使用配置文件
tornado.options.parse_config_file('config')


print(tornade.options.options.list)

# 引用
httpServer = tornado.httpserver.HTTPServer(app)
httpServer.bind(tornado.options.options.port) # 如未指定 port 参数,则使用 default 中定义的 8080
httpServer.start(1)
tornado.ioloop.IOLoop.current().start()

以上两种方法有 2 个问题:

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

所以,也可以用直接把配置写在一个 python 文件里,用时候导入即可:

1
2
3
4
options = {
"port": 8080,
"list": ["pice", "handsome", "list", "good"]
}

不打印访问日志:

1
2
# 注意,需要放在第一行
tornado.options.options.logging = None

从客户端获取参数

客户端可以定义参数的地方有:

  • header 中;
  • Restful 风格的 url 中;
  • url 的查询字符串中;
  • body 中。

从 url 和 body 中获取参数的方法

注意:对于请求体中的数据要求为字符串才能提取到,且格式为表单编码格式(与 url 中的请求字符串格式相同),即 key1=value1&key2=value2,HTTP 报文头 Header 中的 Content-Typeapplication/x-www-form-urlencodedmultipart/form-data,对于请求体数据为 json 或 xml 的, 无法通过这两个方法获取。

获取请求的其他信息

RequestHandler.request: 对象存储了关于请求的相关信息,具体属性有:

  • method: HTTP 的请求方式,如 GET 或 POST;
  • host: 被请求的主机名;
  • uri: 请求的完整资源标示,包括路径和查询字符串;
  • path: 请求的路径部分;
  • query: 请求的查询字符串部分;
  • version: 使用的 HTTP 版本;
  • headers: 请求的协议头,是类字典型的对象,支持关键字素引的方式获取特定协议头信息,例如:request.headers["Content-Type"]
  • body: 请求体数据;
  • remote_ip: 客户端的 IP 地址;
  • files: 用户上传的文件,为字典类型,如:
1
2
3
4
{
"form filename1": [<tornado.httputil.HTTPFile>, <tornado.httputil.HTTPFile>],
"form_filename2": [<tornado.httputil.HTTPFile>, ]
}

tornado.httputil.HTTPFile 是接收到的文件对象,它有三个属性,这三个对象属性可以像字典一样支持关键字素引,如 request.files["form_filename"][0]["body"]

  • filename: 文件的实际名字,与 form_filename 不同,字典中的键名代表的是客户端提交表单时的 key;
  • body: 文件的数据实体;
  • content_type: 文件的类型。

Tornado 异步

Tornado 的 gen 异步实现原理(伪代码)

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
import time
import threading


def genCoroutine(func):
def wrapper(*args, **kwargs):
gen1 = func() # reqA 的生成器
gen2 = next(gen1) # long_io 的生成器

def run(g2):
res = next(g2)
try:
gen1.send(res) # 返回给 reqA 数据
except StopIteration as e:
pass

threading.Thread(target=run, args=(gen2,)).start()

return wrapper


# handler 获取数据 (数据库、其他服务器、循耗时)
def longIo():
print("开始耗时操作")
time.sleep(5)
print("结束耗时操作")
# 返回数据
yield "sunck is a good man"


# 一个客户端的请求
@genCoroutine
def reqA():
print("开始处理 reqA")
res = yield longIo()
print("接收到 longIo 的响应数据:", res)
print("结束处理 reqA")


# 另一个客户端的请求
def reqB():
print("开始处理 reqB")
time.sleep(2)
print("结束处理 reqB")


# tornado 服务
def main():
reqA()
reqB()
while 1:
time.sleep(0.1)
pass


if __name__ == '__main__':
main()

同步写法

  1. 先访问 /long,同时访问 /home
  2. /long 需要执行 10 秒钟;
  3. 访问 /home 的话需要等 /long 执行完,也就是 10 秒钟后才能执行 /home
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import tornado.web  # 基础 web 框架模块
import tornado.ioloop # 核心 IOLoop 模块
import time


class IndexHandler(tornado.web.RequestHandler):
def get(self):
self.write('home')


class LongIO(tornado.web.RequestHandler):
def get(self):
time.sleep(10)
self.write('LongIO')


if __name__ == '__main__':
app = tornado.web.Application([(r'/', IndexHandler), (r'/long', LongIO)], debug=True)
app.listen(8000)
tornado.ioloop.IOLoop.current().start()

异步写法

  • 对于网络 IO 的异步方法:
    • tornado.httpclient.AsyncHTTPClient: Tornado 提供的 异步 Web 请求客户端,用来专门处理异步 Web 请求;
      • fetch(request): 用于执行一个 Web 请求,并异步响应返回一个 Future 实例;
        • request 可以是一个 url
    • @tornado.gen.coroutine :协程模式装饰器,跟 python3 中 async、await 功能一致
  • 非网络 IO 的异步方法:
    • @run_on_executor: 使用这个装饰器,将耗时操作放入线程中,防止阻塞,原理和 concurrent 包基本一样。执行完会返回 Future 实例,值为线程中 return 的执行结果。

异步示例:

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
import tornado.web  # 基础 web 框架模块
import tornado.ioloop # 核心 IOLoop 模块
from tornado.httpclient import AsyncHTTPClient
import time


class IndexHandler(tornado.web.RequestHandler):
def get(self):
self.write('home')


# 装饰器写法(旧方法,兼容时候用)
class LongIO(tornado.web.RequestHandler):
@tornado.gen.coroutine
def get(self):
client = AsyncHTTPClient()
response = yield client.fetch('http://www.baidu.com')
self.write(response.body)


# 原生协程写法(建议)
class LongIO(tornado.web.RequestHandler):
async def get(self):
client = AsyncHTTPClient()
response = await client.fetch('http://www.baidu.com')
self.write(response.body)


# 对于不支持异步的操作,使用 executor 实现异步
# 和 concurrent 包使用方法基本一样,开单独线程
class LongIO(tornado.web.RequestHandler):
from tornado.concurrent import run_on_executor
from concurrent.futures import ThreadPoolExecutor
executor = ThreadPoolExecutor(5) # 启动线程池

# 使用这个装饰器,将下面函运行在线程中,来防止阻塞
@run_on_executor # 返回 futures 实例
def get(self):
time.sleep(1) # 这个方法不支持异步,不能使用 async/await
self.write('longIO')

# 封装,演化 ↓ ↓ ↓ ↓ ↓ ↓

@run_on_executor
def long(self): # 耗时操作提出来
time.sleep(1)
return 123

async def get(self):
Future = await self.long() # 封装后可以使用 async/await 了
self.write(str(Future))


if __name__ == '__main__':
app = tornado.web.Application([(r'/', IndexHandler), (r'/long', LongIO)], debug=True)
app.listen(8000)
tornado.ioloop.IOLoop.current().start()

关于异步,推荐阅读:Tornado 原理浅析及应用场景探讨

Tornado 的 WebSocket 模块

WebSocket 是 html5 规范中新提出的,客户端与服务器通讯的协议,协议本身使用新的 ws://URL 格式。

WebSocket 是独立的、创建在 TCP 上的协议,和 HTTP 的唯一关联是使用 HTTP 协议的 101 状态码进行协议切换,使用的 TCP 端口是 80,可以用于绕过大多数防火墙的限制。

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

目前常见的浏览器如 Chrome、IE、Firefox、Safari、Opera 等都支持 WebSocket,同时需要服务端程序支持 WebSocket

WebSocket简介与最佳实践

  • 被动:
    • WebSocketHandle: 处理通信;
    • open(): 当一个 WebSocket 链接建立后被调用;
    • on_message(message): 当客户端发送消息过来时调用;
    • on_close(): 当 WebSocket 连接被客户端关闭后调用;
  • 主动:
    • write_message(message, binary=False): 主动向客户端发送 message 消息,message 可以是字符串或者字典(自动转为 Json 字符串)
      • 如果 binary 为 False,则 message 会以 UTF-8 编码发送;
      • 如果 binary 为 True,可以发送二进制模式,就是字节码。
    • close(): 服务端主动关闭 WebSocket 链接;
  • check_origin(origin): 判断源 origin,对于符合条件的请求源允许连接;

WebSocket 聊天室应用示例:

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
import json
import tornado.web
import tornado.ioloop
from tornado import websocket


class IndexHandler(tornado.web.RequestHandler):
def get(self):
self.write("websocket.html")


class SendHandler(websocket.WebSocketHandler):
clients = set()

def open(self):
SendHandler.clients.add(self)
self.write_message('当客户端第一次建立连接')

def on_message(self, message):
message = json.loads(message)
self.write_message(f'这是客户端发来的消息{message}')

# 服务器主动关闭
self.close()

def on_close(self):
# 客户端主动关闭
SendHandler.clients.remove(self)


if __name__ == '__main__':
app = tornado.web.Application(handlers=[(r"/", IndexHandler), (r"/send", SendHandler)], debug=False, )
app.listen(8000)
tornado.ioloop.IOLoop.instance().start()