• PythonWEB框架之Tornado


    前言

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

    一、Tornado框架的基本组成

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

    复制代码
    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:有
      其他:无
        
    复制代码

     

    二、Tornado自带功能

    1、Tornado执行流程

    复制代码
    #准备安装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()进入监听状态
    复制代码

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

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

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

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

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

    配置文件:

    复制代码
    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 配置文件
                                )
    复制代码

    2、路由系统

    2.1、动态路由(url传参数)

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

    2.2、域名匹配 

    #支持域名匹配  www.zhanggen.com:8888/index/333333
    app.add_handlers('www.zhanggen.com',[
    
            (r'^/index/$', MainHandler),
            (r'^/index/(d+)$', MainHandler),
    ])

    2.3、反向生成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')

    3、视图

    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()
    复制代码

    3.1、请求相关

    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.2、响应相关

    self.write() :响应字符串

    self.render():响应页面

    self.redirect():页面跳转

    4、模板语言

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

    View Code

    4.1、登录页面

    复制代码
    #准备安装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()进入监听状态
    复制代码
    复制代码
    <!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.2、引入静态文件

    <link rel="stylesheet" href="/zhanggen/coment.css">
    <link rel="stylesheet" href='{{static_url("dist/css/bootstrap.css") }}'>

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

    1、使用static_url()可以不用考虑静态文件修改之后造成引用失效的情况;

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

    4.3、上下文对象

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

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

    5、xsrf_tocken认证

    setings={
    'template_path':'templates',#配置模板路径
    'static_path':'static',     #配置静态文件存放的路径
    'static_url_prefix':'/zhanggen/', #在模板中引用静态文件路径时使用的别名 注意是模板引用时的别名
    "xsrf_cookies": True,           #使用xsrf认证
    }
    复制代码
    <!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>
    复制代码

    6、cokies

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

    6.1、cookies

    设置cokies

      user=self.get_cookie('username')
            if user:
                v=time.time()+10
                self.set_cookie('username', user, expires=v)

    获取cokies

    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)
    复制代码

    6.2、Tornado加密cokies

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

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

    设置加密的cokies

    self.set_secure_cookie('username',user,expires=v)

    获取加密的cokies

    self.get_secure_cookie('username')

    设置在用户不断刷新页面的情况,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
    复制代码

    6.3、@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
        }
    复制代码

    视图

    复制代码
    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
    复制代码

    三、Tornado特色功能

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

    1、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.1、实现WebSocket

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

    a、建立连接

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

    复制代码
    <!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>
    复制代码

    步骤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
    
    
    Host: 127.0.0.1:8002
    
    
    Connection: Upgrade
    
    
    Pragma: no-cache
    
    
    Cache-Control: no-cache
    
    
    Upgrade: websocket
    
    
    Origin: http://localhost:63342
    
    
    Sec-WebSocket-Version: 13
    
    
    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
    
    
    Accept-Encoding: gzip, deflate, br
    
    
    Accept-Language: zh-CN,zh;q=0.8
    
    
    Cookie: csrftoken=Om7ZrGEiMyYdx3F6xJmD5ycSWllhDc1D7SXRZKBoj7geGrQ3uwCHkCDdEJRWN1Zg; key="2|1:0|10:1513731498|3:key|12:emhhbmdnZW4=|664ad11ac6e040938f32893d7515f0680b171c39d0f99b918c3366a397f9331c"
    
    
    Sec-WebSocket-Key: jnqJRYC7EgcTK8OCkVnu9w==
    
    
    
    Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
    
    '
    复制代码

    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>
    复制代码
    复制代码
        conn, address = sock.accept()
        data = conn.recv(1024)
        headers = get_headers(data)
        response_tpl = "HTTP/1.1 101 Switching Protocols
    " 
                       "Upgrade:websocket
    " 
                       "Connection:Upgrade
    " 
                       "Sec-WebSocket-Accept:%s
    " 
                       "WebSocket-Location:ws://%s%s
    
    "
    
        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'))
    复制代码

     步骤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('
    
    ', 1)
        header_list = header.split('
    ')
        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
    " 
                       "Upgrade:websocket
    " 
                       "Connection:Upgrade
    " 
                       "Sec-WebSocket-Accept:%s
    " 
                       "WebSocket-Location:ws://%s%s
    
    "
    
        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()
    复制代码

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

    1.2、基于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>
    复制代码
    <div style="border: 1px solid #dddddd;margin: 10px;">
        <div>游客{{uid}}</div>
        <div style="margin-left: 20px;">{{message}}</div>
    </div>

    视图

    复制代码
    #!/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('
    
    ', 1)
        header_list = header.split('
    ')
        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
    " 
                       "Upgrade:websocket
    " 
                       "Connection:Upgrade
    " 
                       "Sec-WebSocket-Accept:%s
    " 
                       "WebSocket-Location:ws://%s%s
    
    "
    
        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()
    复制代码

    2、异步非阻塞介绍

    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()
    复制代码

    缺点:浪费系统资源

     2.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的异步非阻塞,本质上是请求到达视图 1、先yield 1个Future对象  2、 IO多路复用模块把该socket添加到监听列表循环监听起来;3、 循环监听过程中哪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()
    复制代码

    2.3、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()
    复制代码

    2.3、Tornado-MySQL类库

    如果服务端接收到客户端请求,需要连接数据库再把查询的结果响应客户端,这个过程中连接数据、发送查询SQL、接收数据库返回结果 都会遇到IO阻塞、耗时的问题,所以Tornado提供了Tornado-MySQL模块(对PyMySQL进行二次封装),让我们在使用数据库的时候也可以做到异步非阻塞。

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

    方式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()
    复制代码

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

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

    2、yield Future()

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

    四、Tornado功能扩展

    1、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)
    复制代码

    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>
    复制代码
    复制代码
    <!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>
    复制代码

    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]
    复制代码
    复制代码
    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})
    复制代码

    1.1、分布式存储session信息

    多个鸡蛋不能放在1个篮子,可是如何放在多个篮子里呢?通过什么机制判断哪个鸡蛋应该放在哪个篮子里呢?就就需要一致性hash算法了; 

     一致性hash算法逻辑:

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

    1、每次连接数据库的请求过来都设置1个唯一的随机字符串,然后根据ASCII表把该字符串转换成对应的数字 N; asdsdffrdf ==> 1234

    2、数字N和socket地址列表的长度求余(N%len(socket地址列表)),得到socket地址列表中的index,进而根据索引获取socket地址列表中的socket;

    3、即使取余也无法保证平均,如果增加权重呢?多出现几次,增加出现机率; 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',]

     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模块

    复制代码
    from hash_ring import HashRing
    
    redis_server=['192.168.1.1:6379','192.168.1.2:6379','192.168.1.3:6379']
    weights={
        '192.168.1.1:6379':1,
        '192.168.1.2:6379':1,
        '192.168.1.3:6379':1,
    }
    
    ring=HashRing(redis_server,weights)
    
    ret=ring.get_node('随机字符串')#获取随机得 socket地址
    
    print(ret)
    复制代码

    2、自定义Form组件

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

    待续。。。。

    3、自定义中间件

     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

  • 相关阅读:
    HTTP网页错误代码大全带解释
    记录一下手把手教您做电商网站
    C#中的Attribute
    C#中dynamic的正确用法
    【CSP】最大的矩形
    【CSP】字符与int
    C++数组初始化
    C++中输出字符到文本文档
    C++ 中时钟函数的使用
    各种函数的头文件
  • 原文地址:https://www.cnblogs.com/ExMan/p/9825648.html
Copyright © 2020-2023  润新知