
基础接口使用示例
GET 方法执行顺序:
set_default_headers()
initialize()
prepare()
get()
- 客户端收到响应了
on_finish():一些清理工作
POST 方法执行顺序:
set_default_headers()
initialize()
prepare()
post(): 这里人为的让它抛出一个错误 self.send_error(400)
set_default_headers(): 这里为什么又调了一次?因为在上一步 post 时有些数据可能改变了,而且最后抛出错误了,所以恢复成默认的 header。
write_error()
- 客户端收到响应了
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 import tornado.ioloop
class IndexHandler(tornado.web.RequestHandler):
def get(self): print(self.request) self.write('1') self.write('2') self.write('3')
self.set_header("content-Type", "application/json; charset=UTF-8")
self.set_status(44)
self.set_status(210, "itcast error")
self.send_error(404, content="出现 404 错误")
self.redirect("/")
def set_default_headers(self): self.set_header("Content-type","application/json")
def write_error(self, status_code, **kwargs): self.write(f'<h1> 出错了,程序员 GG 正在赶过来!{kwargs}</h1>')
class PyHandler(tornado.web.RequestHandler): def initialize(self, sub): self.sub = sub
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
| import tornado.httpserver
httpserver = tornado.httpserver.HTTPServer(app)
httpserver.listen(8000)
|
多进程
Tornado 默认是单进程的,现在介绍多进程的开启方法
注意:虽然 tornado 给我们提供了一次性启动多个进程的方式,但是由于一些问题,不建议使用下面的方式启动多进程,而是手动启动多个进程,并且还能绑定不同的端口。
不建议使用的原因是:
- 每个子进程都会从父进程中复制一份 IOLoop 的实例,如果在创建子进程前修改了 IOLoop,会影响所有的子进程;
- 由于所有的进程都是由同一个命令启动的,无法做到在不停止服务的情况下修改某个进程的代码;
- 所有进程共享一个端口,想要分别监控很困难。
如果要使用,建议使用默认的单进程模式,然后手动启动多进程,并绑定在不同的监听端口上:
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
|
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]
|
引用,上边定义的 port 和 list 参数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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) httpServer.start(1) tornado.ioloop.IOLoop.current().start()
|
以上两种方法有 2 个问题:
- 书写格式仍需要按照 python 的语法要求
- 不支持字典类型
所以,也可以用直接把配置写在一个 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-Type 为 application/x-www-form-urlencoded 或 multipart/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() gen2 = next(gen1)
def run(g2): res = next(g2) try: gen1.send(res) except StopIteration as e: pass
threading.Thread(target=run, args=(gen2,)).start()
return wrapper
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")
def main(): reqA() reqB() while 1: time.sleep(0.1) pass
if __name__ == '__main__': main()
|
同步写法
- 先访问
/long,同时访问 /home;
/long 需要执行 10 秒钟;
- 访问
/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 import tornado.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 实例;
@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 import tornado.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)
class LongIO(tornado.web.RequestHandler): from tornado.concurrent import run_on_executor from concurrent.futures import ThreadPoolExecutor executor = ThreadPoolExecutor(5)
@run_on_executor def get(self): time.sleep(1) self.write('longIO')
@run_on_executor def long(self): time.sleep(1) return 123
async def get(self): Future = await self.long() 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()
|