Python+WEB框架之Tornado

前言

Tornado(龙卷风)和Django同样是Python中比较主流的web框架,Tornado 和如今的主流 Web 服务器框架也有着明显的区别:Tornado自带socket,而且实现了异步非阻塞并对WebSocket协议自然支持;javascript

 

1、Tornado框架的基本组成

Tonado由 路由系统、视图、模板语言4大部分组成,若是习惯了使用Django你会感受它功能单薄,可是只有这样才能足够轻量,若是用到什么功能就本身去GitHub上找现成的插件,或者自实现;如下将对这些基本组件进行逐一介绍。css

Django功能概览:

socket:有 
  中间件:无(使用Python的wsgiref模块)
  路由系统:有
  视图函数:有
  ORM操做:有
  模板语言:有
  simple_tag:有
  cokies:有
  session:有
  csrf:有
  xss:有
  其余:缓存、信号、Form组件、ModelFormm、Admin








tornado功能概览:

  socket:有(异步非阻塞、支持WebScoket)
  路由系统:有
  视图函数:有
  静态文件:有
  ORM操做:无
  模板语言:有
  simple_tag:有,uimethod,uimodule
  cokies:有
  session:无
  csrf:有
  xss:有
  其余:无
Django和Tonado功能对比

 

2、Tornado自带功能

一、Tornado执行流程html

#准备安装Tornado: pip install tornado

import tornado.ioloop
import tornado.web

class MainHandler(tornado.web.RequestHandler): #注意继承RequestHandler 而不是redirectHandler
    def get(self):
        self.write('hellow ,world')


application=tornado.web.Application([
                        (r'/index/',MainHandler) #路由

                                     ])


if __name__ == '__main__':
    application.listen(8888)                  #建立1个socket对象
    tornado.ioloop.IOLoop.instance().start()  #conn,addr=socket.accept()进入监听状态
View Code

第一步:执行脚本,监听 8888 端口前端

第二步:浏览器客户端访问 /index  -->  http://127.0.0.1:8888/index/java

第三步:服务器接受请求,并交由对应的类处理该请求node

第四步:类接受到请求以后,根据请求方式(post / get / delete ...)的不一样调用并执行相应的方法python

第五步:方法返回值的字符串内容发送浏览器mysql

 

配置文件:jquery

setings={
'template_path':'templates',#配置模板路径
'static_path':'static',     #配置静态文件存放的路径
'static_url_prefix':'/zhanggen/', #在模板中引用静态文件路径时使用的别名 注意是模板引用时的别名
"xsrf_cookies": True,               #使用xsrf认证
 'cookie_secret' :'xsseffekrjewkhwy'#cokies加密时使用的盐
}
application=tornado.web.Application([
                        (r'/login/',LoginHandler) ,#参数1 路由系统
                        (r'/index/',IndexHandler) ,#参数1 路由系统

                                     ],
                        **setings                  #参数2 配置文件
                            )
View Code

 

 

二、路由系统git

2.一、动态路由(url传参数)

app=tornado.web.Application(
    [
        (r'^/index/$',MainHandler),
        (r'^/index/(\d+)$',MainHandler), #url传参
    ]
)
View Code

2.二、域名匹配 

#支持域名匹配  www.zhanggen.com:8888/index/333333
app.add_handlers('www.zhanggen.com',[

        (r'^/index/$', MainHandler),
        (r'^/index/(\d+)$', MainHandler),
])
View Code

2.三、反向生成url

app.add_handlers('www.zhanggen.com',[

        (r'^/index/$', MainHandler,{},"name1"), #反向生成url
        (r'^/index/(\d+)$', MainHandler,{},"name2"),
])
路由
class MainHandler(tornado.web.RequestHandler):
    def get(self,*args,**kwargs):
        url1=self.application.reverse_url('name1')
        url2 = self.application.reverse_url('name2', 666)
        print(url1,url2)
        self.write('hello word')
视图

 

 

三、视图

tornado的视图才有CBV模式,url匹配成功以后先  视图执行顺序为 initialize 、prepare、get/post/put/delete、finish;

import tornado.ioloop
import tornado.web
class MainHandler(tornado.web.RequestHandler):
    def initialize(self): #1
        print()

    def prepare(self):
        pass

    def get(self,*args,**kwargs):
        self.write('hello word')

    def post(self, *args, **kwargs):
        pass

    def finish(self, chunk=None):
        pass
        super(self,MainHandler).finish()
View Code

 

3.一、请求相关

self.get_body_argument('user') :获取POST请求携带的参数

self.get_body_arguments('user_list') :获取POST请求参数列表(如chebox标签和select多选)

self.request.body.decode('utf-8'):获取json数据

self.get_query_argument('user') :获取GET请求携带的参数

self.get_query_arguments('user_list') :获取GET请求参数列表(如chebox标签和select多选)

self.get_argument('user') :获取GET和POST请求携带的参数

self.get_arguments('user_list'):获取GET和POST请求参数列表(如chebox标签和select多选)

 

注:以上取值方式若是取不到值就会报错,能够设置取不到值就取None;(例如 self.get_argument('user',None))

 

3.二、响应相关

self.write() :响应字符串

self.render():响应页面

self.redirect():页面跳转

 

四、模板语言

tornado的模板语言和Python语法一致

123
View Code

4.一、登陆页面

#准备安装Tornado: pip install tornado

import tornado.ioloop
import tornado.web

class LoginHandler(tornado.web.RequestHandler): #注意继承RequestHandler 而不是redirectHandler
    def get(self):
        self.render('login.html')

setings={
'template_path':'templates',#配置模板路径
'static_path':'static',     #配置静态文件存放的路径
'static_url_prefix':'/zhanggen/' #在模板中引用静态文件路径时使用的别名 注意是模板引用时的别名
}
application=tornado.web.Application([
                        (r'/login/',LoginHandler) #参数1 路由系统

                                     ],
                        **setings                  #参数2 配置文件
                            )


