Tornado

异步协程编程。(其实是异步IO而非真正的异步,从内核拷贝到用户空间的过程还是同步的)

适合用户量大、高并发,如抢票、网页游戏、在线聊天等场景;或大量HTTP持久连接,通过单TCP持久连接,HTTP1.1默认持久连接。

用于解决高性能需求,C10K问题,注重性能,走少而精的方向。

特点是:HTTP服务器、异步编程、WebSockets。

安装测试

import tornado.web # web模块
import tornado.ioloop # io循环模块

class MainHandler(tornado.web.RequestHandler):    # 路由类
    def get(self, *args, **kwargs):    # 处理get请求
        self.write('Hello, world!')

def main():
    app = tornado.web.Application([    # web框架的核心应用类,与服务器对应的接口,是服务器的实例
        ('/', MainHandler),
    ])
    app.listen(8000)    # 监听端口
    tornado.ioloop.IOLoop.instance().start()    # 启动服务器,instance()获取IOLoop实例

if __name__ == '__main__':
    main()

高性能原理

IOLoop与epoll交互,epoll管理socket请求,当有socket建立时,epoll开始监听这个socket,当有数据发送过来时,epoll通知IOLoop将数据发送到Application的路由表,路由转发到指定Handler处理并返回到对应socket。

HTTP服务器

import tornado.httpserver

httpServer = tornado.httpserver.HTTPServer(app) # 创建服务器对象实例

httpServer.listen(8000)

多进程

tornado默认是单进程。

httpServer.bind(8000) # 将服务器绑定到指定端口而不是监听

httpServer.start([unit]) # 多进程启动,默认是1,小于等于0时开启CPU核心数个进程

区别

app.listen()只能在单进程中使用,虽然提供了多进程,仍存在问题不推荐使用上述方法,应该手动启动多进程。

问题1:子进程会复制IOLoop实例,修改IOLoop会影响所有子进程

问题2:所有进程都是由一个命令启动,无法在不停止服务的情况下修改代码

问题3:所有进程共享一个端口,无法单独监控不同进程

开发过程还是用单线程。

OPTIONS

把端口当参数传入,使用tornado.options模块,全局参数的定义、存储、转换。

  • 方法、属性:

    • define(name, default=None, type=None, help=None, metavar=None, multiple=False, group=None, callback=None)

      • name:变量名,必须唯一
      • default:默认值
      • type:变量类型,从命令行或文件导入参数时tornado自动转换,没有type会根据default类型转换
      • help:用作提示信息
      • multiple:接收多个值,默认False
    • options
      • 全局options对象,所有定义的变量都会变成其属性
from tornado.options import define, options

define('port', default=8000, type=int)

app.listen(options.port)
  • 获取参数:

    • tornado.options.parse_command_line()

      • 接收命令行参数,保存到options对象中
    • tornado.options.parse_config_file(path)
      • 从文件导入参数
      • 任意文件,用python语法配置值即可
from tornado.options import parse_command_line()
python3 test.py --port=8000 # 多个值用逗号分隔
from tornado.options import parse_config_file(path)

用python模块文件,可以直接导入对应参数,免去define和parse。
使用parse方法时,默认打开logging功能,向屏幕输出信息,第一行options.logging=None可以关闭日志或命令行--logging=none

配置参数

对创建app的路由参数进行解耦,移植到application.py文件中,通过继承Application类来重写handlers并配置参数。

from views.index import IndexHandler, HomeHandler
import tornado.web
import config

class Application(tornado.web.Application):
    def __init__(self):
        handlers =[
            ('/', IndexHandler),
            ('/home', HomeHandler),
        ]
        super(Application, self).__init__(handlers, **config.settings)

settings可设置的参数

常见的settings解释:

  1. debug,设置为True,调试模式,自动重启服务器/取消缓存模板/静态文件hash/提供追踪信息,但会因代码错误而停止,一个debug=True相当于autoreload=True, compiled_template_cache=False, static_hash_cache=False, serve_traceback=True;
  2. static_path,设置静态文件目录;
  3. template_path,设置模板文件目录;
  4. autoescape=None,关闭项目文档自动转义;
  5. cookie_secret,cookie安全密钥;

为Handler传参用字典,调用使用initialize(),并且该方法总在HTTP方法前执行