if __name__ == '__main__':
    application.listen(8888)                  #建立1个socket对象
    tornado.ioloop.IOLoop.instance().start()  #conn,addr=socket.accept()进入监听状态
View Code
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="/zhanggen/dist/css/bootstrap.css">
    <title>Title</title>
</head>
<body>
<div class="container">
    <div class="row">
        <div class="col-md-5 col-md-offset-3">
            <form method="post" >
                  <div class="form-group">
                    <label for="exampleInputEmail1">用户名</label>
                    <input type="email" class="form-control" id="exampleInputEmail1" placeholder="用户名">
                  </div>
                  <div class="form-group">
                    <label for="exampleInputPassword1">密码</label>
                    <input type="password" class="form-control" id="exampleInputPassword1" placeholder="密码">
                  </div>
                  <button type="submit" class="btn btn-default">提交</button>
            </form>
        </div>
    </div>
</div>
</body>
</html>
模板语言

 

4.二、引入静态文件

<link rel="stylesheet" href="/zhanggen/coment.css">
经过别名引入静态文件
<link rel="stylesheet" href='{{static_url("dist/css/bootstrap.css") }}'>
static_url()方式引入静态文件

 

经过static_url()方法引入静态文件的好处: 

一、使用static_url()能够不用考虑静态文件修改以后形成引用失效的状况;

二、还会生成静态文件url会有一个v=...的参数,这是tornado根据静态文件MD5以后的值,若是后台的静态文件修改,这个值就会变化,前端就会从新向后台请求静态文件,保证页面实时更新,不引用浏览器缓存;

 

4.三、上下文对象

若是模板语言中声明了变量,上下文对象必须对应传值,若是没有就设置为空,不然会报错;

self.render('login.html',**{'erro_msg':'' }) #模板中声明了变量,视图必须传值,若是没有就设置为空;
View Code

 

五、xsrf_tocken认证

setings={
'template_path':'templates',#配置模板路径
'static_path':'static',     #配置静态文件存放的路径
'static_url_prefix':'/zhanggen/', #在模板中引用静态文件路径时使用的别名 注意是模板引用时的别名
"xsrf_cookies": True,           #使用xsrf认证
}
配置文件setings={"xsrf_cookies": True, }
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href='{{static_url("dist/css/bootstrap.css") }}'>
    <title>Title</title>
</head>
<body>
<div class="container">
    <div class="row">
        <div class="col-md-5 col-md-offset-3">
            <form method="post" >
                {%raw xsrf_form_html() %}
                  <div class="form-group">
                    <input type="text" class="form-control" placeholder="用户名" name="user">
                  </div>
                  <div class="form-group">
                    <input type="password" class="form-control" placeholder="密码" name="pwd">
                  </div>
                  <button type="submit" class="btn btn-default">提交</button>
            </form>
        </div>
    </div>
</div>
</body>
</html>
模板语言 {%raw xsrf_form_html() %}

 

六、cokies

Tornado不自带session,可是包含cookies;

6.一、cookies

设置cokies

  user=self.get_cookie('username')
        if user:
            v=time.time()+10
            self.set_cookie('username', user, expires=v)
set_cookie('key',value , expires=过时时间)

获取cokies

self.get_cookie('username')
self.get_cookie('username')

 

设置在用户不断刷新页面的状况,cookies不过时;

import tornado.ioloop
import tornado.web
import time
class SeedListHandler(tornado.web.RequestHandler):
    def initialize(self):
        user=self.get_cookie('username')
        if user:
            v=time.time()+10
            self.set_cookie('username', user, expires=v)
构造initialize方法

 

6.二、Tornado加密cokies

配置加密规则使用的字符串

setings={
        'template_path':'templates',
        'static_path': 'static',
        'static_url_prefix':'/zhanggen/', #配置文件别名必须以/开头以/结尾
        'cookie_secret':'sssseertdfcvcvd'#配置加密cookie使用得加密字符串

    }
setings

 

设置加密的cokies

self.set_secure_cookie('username',user,expires=v)
self.set_secure_cookie('key',value,expires=过时时间)

 

获取加密的cokies

self.get_secure_cookie('username')
get_secure_cookie('key')

 

设置在用户不断刷新页面的状况,SecureCookies不过时;

import tornado.ioloop
import tornado.web
import time
class SeedListHandler(tornado.web.RequestHandler):
    def initialize(self):
        user=self.get_secure_cookie('username')
        if user:
            v=time.time()+10
            self.set_secure_cookie('username', user, expires=v)  #设置加密cookies
构造initialize方法

 

6.三、@authenticated 装饰器

执行 self.curent_user,有值就登陆用户,无就去执行get_curent_user方法,get_curent_user没有返回用户信息,会记录当前url更加配置文件跳转到登陆页面;

 

配置认证失败跳转的url

setings={
        'template_path':'templates',
        'static_path': 'static',
        'static_url_prefix':'/zhanggen/', #配置文件别名必须以/开头以/结尾
        'cookie_secret':'sssseertdfcvcvd',#配置加密cookie使用得加密字符串
        'login_url':'/login/'              #@authenticated 验证失败跳转的url
    }
setings

视图

import tornado.ioloop
import tornado.web
import time
from tornado.web import authenticated
class SeedListHandler(tornado.web.RequestHandler):
    def initialize(self):
        user=self.get_secure_cookie('username')
        if user:
            v=time.time()+10
            self.set_secure_cookie('username', user, expires=v)  #设置加密cookies

    def get_current_user(self):
        return self.get_secure_cookie('username')

    @authenticated #执行 self.curent_user,有值就登陆用户,无就去执行get_curent_user方法
    def get(self, *args, **kwargs):
        self.write('种子列表')
视图
if user == 'zhanggen' and pwd=='123.com':
            v = time.time() + 10
            self.set_secure_cookie('username',user,expires=v)
            net_url=self.get_query_argument ('next',None)
            if not net_url:
                net_url='/index/'
            self.redirect(net_url)
            return
获取即将跳转的url

 

 

 

3、Tornado特点功能

Tornado有2大特点:原生支持WebSocket协议、异步非阻塞的Web框架

 

一、WebSocket协议

HTTP和WebSocket协议都是基于TCP协议的,不一样于HTTP协议的是WebSocket和服务端创建是长链接且链接成功以后,会建立一个全双工通道,这时服务端能够向客户端推送消息,客户端也能够向服务端推送消息,其本质是保持TCP链接,在浏览器和服务端经过Socket进行通讯,因为WebSocket协议创建的是双向全双工通道,因此客户端(浏览器)和服务端(Web框架)双方都要支持WebSocket协议,Tornado原生支持这种协议;

 

1.0、WebSocket 和HTTP轮询、长轮询、长链接的区别?

HTTP轮询:

每间隔1段时间 向服务端发送http请求;

优势:后端程序编写比较容易。
缺点:请求中有大半是无用,浪费带宽和服务器资源,有数据延迟。
实例:适于小型应用。

 

HTTP长轮询:

每间隔1段时间 向服务端发送http请求,服务器接收到请求以后hold住本次链接1段时间,客户端进入pending状态;

若是在hold期间服务端有新消息:会当即响应给客户端;

若是没有新消息:超过hold时间,服务端会放开客户端;

一直循环往复;

 

优势:在无消息的状况下不会频繁的请求。
缺点:服务器hold链接会消耗资源
实例:WebQQ、WEB微信、Hi网页版、Facebook IM。

 

HTTP长链接:

客户端就发送1个长链接的请求,服务器端就能源源不断地往客户端输入数据。

优势:消息即时到达,客户端无需重复发送请求。
缺点:服务器维护一个长链接会增长开销。

 

WebSocket 协议:

服务端和客户端链接创建全双工通道一直不断开;

优势:实现了实时通信

缺点:旧版本浏览器不支持WebSocket协议,兼容性不强;(这也行也是腾讯的WEB微信、WEBQQ不使用该协议的缘由吧?)

 

 

1.一、实现WebSocket

实现WebScoket协议,须要遵循2项规则 建立WebSocket链接、服务端对封包和解包

 

a、创建链接

步骤1:客户端向server端发送请求中,请求信息中携带Sec-WebSocket-Key: jnqJRYC7EgcTK8OCkVnu9w==\r\n;

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
    <div>
        <input type="text" id="txt"/>
        <input type="button" id="btn" value="提交" onclick="sendMsg();"/>
        <input type="button" id="close" value="关闭链接" onclick="closeConn();"/>
    </div>
    <div id="content"></div>

<script type="text/javascript">
    var socket = new WebSocket("ws://127.0.0.1:8002");

    socket.onopen = function () {
        /* 与服务器端链接成功后,自动执行 */

        var newTag = document.createElement('div');
        newTag.innerHTML = "【链接成功】";
        document.getElementById('content').appendChild(newTag);
    };

    socket.onmessage = function (event) {
        /* 服务器端向客户端发送数据时,自动执行 */
        var response = event.data;
        var newTag = document.createElement('div');
        newTag.innerHTML = response;
        document.getElementById('content').appendChild(newTag);
    };

    socket.onclose = function (event) {
        /* 服务器端主动断开链接时,自动执行 */
        var newTag = document.createElement('div');
        newTag.innerHTML = "【关闭链接】";
        document.getElementById('content').appendChild(newTag);
    };

    function sendMsg() {
        var txt = document.getElementById('txt');
        socket.send(txt.value);
        txt.value = "";
    }
    function closeConn() {
        socket.close();
        var newTag = document.createElement('div');
        newTag.innerHTML = "【关闭链接】";
        document.getElementById('content').appendChild(newTag);
    }

</script>
</body>
</html>
JavaScript客户端

 

步骤2:服务端接收到客户端请求,获取请求头,从中获取Sec-WebSocket-Key;

 

步骤3:获取到的Sec-WebSocket-Key对应的字符和magic_string进行拼接; 

magic_string = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'  #固定且全球惟一
value = headers['Sec-WebSocket-Key'] + magic_string

 

步骤4:设置响应头,步骤3拼接完成以后的结果进行 base64加密;

ac = base64.b64encode(hashlib.sha1(value.encode('utf-8')).digest())
GET / HTTP/1.1\r\n

Host: 127.0.0.1:8002\r\n

Connection: Upgrade\r\n

Pragma: no-cache\r\n

Cache-Control: no-cache\r\n

Upgrade: websocket\r\n

Origin: http://localhost:63342\r\n

Sec-WebSocket-Version: 13\r\n

User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36\r\n

Accept-Encoding: gzip, deflate, br\r\n

Accept-Language: zh-CN,zh;q=0.8\r\n

Cookie: csrftoken=Om7ZrGEiMyYdx3F6xJmD5ycSWllhDc1D7SXRZKBoj7geGrQ3uwCHkCDdEJRWN1Zg; key="2|1:0|10:1513731498|3:key|12:emhhbmdnZW4=|664ad11ac6e040938f32893d7515f0680b171c39d0f99b918c3366a397f9331c"\r\n

Sec-WebSocket-Key: jnqJRYC7EgcTK8OCkVnu9w==\r\n


Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits\r\n\r\n'
WebSocket响应头格式

 