('/home', HomeHandler, {'args1':'hello', 'args2':'world')

class HomeHandler(tornado.web.RequestHandler):
    def initialize(self, **kwargs): # 接收路由的参数,在执行请求方法之前调用
        self.word1 = kwargs.get('args1')
        self.word2 = kwargs.get('args2')

数据

RequestHandler的write方法会自动将字典类型数据转为json字串,并设置Content-type为Application/json,如果手动转换,还需要通过self.set_header(name, value)设置Content-type头,否则为text/html。

对响应头的修改可以通过重载set_default_headers()方法来设置,使其在HTTP方法之前执行。

对响应设置状态码通过self.set_status(code, reason=None)设置。

重定向self.redirect(url)。

抛出错误self.send_error(code, **kwargs)

处理错误write_error(self, code, **kwargs)并返回对应界面,在HTTP方法抛出错误之后执行。

反向解析self.reverse_url(name),name由tornado.web.url(path, handler, **kwargs, name)中提供。

tornado.web.Application([
    ('/', MainHandler),
    tornado.web.url('/test', TestHandler, name='testing'),
])

class IndexHandler(RequestHandler):
    def get(self):
        url = self.reverse_url('testing')
        return self.write('<a href='{}'>test</a>'.format(url))

class TestHandler(RequestHandler):
    def get(self):
        return self.write('test')

RequestHandler

提取url特定部分,GET,POST,在头部增加自定义字段

提取特定部分,在路由路径中/test/(?P<h1>\w+)/(?P<h2>\w+)/(?P<h3>\w+)

  • 获取get/post参数self.get_argument(name, default=ARG_DEFAULT, strip=True)/self.get_arguments(name, strip=True)

    • name是字段名,default设置默认值,strip去空格,前者返回单字段,后者返回列表,同名获取最后一个值

渲染页面self.render(template_name)

增加自定义字段,self.add_header(name, value)

self.request对象

  • 属性

    • method:HTTP请求方法
    • host:被请求的主机名
    • uri:请求的完整资源地址,包括路径和参数
    • version:使用的HTTP版本
    • headers:请求头部分,字典
    • body:请求体数据
    • remote_ip:客户端IP
    • files:用户上传的文件,字典

tornado.httputil.HTTPFile对象

上传文件的时候使用,是接收到的文件对象,就是self.request.files内的列表。

  • 属性

    • filename,文件名称
    • body,文件的数据实体
    • content_type,文件类型

响应输出

self.write(body),数据写到缓冲区

self.finish(),刷新缓冲区,关闭当次请求通道,之后的write无效

RequestHandler可重载方法

initialize(),参数初始化,一般用于接收路由参数

prepare(),请求处理前调用

http,请求

set_default_headers(),配置默认headers

write_error(),配合self.send_error()方法自定义错误页面

on_finish() ,请求处理结束后调用,常用于内存资源释放或日志记录

执行顺序,无错误:headers/initialize/prepare/http/on_finish,错误:headers/initialize/prepare/http/headers/errors/on_finish。

模板

  1. 配置模板路径,config.py中配置settings的template_path,在Application方法中调用

  2. 渲染并返回给客户端,render(template_name, args=args)

  3. 模板语法,字典不支持点语法,基本语法

  4. 函数,{{ static_url('css/style.css') }},静态文件目录拼接,利用static_path与提供的地址进行拼接,并创建基于文件内容的hash值,保证每次加载的都是最新文件

  5. 转义,关闭自动转义{% raw string %},{% autoescape None %},配置中"autoescape":None,escape()函数开启转义

  6. 继承,{% block main %}{% end %},{% enxtends "base.html" %}{% block main %}{% end %}

  7. 静态文件,StaticFileHandler映射静态文件,该类由tornado.web提供,在路由中直接使用,传入path参数和default_filename参数,(r'/', StaticFileHandler,

数据库

Tornado没有自带的ORM,数据库需要自行选择框架和驱动去适配。

常见的peewee和sqlalchemy都可以,但sqla的学习曲线比较长。

安全

  • set_cookie(name, value, domain=None, expires=None, path='/', expires_days=None, **kwargs)

    • name:cookie名
    • value:值
    • domain:提交cookie的匹配的域名
    • path:提交匹配的路径
    • expires:cookie有效期,可以是时间戳证书、时间元组、datetime类型,是UTC时间
    • expires_days:同样有效期,天数,优先级低于expires
  • get_cookie(name, default=None)
    • name:获取的cookie名
    • default:如果指定name的cookie不存在,返回默认值
  • clear_cookie(name, path='/', domain=None)
    • name:指定要删除的cookie名
    • path:指定匹配的路径
    • domain:指定匹配的域名
  • clear_all_cookies(path=‘/’, domain=None)

  • set_secure_cookie(name, value, expires_days=30, version=None, **kwargs)
    • Application配置密钥cookie_secret=‘xxxxx'
  • get_secure_cookie(name, value=None, max_age_days=31, min_version=None)
    • value:验证不通过的返回值
    • max_age_days:过滤安全cookie的时间戳
  • xsrf保护,同源策略,针对POST请求,也不是绝对安全的
    • xsrf_cookies=True
    • 模板中应用
      • {% module xsrf_form_html() %}
      • 为浏览器设置_xsrf的安全cookie,浏览器关闭后失效
      • 为模板表单添加了隐藏域
    • 非模板应用
      • self.xsrf_token:手动设置_xsrf的cookie,浏览器可以获取对应token;手动添加隐藏域:为表单生成隐藏域,值通过js脚本从cookie中获取
      • Ajax请求,$.ajax({url:xxx, method:xxx, data:xxx, callback:function(data) {}, headers:{'X-XSRFToken':getCookie('_xsrf')}})
      • 一般在进入主页时提供xsrf的cookie即可,后续只需验证

用户验证

  • tornado.web.authenticated装饰器

    • 用法,包装http方法
    • 验证,RequestHandler重载get_current_user(self)方法,验证逻辑写在该方法内

同步与异步

同步就是按顺序执行程序,前一个阻塞了,就不会执行到后一个;异步则是遇到阻塞将程序交给内核处理,直接执行下去,如果阻塞操作结束有返回结果,再获取其结果

回调函数实现异步

回调函数其实就是在任务最后执行的函数,因为函数可以作为参数传入,因此可以将阻塞操作封装到函数里作为回调函数传给任务,而这个任务可以调用子线程执行,这样就不会造成同步阻塞。

协程实现异步

  1. 使用yield生成器,yield阻塞操作函数,阻塞函数中向生成器send数据,主函数中调用next()运行生成器即可,注意捕捉StopIteration错误,会用到全局变量用于在阻塞操作中调用

  2. 1中要当作生成器处理,所以不可避免代码较多不能像普通函数调用,因此改进版用装饰器封装

  3. 2中依然存在全局变量,对任务函数和阻塞操作函数都作为生成器处理,多线程参数为阻塞操作函数生成器,next()该生成器,并将结果send到任务函数生成器中,注意捕捉错误

# 例1
gen = None

def blocking_task(time):
    def run():
        time.sleep(time)
        try:
            global gen
            gen.send('Worked down')
        except StopIteration as e:
            pass
        threading.Thread(target=run).start()

def worker1(time):
    print('Start work1')
    res = yield blocking_task(time)
    print(res)
    print('End work1')

def worker2(time):
    print('Start work2')
    time.sleep(time)
    print('End work2')

def main():
    global gen
    gen = worker1(5)
    next(gen)

    worker2(2)

# 例2
def blocking_task(time):
    def run():
        time.sleep(time)
        try:
            global gen
            gen.send('Worked down')
        except StopIteration as e:
            pass
        threading.Thread(target=run).start()

def gen_decorator(f):
    def wrap(*args, **kwargs):
        global gen
        f(*args, **kwargs)
        next(f)
    return wrap

@gen_decorator
def worker1(time):
    print('Start work1')
    res = yield blocking_task(time)
    print(res)
    print('End work1')

def worker2(time):
    print('Start work2')
    time.sleep(time)
    print('End work2')

def main():
    worker1(5)
    worker2(2)

# 例3
def blocking_task(time):
    time.sleep(time)
    yield 'Worked Down'

def gen_decorator(f):
    def wrap(*args, **kwargs):
        gen1 = worker1(*args, **kwargs)
        gen2 = next(gen1)
        def run(g):
            res = next(g)
            try:
                gen1.send(res)
            except StopIteration as e:
                pass
            threading.Thread(target=run, args=(gen2,)).start()
    return wrap

@gen_decorator
def worker1(time):
    print('Start work1')
    res = yield blocking_task(time)
    print(res)
    print('End work1')

def worker2(time):
    print('Start work2')
    time.sleep(time)
    print('End work2')

def main():
    worker1(5)
    worker2(2)

Tornado的异步

epoll用于解决网络IO并发问题,Tornado的异步基于epoll。

  • tornado.httpclient.AsyncHTTPClient,这是Tornado提供的异步Web请求客户端,用来运行异步Web请求

    • fetch(request, callback=None),用于执行一个Web请求,并异步响应返回一个tornado.httpclient.HTTPResponse,request可以是一个url,或一个tornado.httpclient.HTTPRequest对象,url自动转换为对象
  • HTTPResponse,响应类
    • code,状态码
    • reason,状态描述
    • body,响应数据
    • error,异常
  • HTTPRequest,请求类,构造函数可以接收参数
    • url,字串类型,目的网址
    • method,字串类型,请求方法
    • headers,字典类型或HTTPHeaders类型,请求头
    • body,HTTP请求体
  • tornado.web.asynchronous,异步回调装饰器,不关闭通信通道

  • tornado.gen.coroutine,异步协程装饰器

# 回调异步
import json

class TestHandler(RequestHandler):
    def on_response(self, response):
        if response.error:
            self.write_error(500)
        else:
            self.finish(json.loads(response.body))

    @tornado.web.asynchronous
    def get(self, *args, **kwargs):
        url = 'xxxx'
        client = AsyncHTTPClient()
        client.fetch(url, self.on_response)

# 协程异步
class TestHandler(RequestHandler):
    @tornado.gen.coroutine
    def get(self, *args, **kwargs):
        url = 'xxxx'
        client = AsyncHTTPClient()
        res = client.fetch(url)
        if res.error:
            self.write_error(500)
        else:
            self.write(json.loads(res.body))

# 协程异步改进,异步客户端独立出来
class TestHandler(RequestHandler):
    @tornado.gen.coroutine
    def get(self, *args, **kwargs):
        res = yield get_data()
        self.write(res)

    @tornado.gen.coroutine
    def get_data(self):
        url = 'xxx'
        client = AsyncHTTPClient()
        res = yield client.fetch(url)
        if res.error:
            data = {'res':0}
        else:
            data = json.loads(res.body)
        raise tornado.gen.Return(data) # 相当于yield中的send方法

Websockets

WebSocket是HTML5规范中提出的新的B/S通信协议,该协议使用新的协议头ws://url

WebSocket是独立的创建在TCP协议之上的协议,和HTTP唯一的关系是使用了HTTP协议的101状态码。

WebSocket使客户端与服务端之间的数据交互变得更加简单,允许服务器直接向客户端推送数据。

大多数浏览器都已经支持WebSocket。

Tornado的WebSocket模块

  • tornado.websocket.WebSocketHandler用于处理通信

    • open() 有新连接时被调用,一般用于记录用户身份并初始化信息
    • on_message(msg) 收到消息时调用
    • on_close() 断开连接时调用,一般用于清理内存
    • write_message(msg, binary=False) 用于主动向客户端发送消息,可以是字串或字典(自动json字串),如果binary为False,msg以utf-8编码,如果为True,发送字节码
    • close() 服务端主动断开连接
    • check_origin(origin) 判断源origin,对于符合条件的请求源允许连接
<div>
    <input type='text' id='message' />
    <button onclick='sendMessage()'>Send</button>
    <button onclick='exsitsPage()'>Exit</button>
</div>
<script>
    // 建立WebSocket连接
    var ws = new WebSocket("ws://ip:port/chat")

    // 接收服务器发来的数据
    ws.onmessage = function(e) {
        var obj = querySelector('div')
        data = "<p> %s </p>" % e.data
        document.insertAdjacentHTML('beforeBegin', data)
    }

    // 向服务器发送数据
    function sendMessage() {
        var message = document.querySelector('#message').data
        ws.send(message)
        document.querySelector('#message').data = ''
    }

    // 主动关闭连接,关闭浏览器会自动断开连接
    function exsitsPage(ws) {
        ws.close()
    }
</script>
import tornado.websocket

class ChatHandler(tornado.websocket.WebSocketHandler):
    users = list() # 用于存储连接的用户信息

    def open(self):
        self.users.append(self)
        for user in self.users:
            user.write_message(u"[{}]进入聊天室".format(self.request.remote_ip))

    def on_message(self, msg):
        for user in self.users:
            user.write_message(u"[{}]说:{}".format(self.request.remote_ip, msg))

    def on_close(self):
        pass

    def check_origin(self, origin):
        return True

最新文章

  1. vim入门过程
  2. swith 好久不用都忘记了
  3. C++实现树的基本操作,界面友好,操作方便,运行流畅,运用模板
  4. Activity的四种启动模式详解
  5. 17.allegro导入导出[原创]
  6. ios 应用剖析
  7. linux系统基础(一)
  8. PAT (Basic Level) 1013. 数素数 (20)
  9. 案例:计算1!+2!+3!+......+n!
  10. Struts2与ajax整合之缺点
  11. JavaScript 面向对象思想 贪吃蛇游戏
  12. Chapter 2 Open Book——7
  13. 《Linux命令行与shell脚本编程大全》第十九章 初识sed和gawk
  14. Mac 配置Charles,抓取移动设备数据
  15. 新概念英语(1-51)A pleasant climate
  16. android checkBox选中与取消
  17. Django中提供的6种缓存方式
  18. VC2012+QT新建一个控制台程序
  19. python [] 数组 list 交集 并集 差集
  20. 有术:DIY代理服务器

热门文章

  1. 浅谈getStackTrace()方法(一)
  2. java面试题之Executor和Executors的区别
  3. Keepalived中Master和Backup角色选举策略
  4. 重置css样式
  5. gulpfile.js备份
  6. Unity 导出的android项目自动生成Private Libraries
  7. (超详细)使用git命令行将本地仓库代码上传到github或gitlab远程仓库
  8. bzoj 2437[Noi2011]兔兔与蛋蛋 黑白染色二分图+博弈+匈牙利新姿势
  9. vue.js源码学习分享(七)
  10. 一、git clone