b、数据传输(解包、封包

客户端和服务端传输数据时,须要对数据进行【封包】和【解包】。客户端的JavaScript类库已经封装【封包】和【解包】过程,但Socket服务端须要手动实现。

 

步骤1:Socket服务端接收客户端发送的数据,并对其解包;

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
    <div>
        <input type="text" id="txt"/>
        <input type="button" id="btn" value="提交" onclick="sendMsg();"/>
        <input type="button" id="close" value="关闭链接" onclick="closeConn();"/>
    </div>
    <div id="content"></div>

<script type="text/javascript">
    var socket = new WebSocket("ws://127.0.0.1:8002");

    socket.onopen = function () {
        /* 与服务器端链接成功后,自动执行 */

        var newTag = document.createElement('div');
        newTag.innerHTML = "【链接成功】";
        document.getElementById('content').appendChild(newTag);
    };

    socket.onmessage = function (event) {
        /* 服务器端向客户端发送数据时,自动执行 */
        var response = event.data;
        var newTag = document.createElement('div');
        newTag.innerHTML = response;
        document.getElementById('content').appendChild(newTag);
    };

    socket.onclose = function (event) {
        /* 服务器端主动断开链接时,自动执行 */
        var newTag = document.createElement('div');
        newTag.innerHTML = "【关闭链接】";
        document.getElementById('content').appendChild(newTag);
    };

    function sendMsg() {
        var txt = document.getElementById('txt');
        socket.send(txt.value);
        txt.value = "";
    }
    function closeConn() {
        socket.close();
        var newTag = document.createElement('div');
        newTag.innerHTML = "【关闭链接】";
        document.getElementById('content').appendChild(newTag);
    }

</script>
</body>
</html>
JavaScript类库已经封装【封包】和【解包】过程

 

conn, address = sock.accept()
    data = conn.recv(1024)
    headers = get_headers(data)
    response_tpl = "HTTP/1.1 101 Switching Protocols\r\n" \
                   "Upgrade:websocket\r\n" \
                   "Connection:Upgrade\r\n" \
                   "Sec-WebSocket-Accept:%s\r\n" \
                   "WebSocket-Location:ws://%s%s\r\n\r\n"

    value = headers['Sec-WebSocket-Key'] + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
    ac = base64.b64encode(hashlib.sha1(value.encode('utf-8')).digest())
    response_str = response_tpl % (ac.decode('utf-8'), headers['Host'], headers['url'])
    conn.send(bytes(response_str, encoding='utf-8'))
Socket解包+回应完成握手

 

 

 步骤2:Socket服务端对发送给服务端的数据进行封包;

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import socket
import base64
import hashlib


def get_headers(data):
    """
    将请求头格式化成字典
    :param data:
    :return:
    """
    header_dict = {}
    data = str(data, encoding='utf-8')

    header, body = data.split('\r\n\r\n', 1)
    header_list = header.split('\r\n')
    for i in range(0, len(header_list)):
        if i == 0:
            if len(header_list[i].split(' ')) == 3:
                header_dict['method'], header_dict['url'], header_dict['protocol'] = header_list[i].split(' ')
        else:
            k, v = header_list[i].split(':', 1)
            header_dict[k] = v.strip()
    return header_dict


def send_msg(conn, msg_bytes):
    """
    WebSocket服务端向客户端发送消息
    :param conn: 客户端链接到服务器端的socket对象,即: conn,address = socket.accept()
    :param msg_bytes: 向客户端发送的字节
    :return:
    """
    import struct

    token = b"\x81"
    length = len(msg_bytes)
    if length < 126:
        token += struct.pack("B", length)
    elif length <= 0xFFFF:
        token += struct.pack("!BH", 126, length)
    else:
        token += struct.pack("!BQ", 127, length)

    msg = token + msg_bytes
    conn.send(msg)
    return True


def run():
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.bind(('127.0.0.1', 8002))
    sock.listen(5)

    conn, address = sock.accept()
    data = conn.recv(1024)
    headers = get_headers(data)
    response_tpl = "HTTP/1.1 101 Switching Protocols\r\n" \
                   "Upgrade:websocket\r\n" \
                   "Connection:Upgrade\r\n" \
                   "Sec-WebSocket-Accept:%s\r\n" \
                   "WebSocket-Location:ws://%s%s\r\n\r\n"

    value = headers['Sec-WebSocket-Key'] + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
    ac = base64.b64encode(hashlib.sha1(value.encode('utf-8')).digest())
    response_str = response_tpl % (ac.decode('utf-8'), headers['Host'], headers['url'])
    conn.send(bytes(response_str, encoding='utf-8'))

    while True:
        try:
            info = conn.recv(8096)
        except Exception as e:
            info = None
        if not info:
            break
        payload_len = info[1] & 127
        if payload_len == 126:
            extend_payload_len = info[2:4]
            mask = info[4:8]
            decoded = info[8:]
        elif payload_len == 127:
            extend_payload_len = info[2:10]
            mask = info[10:14]
            decoded = info[14:]
        else:
            extend_payload_len = None
            mask = info[2:6]
            decoded = info[6:]

        bytes_list = bytearray()
        for i in range(len(decoded)):
            chunk = decoded[i] ^ mask[i % 4]
            bytes_list.append(chunk)
        body = str(bytes_list, encoding='utf-8')
        send_msg(conn, body.encode('utf-8'))

    sock.close()


if __name__ == '__main__':
    run()
View Code

 

WebSocket协议参考博客:http://www.cnblogs.com/wupeiqi/p/6558766.html

 

 

1.二、基于Tornado实现Web聊天室

Tornado是一个支持WebSocket的优秀框架,固然Tornado内部封装功能更加完整,如下是基于Tornado实现的聊天室示例:

 

模板语言

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Python聊天室</title>
</head>
<body>
    <div>
        <input type="text" id="txt"/>
        <input type="button" id="btn" value="提交" onclick="sendMsg();"/>
        <input type="button" id="close" value="关闭链接" onclick="closeConn();"/>
    </div>
    <div id="container" style="border: 1px solid #dddddd;margin: 20px;min-height: 500px;">

    </div>

    <script src="/static/jquery-3.2.1.min.js"></script>
    <script type="text/javascript">
        $(function () {
            wsUpdater.start();
        });

        var wsUpdater = {
            socket: null,
            uid: null,
            start: function() {
                var url = "ws://127.0.0.1:8009/chat";
                wsUpdater.socket = new WebSocket(url);
                wsUpdater.socket.onmessage = function(event) {
                    console.log(event);
                    if(wsUpdater.uid){
                        wsUpdater.showMessage(event.data);
                    }else{
                        wsUpdater.uid = event.data;
                    }
                }
            },
            showMessage: function(content) {
                $('#container').append(content);
            }
        };

        function sendMsg() {
            var msg = {
                uid: wsUpdater.uid,
                message: $("#txt").val()
            };
            wsUpdater.socket.send(JSON.stringify(msg));
        }

</script>

</body>
</html>
index.html
<div style="border: 1px solid #dddddd;margin: 10px;">
    <div>游客{{uid}}</div>
    <div style="margin-left: 20px;">{{message}}</div>
</div>
message.html

 

视图

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import socket
import base64
import hashlib


def get_headers(data):
    """
    将请求头格式化成字典
    :param data:
    :return:
    """
    header_dict = {}
    data = str(data, encoding='utf-8')

    header, body = data.split('\r\n\r\n', 1)
    header_list = header.split('\r\n')
    for i in range(0, len(header_list)):
        if i == 0:
            if len(header_list[i].split(' ')) == 3:
                header_dict['method'], header_dict['url'], header_dict['protocol'] = header_list[i].split(' ')
        else:
            k, v = header_list[i].split(':', 1)
            header_dict[k] = v.strip()
    return header_dict


def send_msg(conn, msg_bytes):
    """
    WebSocket服务端向客户端发送消息
    :param conn: 客户端链接到服务器端的socket对象,即: conn,address = socket.accept()
    :param msg_bytes: 向客户端发送的字节
    :return:
    """
    import struct

    token = b"\x81"
    length = len(msg_bytes)
    if length < 126:
        token += struct.pack("B", length)
    elif length <= 0xFFFF:
        token += struct.pack("!BH", 126, length)
    else:
        token += struct.pack("!BQ", 127, length)

    msg = token + msg_bytes
    conn.send(msg)
    return True


def run():
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.bind(('127.0.0.1', 8002))
    sock.listen(5)

    conn, address = sock.accept()
    data = conn.recv(1024)
    headers = get_headers(data)
    response_tpl = "HTTP/1.1 101 Switching Protocols\r\n" \
                   "Upgrade:websocket\r\n" \
                   "Connection:Upgrade\r\n" \
                   "Sec-WebSocket-Accept:%s\r\n" \
                   "WebSocket-Location:ws://%s%s\r\n\r\n"

    value = headers['Sec-WebSocket-Key'] + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
    ac = base64.b64encode(hashlib.sha1(value.encode('utf-8')).digest())
    response_str = response_tpl % (ac.decode('utf-8'), headers['Host'], headers['url'])
    conn.send(bytes(response_str, encoding='utf-8'))

    while True:
        try:
            info = conn.recv(8096)
        except Exception as e:
            info = None
        if not info:
            break
        payload_len = info[1] & 127
        if payload_len == 126:
            extend_payload_len = info[2:4]
            mask = info[4:8]
            decoded = info[8:]
        elif payload_len == 127:
            extend_payload_len = info[2:10]
            mask = info[10:14]
            decoded = info[14:]
        else:
            extend_payload_len = None
            mask = info[2:6]
            decoded = info[6:]

        bytes_list = bytearray()
        for i in range(len(decoded)):
            chunk = decoded[i] ^ mask[i % 4]
            bytes_list.append(chunk)
        body = str(bytes_list, encoding='utf-8')
        send_msg(conn, body.encode('utf-8'))

    sock.close()


if __name__ == '__main__':
    run()
View Code

 

 

二、异步非阻塞介绍

Web框架分阻塞式和异步非阻塞2种;

 

2.1.阻塞式IO(Django、Flask、Bottle)

大多数的Web框架都是阻塞式的,体如今1个请求到达服务端若是服务端未处理完该请求,后续请求一直等待;

解决方案:

开启多线程/多进程:多个线程提升并发;

import tornado.ioloop
import time
import tornado.web
import tornado.websocket
from tornado.httpserver import HTTPServer
class IndexHadlar(tornado.web.RequestHandler):
    def get(self):
        print('请求开始')
        time.sleep(10)
        self.write('hello,world ')
        print("请求结束")
application=tornado.web.Application([
    (r'/index/',IndexHadlar)
])


if __name__ == '__main__':
    # 单线程模式
    # application.listen(8888)
    # tornado.ioloop.IOLoop.instance().start()
    # 多线程模式
    server=HTTPServer(application)
    server.bind(8888)
    server.start(3) #开启4个进程
    tornado.ioloop.IOLoop.instance().start()
Tornado多进程模式(仅支持Linux平台)

缺点:浪费系统资源

 

 

2.二、Tornado异步非阻塞(Tornado/NodeJS)

异步非阻塞就是在服务端结合IO多路复用select/poll/epoll模板,作到1个线程在遇到IO操做的状况下,还能够作一些其余的任务;Tornado默认是阻塞的同时也支持异步非阻塞功能;

Tornado异步非阻塞=IO多路复用(循环检查socket是否发生变化)+携程(哪一个有变化?就切换到那个socket!)

 

 

1.客户端发送请求若是请求内容不涉及IO操做(链接数据、还得去其余网站获取内容)服务端直接响应客户端;

 

2.若是请求内容涉及IO操做,服务端把本次链接的socket信息添加到socket监听列表中监听起来;

而后去链接其它socket(数据库、其它站点)因为是不阻塞的因此服务端把此次发送socket信息也监听起来;(一直循环监听,直到socket监听列表中的socket发生变化)

 

3.把socket所有监听以后,就能够去继续接收其它请求了,若是检测到socket监听列表中的socket有变化(有数据返回),找到对应socket响应数据,并从socket监听列表中剔除;

 

小结:

Tornado的异步非阻塞,本质上是请求到达视图 一、先yield 1个Future对象  二、 IO多路复用模块把该socket添加到监听列表循环监听起来;三、 循环监听过程当中哪1个socket发生变化有response,执行 Future.set_result(response),请求至此返回结束,不然socket链接一直不断开,IO多路复用模块一直循环监听socket是否发生变化?;

 

当发送GET请求时,因为方法被@gen.coroutine装饰且yield 一个 Future对象,那么Tornado会等待,等待用户向future对象中放置数据或者发送信号,若是获取到数据或信号以后,就开始执行doing方法。

异步非阻塞体如今当在Tornaod等待用户向future对象中放置数据时,还能够处理其余请求。

注意:在等待用户向future对象中放置数据或信号时,此链接是不断开的。

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import tornado.ioloop
import time
import tornado.web
import tornado.websocket
from tornado import gen             #导入
from tornado.concurrent import Future
import time

class IndexHadlar(tornado.web.RequestHandler):
    @gen.coroutine #coroutine(携程装饰器)
    def get(self):
        print('请求开始')
        future=Future()
        tornado.ioloop.IOLoop.current().add_timeout(time.time()+10,self.doing)
        yield future #yield 1个future对象,IO以后自动切换到doing方法执行;

    def doing(self):
        self.write('请求完成')
        self.finish()           #关闭链接


application=tornado.web.Application([
    (r'/index/',IndexHadlar)
])


if __name__ == '__main__':
    # 单进程模式
    application.listen(8888)
    tornado.ioloop.IOLoop.instance().start()
Tornado异步非阻塞模式

 

 

2.三、Tornado httpclient类库

若是服务端接受到客户端的请求,须要去其余API获取数据,再响应给客户端,这就涉及到了IO操做,Tornado提供了httpclient类库用于发送Http请求,其配合Tornado的异步非阻塞使用。

#!/usr/bin/env python
# -*- coding:utf-8 -*-

import tornado.web
from tornado.web import RequestHandler
from tornado import gen
from tornado import httpclient


class AsyncHandler(RequestHandler):
    @gen.coroutine
    def get(self):
        print('收到报警')
        http=httpclient.AsyncHTTPClient()
        yield http.fetch('https://github.com',self.done)

    def done(self,respose,*args,**kwargs):
        print(respose)
        self.write('推送成功')
        self.finish()


application = tornado.web.Application([
    (r"/zhanggen/", AsyncHandler),
])

if __name__ == '__main__':
    application.listen(8888)
    tornado.ioloop.IOLoop.instance().start()
httpclient模块

 

 

2.三、Tornado-MySQL类库

若是服务端接收到客户端请求,须要链接数据库再把查询的结果响应客户端,这个过程当中链接数据、发送查询SQL、接收数据库返回结果 都会遇到IO阻塞、耗时的问题,因此Tornado提供了Tornado-MySQL模块(对PyMySQL进行二次封装),让咱们在使用数据库的时候也能够作到异步非阻塞。

# yield cur.execute("SELECT name,email FROM web_models_userprofile where name=%s", (user,))
方式1

 

方式1 须要对每一个IO操做分别yeild,操做起来比较繁琐,因此能够经过task的方式把IO操做封装到函数中统一进行异步处理(不管什么方式本质都会yelid 1个Future对象);

#!/usr/bin/env python
# -*- coding:utf-8 -*-
"""
须要先安装支持异步操做Mysql的类库:
    Tornado-MySQL: https://github.com/PyMySQL/Tornado-MySQL#installation

    pip3 install Tornado-MySQL

"""

import tornado.web
from tornado import gen

import tornado_mysql
from tornado_mysql import pools

POOL = pools.Pool(
    dict(host='127.0.0.1', port=3306, user='root', passwd='123', db='cmdb'),
    max_idle_connections=1,
    max_recycle_sec=3)


@gen.coroutine
def get_user_by_conn_pool(user):
    cur = yield POOL.execute("SELECT SLEEP(%s)", (user,))
    row = cur.fetchone()
    raise gen.Return(row)


@gen.coroutine
def get_user(user):
    conn = yield tornado_mysql.connect(host='127.0.0.1', port=3306, user='root', passwd='123', db='cmdb',
                                       charset='utf8')
    cur = conn.cursor()
    # yield cur.execute("SELECT name,email FROM web_models_userprofile where name=%s", (user,))
    yield cur.execute("select sleep(10)")
    row = cur.fetchone()
    cur.close()
    conn.close()
    raise gen.Return(row)


class LoginHandler(tornado.web.RequestHandler):
    def get(self, *args, **kwargs):
        self.render('login.html')

    @gen.coroutine
    def post(self, *args, **kwargs):
        user = self.get_argument('user')
        data = yield gen.Task(get_user, user)  #把函数添加任务
        if data:
            print(data)
            self.redirect('http://www.oldboyedu.com')
        else:
            self.render('login.html')


application = tornado.web.Application([
    (r"/login", LoginHandler),
])

if __name__ == "__main__":
    application.listen(8888)
    tornado.ioloop.IOLoop.instance().start()
方式2

 

 

三、使用 Tornado异步非阻塞功能小结:

一、视图之上加@gen.coroutine装饰器

二、yield Future()

三、Future对象的set_result()执行请求会当即返回;

 

 

 

 

4、Tornado功能扩展

一、session

Tornado原生不带session,因此须要自定制session框架;

自定制session知识储备

a、python的 __getitem__、__setitem__,__delitem__内置方法

class Foo(object):
    def __getitem__(self, item):
        return 666
    def __setitem__(self, key, value):
        pass
    def __delitem__(self, key):
        pass

obj=Foo()
print(obj['name'])   #Python的[]语法,会自动执行对象的__getitem__方法;

obj['name']=888      #会自动执行对象的__setitem__方法

del obj['name']     #会自动执行对象的__delitem__方法

class Yuchao(object):
    def __init__(self,num):
        self.num=num
    def __add__(self, other):
        return self.num+other.num

'''
python 内置的方法
__new__
__init__
__add__
__getitem__
__setitem__
__delitem__
__call__

'''
a=Yuchao('5')
b=Yuchao('5')

print(a+b)
View Code

 

b、Tornado在请求处理以前先执行initialize方法;

模板语言

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href='{{static_url("dist/css/bootstrap.css") }}'>
    <title>Title</title>
</head>
<body>
<div class="container">
    <div class="row">
        <div class="col-md-5 col-md-offset-3">
            <form method="post" >
                {%raw xsrf_form_html() %}
                  <div class="form-group">
                    <input type="text" class="form-control" placeholder="用户名" name="user">
                  </div>
                  <div class="form-group">
                    <input type="password" class="form-control" placeholder="密码" name="pwd">
                  </div>
                  <button type="submit" class="btn btn-default">提交</button>
                  <p>{{msg}}</p>
            </form>
        </div>
    </div>
</div>
</body>
</html>
login.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h2>首页</h2>
<h1>循环列表</h1>
<ul>
    {% for item in userlist %}
    <li>{{item}} </li>
    {% end %}
    <!--注意不是Django里面的enfor直接end if 也是end-->
</ul>
<h1>列表索引取值</h1>
{{userlist[1]}}

<h1>循环字典</h1>
<ul>
    {% for item in userdict.items() %}
    <li>{{item}} </li>
    {% end %}
    <!--注意不是Django里面的enfor直接end if 也是end-->
</ul>

<h1>字典索引取值</h1>
{{userdict['name']}}
{{userdict.get('age')}}

</body>
</html>
index.html

 

c、自定制session

from hashlib import sha1
import os, time
create_session_id = lambda: sha1(bytes('%s%s' % (os.urandom(16), time.time()), encoding='utf-8')).hexdigest()

contatiner={}

class Zg(object):
    def __init__(self,handler ):
        self.handler=handler
        random_str=self.handler.get_cookie('MySessionId') #获取用户cokies中的随机字符串
        if not random_str: #若是没有随机字符串,则建立1个;
            random_str=create_session_id()
            contatiner[random_str]={}
        else:              #若是有检查是不是伪造的随机字符串?
            if random_str not in contatiner:
                random_str = create_session_id()#伪造的从新生产一个
                contatiner[random_str] = {}
        self.random_str=random_str              #最后生成随机字符串
        self.handler.set_cookie('MySessionId',random_str,max_age=10) #把随机字符串,写到用户cokies中;

    def __getitem__(self, item):

        return contatiner[self.random_str].get(item)

    def __setitem__(self, key, value):
        contatiner[self.random_str][key]=value
    def __delitem__(self, key):
        if contatiner[self.random_str][key]:
            del contatiner[self.random_str][key]
session
class LoginHandler(tornado.web.RequestHandler):
    def initialize(self):
        self.session=Zg(self) #sel是Handler对象,方便获取cokies
    def get(self):
        self.render('login.html',**{'msg':''})
    def post(self):
        user = self.get_argument('user')
        pwd = self.get_argument('pwd')
        if user == 'zhanggen' and pwd == '123.com':
            self.session['user_info']=user
            self.redirect('/index/')
            return
        self.render('login.html', **{'msg': '用户名/密码错误'})

class IndexHandler(tornado.web.RequestHandler):
    def initialize(self):
        self.session = Zg(self)
    def get(self):
        username = self.session['user_info']
        if not username:
            self.redirect('/login/')
            return
        userlist = ['张根', '于超', '李兆宇']
        userdict = {'name': '张根', 'gender': 'man', 'age': 18}
        print(contatiner)
        self.render('index.html', **{'userlist': userlist, 'userdict': userdict})
应用session

 

1.一、分布式存储session信息

 

N个鸡蛋不能放在1个篮子,若是想要把N个鸡蛋放在N个篮子里,须要解决如下2个问题;

问题1:经过什么机制判断哪1个鸡蛋应该放在哪1个篮子里?

问题2:想要吃吃某1个鸡蛋时 要已O1的时间复杂度,把它快速取出来;

就就须要一致性hash算法了; 

 

 一致性hash算法逻辑:

0、定义一个socket地址列表 ['192.168.1.1:6379','192.168.1.2:6379','192.168.1.3:6379']

一、每次链接数据库的请求过来,获取当前用户生成1个惟一的随机字符串,而后根据ASCII表把该字符串转换成对应的数字 N;asdsdffrdf ==> 1234
二、数字N和socket地址列表的长度求余(N%len(socket地址列表)),获得socket地址列表中的index,进而根据索引获取socket地址列表中的socket;

三、即便取余也没法保证平均,若是增长权重呢?多出现几回,增长出现机率; v=['192.168.1.1:6379','192.168.1.2:6379','192.168.1.3:6379','192.168.1.1:6379','192.168.1.1:6379',]

4.若是想要获取放进去的session信息就拿着那1个步骤1生成的惟一的随机字符串过来,反解步骤一、2便可;

 

 Python3一致性hash模块

# -*- coding: utf-8 -*-
"""
    hash_ring
    ~~~~~~~~~~~~~~
    Implements consistent hashing that can be used when
    the number of server nodes can increase or decrease (like in memcached).

    Consistent hashing is a scheme that provides a hash table functionality
    in a way that the adding or removing of one slot
    does not significantly change the mapping of keys to slots.

    More information about consistent hashing can be read in these articles:

        "Web Caching with Consistent Hashing":
            http://www8.org/w8-papers/2a-webserver/caching/paper2.html

        "Consistent hashing and random trees:
        Distributed caching protocols for relieving hot spots on the World Wide Web (1997)":
            http://citeseerx.ist.psu.edu/legacymapper?did=38148


    Example of usage::

        memcache_servers = ['192.168.0.246:11212',
                            '192.168.0.247:11212',
                            '192.168.0.249:11212']

        ring = HashRing(memcache_servers)
        server = ring.get_node('my_key')

    :copyright: 2008 by Amir Salihefendic.
    :license: BSD
"""

import math
import sys
from bisect import bisect

if sys.version_info >= (2, 5):
    import hashlib
    md5_constructor = hashlib.md5
else:
    import md5
    md5_constructor = md5.new

class HashRing(object):

    def __init__(self, nodes=None, weights=None):
        """`nodes` is a list of objects that have a proper __str__ representation.
        `weights` is dictionary that sets weights to the nodes.  The default
        weight is that all nodes are equal.
        """
        self.ring = dict()
        self._sorted_keys = []

        self.nodes = nodes

        if not weights:
            weights = {}
        self.weights = weights

        self._generate_circle()

    def _generate_circle(self):
        """Generates the circle.
        """
        total_weight = 0
        for node in self.nodes:
            total_weight += self.weights.get(node, 1)

        for node in self.nodes:
            weight = 1

            if node in self.weights:
                weight = self.weights.get(node)

            factor = math.floor((40*len(self.nodes)*weight) / total_weight)

            for j in range(0, int(factor)):
                b_key = self._hash_digest( '%s-%s' % (node, j) )

                for i in range(0, 3):
                    key = self._hash_val(b_key, lambda x: x+i*4)
                    self.ring[key] = node
                    self._sorted_keys.append(key)

        self._sorted_keys.sort()

    def get_node(self, string_key):
        """Given a string key a corresponding node in the hash ring is returned.

        If the hash ring is empty, `None` is returned.
        """
        pos = self.get_node_pos(string_key)
        if pos is None:
            return None
        return self.ring[ self._sorted_keys[pos] ]

    def get_node_pos(self, string_key):
        """Given a string key a corresponding node in the hash ring is returned
        along with it's position in the ring.

        If the hash ring is empty, (`None`, `None`) is returned.
        """
        if not self.ring:
            return None

        key = self.gen_key(string_key)

        nodes = self._sorted_keys
        pos = bisect(nodes, key)

        if pos == len(nodes):
            return 0
        else:
            return pos

    def iterate_nodes(self, string_key, distinct=True):
        """Given a string key it returns the nodes as a generator that can hold the key.

        The generator iterates one time through the ring
        starting at the correct position.

        if `distinct` is set, then the nodes returned will be unique,
        i.e. no virtual copies will be returned.
        """
        if not self.ring:
            yield None, None

        returned_values = set()
        def distinct_filter(value):
            if str(value) not in returned_values:
                returned_values.add(str(value))
                return value

        pos = self.get_node_pos(string_key)
        for key in self._sorted_keys[pos:]:
            val = distinct_filter(self.ring[key])
            if val:
                yield val

        for i, key in enumerate(self._sorted_keys):
            if i < pos:
                val = distinct_filter(self.ring[key])
                if val:
                    yield val

    def gen_key(self, key):
        """Given a string key it returns a long value,
        this long value represents a place on the hash ring.

        md5 is currently used because it mixes well.
        """
        b_key = self._hash_digest(key)
        return self._hash_val(b_key, lambda x: x)

    def _hash_val(self, b_key, entry_fn):
        return (( b_key[entry_fn(3)] << 24)
                |(b_key[entry_fn(2)] << 16)
                |(b_key[entry_fn(1)] << 8)
                | b_key[entry_fn(0)] )

    def _hash_digest(self, key):
        m = md5_constructor()
        m.update(key.encode('utf-8'))
        # return map(ord, m.digest())
        return list(m.digest())
hash_ring.py

 

 

二、自定义Form组件

Form组件2大功能:自动生成html标签 +对用户数据进行验证

 

待续。。。。

 

 

三、自定义中间件

 tornado在执行视图以前会先执行initialize  prepare方法,完成响应以后会执行finish方法,利用这个特性就能够作一个相似Django中间件的功能;

import tornado.ioloop
import tornado.web
class MiddleWare1(object):
   def process_request(self,request):
       #request 是RequestHandler的实例
       print('访问前通过中间件ware1')
   def process_response(self,request):
       print('访问结束通过中间件ware1')


class BaseMiddleWare(object):
   middleware = [MiddleWare1(),]



class MiddleRequestHandler(BaseMiddleWare,tornado.web.RequestHandler):

   def prepare(self):                           #从新父类的 prepare方法(默认是pass)
       for middleware in self.middleware:
           middleware.process_request(self)

   def finish(self, chunk=None):               #重写父类finish方法
       for middleware in self.middleware:
           middleware.process_response(self)
       super(MiddleRequestHandler,self).finish()  #注意最后须要执行父类RequestHandler的finish方法才能结束;

   def get(self, *args, **kwargs):
        self.write('hhhhhhhh')

   def post(self, *args, **kwargs):
       print(self.request)
       self.write('post')


application = tornado.web.Application([
       (r'/index/',MiddleRequestHandler),
   ]
)

if __name__ == '__main__':
   application.listen(8888)
   tornado.ioloop.IOLoop.instance().start()
# 注:在tornado中要实现中间件的方式,经过prepare和finish这两种方法
自定义中间件

 

 

 

 

 

 

银角大王博客:

http://www.cnblogs.com/wupeiqi/articles/5341480.html

http://www.cnblogs.com/wupeiqi/p/5938916.html(自定义Form组件)

http://www.cnblogs.com/wupeiqi/articles/5702910.html

相关文章
相关标签/搜索