zoukankan      html  css  js  c++  java
  • 轮询、长轮询和websocket

    一、轮询

    在一些需要进行实时查询的场景下应用
    比如投票系统:
      大家一起在一个页面上投票
      在不刷新页面的情况下,实时查看投票结果

    1、后端代码

    from flask import Flask, render_template, request, jsonify
    
    
    app = Flask(__name__)
    
    USERS = {
        1: {'name': '明凯', 'count': 300},
        2: {'name': '厂长', 'count': 200},
        3: {'name': '7酱', 'count': 600},
    }
    
    
    @app.route('/')
    def index():
        return render_template('Poll.html', users=USERS)
    
    
    @app.route('/vote', methods=['POST'])
    def vote():
        # 接收uid,通过uid给打野票数 +1
        # 用户提交Json数据过来,用request.json获取
        uid = request.json.get('uid')
        USERS[uid]['count'] += 1
        return "投票成功"
    
    
    @app.route('/get_vote')
    def get_vote():
        # 返回users数据
        # jsonify 是flask自带的序列化器
        return jsonify(USERS)
    
    
    if __name__ == '__main__':
        app.run()

    2、前端代码

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>投票系统</title>
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css">
        <script src="https://cdn.bootcss.com/axios/0.19.0-beta.1/axios.js"></script>
    </head>
    <style>
        .my-li {
            list-style: none;
            margin-bottom: 20px;
            font-size: 18px;
        }
    </style>
    <body>
    <div class="container">
        <div class="row">
            <div class="col-md-6 col-md-offset-3">
                <h1>LPL第一打野投票</h1>
                {% for (uid, user) in users.items() %}
                    <button class="btn btn-success" onclick="vote({{ uid }})">投票</button>
                    <li class="list-group-item-info my-li" id="{{ uid }}">{{ user.name }}目前的票数是: {{ user.count }}</li>
                {% endfor %}
            </div>
        </div>
    </div>
    
    <script>
        // 投票
        function vote(uid) {
            // 向后端发送投票请求
            axios.request({
                url: '/vote',
                method: 'post',
                data: {
                    uid: uid
                }
            }).then(function (response) {
                console.log(response.data);
            })
        }
    
        // 获取最新的投票结果
        function get_vote() {
            axios.request({
                url: '/get_vote',
                method: 'get'
            }).then(function (response) {
                // 获取后端传过来的新数据
                // 重新渲染页面
                let users = response.data;
                for (let uid in users) {
                    //根据uid获取li标签 改变innerText
                    let liEle = document.getElementById(uid);
                    liEle.innerText = `${users[uid]['name']}目前的票数是: ${users[uid]['count']}`
                }
            });
        }
    
        // 页面加载完后,立刻获取数据
        window.onload = function () {
            setInterval(get_vote, 2000)
        }
    
    </script>
    
    </body>
    </html>

    3、轮询

    特点:每隔一段时间不断向后端发送请求
    缺点:消耗大 有延迟

    二、长轮询

    由于上面的轮询是不能实时查看到投票情况的,存在一定的延迟性
    长轮询可以实现实时查看投票情况

    1、后端代码

    from flask import Flask, render_template, request, jsonify, session
    import uuid
    import queue
    
    app = Flask(__name__)
    app.secret_key = '切克闹'
    
    USERS = {
        1: {'name': '明凯', 'count': 300},
        2: {'name': '厂长', 'count': 200},
        3: {'name': '7酱', 'count': 600},
    }
    
    Q_DICT = {
        # uid: q对象
    }
    
    
    @app.route('/')
    def index():
        # 模拟用户登录
        # 模拟用户登录后的唯一id
        user_id = str(uuid.uuid4())
        # 每个用户都有自己的Q对象
        Q_DICT[user_id] = queue.Queue()
        # 把用户的id存到session
        session['user_id'] = user_id
        # 页面展示投票的人的信息
        return render_template('longPoll.html', users=USERS)
    
    
    @app.route('/vote', methods=['POST'])
    def vote():
        # 处理投票,给打野的票数 +1
        # 用户提交Json数据过来,用request.json获取
        uid = request.json.get('uid')
        USERS[uid]['count'] += 1
        # 投票成功后,给每个用户的Q对象put最新的值进去
        for q in Q_DICT.values():
            q.put(USERS)
        return "投票成功"
    
    
    @app.route('/get_vote')
    def get_vote():
        # 请求进来,从session获取用户的id
        user_id = session.get('user_id')
        # 根据用户的id 获取用户的Q对象
        q = Q_DICT.get(user_id)
        try:
            ret = q.get(timeout=30)
        except queue.Empty:
            ret = ''
        except Exception:
            ret = ''
        return jsonify(ret)
    
    
    if __name__ == '__main__':
        app.run()

    2、前端代码

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>投票系统</title>
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css">
        <script src="https://cdn.bootcss.com/axios/0.19.0-beta.1/axios.js"></script>
    </head>
    <style>
        .my-li {
            list-style: none;
            margin-bottom: 20px;
            font-size: 18px;
        }
    </style>
    <body>
    <div class="container">
        <div class="row">
            <div class="col-md-6 col-md-offset-3">
                <h1>LPL第一打野投票</h1>
                {% for (uid, user) in users.items() %}
                    <button class="btn btn-success" onclick="vote({{ uid }})">投票</button>
                    <li class="list-group-item-info my-li" id="{{ uid }}">{{ user.name }}目前的票数是: {{ user.count }}</li>
                {% endfor %}
            </div>
        </div>
    </div>
    
    <script>
        // 投票
        function vote(uid) {
            // 向后端发送投票请求
            axios.request({
                url: '/vote',
                method: 'post',
                data: {
                    uid: uid
                }
            }).then(function (response) {
                console.log(response.data);
            })
        }
    
        // 获取最新的投票结果
        function get_vote() {
            axios.request({
                url: '/get_vote',
                method: 'get'
            }).then(function (response) {
                // 判断后端的数据是否为空
                if (response.data != '') {
                    // 获取到最新的数据
                    let users = response.data;
                    for (uid in users) {
                        // 根据uid找到每个li标签
                        let liEle = document.getElementById(uid);
                        // 给每个li标签设置最新的数据
                        liEle.innerText = `${users[uid]['name']}目前的票数是: ${users[uid]['count']}`
                    }
                }
                // 获取完数据后,再发送请求,看还有没有人投票,有的话再去获取最新的数据
                get_vote()
            });
        }
    
        // 页面加载完后,立刻获取数据
        window.onload = function () {
            get_vote()
        }
    
    </script>
    
    </body>
    </html>

    3、长轮询

    特点:满足实时更新
    缺点:消耗大
    实现:
      利用queue对象实现请求夯住
      每个请求进来都要生成一个q对象
      如果有人投票 给所有的q对象put数据
      拿数据请求从自己的q对象get数据

    三、websocket介绍

    1.对比

    http协议
      短连接 无状态 基于TCP/UDP协议进行传输数据(TCP/UDP: 传输协议)

    socket
      socket不是传输协议 跟websocket是两个完全不一样的东西 socket是套接字 API接口

    websocket
      H5出的新协议 浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输
      解决轮询问题
      特点:

        0. 本质上是基于TCP的协议
        1. 但在握手阶段是基于HTTP进行握手(因此websocket与Http有一定的交集,但不是同一个东西)
        2. 发送数据加密
        3. 保持连接不断开

    2.不同

    以前客户端想知道服务端的处理进度,要不停地使用 Ajax 进行轮询,让浏览器隔个几秒就向服务器发一次请求,这对服务器压力较大。另外一种轮询就是采用 long poll 的方式,这就跟打电话差不多,没收到消息就一直不挂电话,也就是说,客户端发起连接后,如果没消息,服务端就一直不返回 Response 给客户端,连接阶段一直是阻塞的。

    websocket 是服务器推送技术的一种,最大的特点是服务器可以主动向客户端推送消息,客户端也可以主动向服务器发送消息。解决了轮询造成的同步延迟问题。由于 WebSocket 只需要一次 HTTP 握手,服务端就能一直与客户端保持通信,直到关闭连接,这样就解决了服务器需要反复解析 HTTP 协议,减少了资源的开销。

    只要不是太老的浏览器,一般都支持websocket。

    3.特点

    • 在单个 TCP 连接上进行全双工通讯的协议。
    • 与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
    • 数据格式比较轻量,性能开销小,通信高效。
    • 可以发送文本,也可以发送二进制数据。
    • 没有同源限制,客户端可以与任意服务器通信。
    • 协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。

    四、Web Socket客户端的实现

    1、WebSocket 构造函数

    WebSocket 对象作为一个构造函数,用于新建 WebSocket 实例

    var ws = new WebSocket('ws://localhost:8000');  // 建立连接发送的是GET请求,如果是使用Flask的CBV编程的时候注意咯,要定义get方法

    2、WebSocket.readyState 属性

    属性描述
    WebSocket.readyState

    只读属性 readyState 表示这个连接的状态,可以是以下值:

    • CONNECTING:  0 - 表示连接尚未建立。

    • OPEN:              1 - 表示连接已建立,可以进行通信。

    • CLOSING:         2 - 表示连接正在进行关闭。

    • CLOSED:           3 - 表示连接已经关闭或者连接不能打开。

    WebSocket.bufferedAmount

    只读属性 bufferedAmount 表示还没有发送出去的 UTF-8 文本字节数。

     示例1:

    switch (ws.readyState) {
      case ws.CONNECTING:
        // do something
        break;
      case ws.OPEN:
        // do something
        break;
      case ws.CLOSING:
        // do something
        break;
      case ws.CLOSED:
        // do something
        break;
      default:
        // this never happens
        break;
    }

    示例2:

    // webSocket.bufferedAmount
    实例对象的bufferedAmount属性,表示还有多少字节的二进制数据没有发送出去。它可以用来判断发送是否结束。
    如果为 0 代表全部发送完毕,如果不为 0 代表还有多少字节没有发送出去。
    
    var data = new ArrayBuffer(10000000);
    ws.send(data);
    
    if (ws.bufferedAmount === 0) {
      // 发送完毕
    } else {
      // 发送还没结束
    }

    3、WebSocket事件

    事件事件处理程序描述
    open WebSocket.onopen 连接建立时触发
    message WebSocket.onmessage 客户端接收服务端数据时触发
    error WebSocket.onerror 通信发生错误时触发
    close WebSocket.onclose 连接关闭时触发

    3.1 webSocket.onopen

    实例对象的onopen属性,用于指定连接成功后的回调函数。

    ws.onopen = function () {
      ws.send('Hello Server!');
    }

    如果要指定多个回调函数,可以使用addEventListener方法

    ws.addEventListener('open', function (event) {
      ws.send('Hello Server!');
    });

    3.2 webSocket.onclose

    实例对象的onclose属性,用于指定连接关闭后的回调函数。

    ws.onclose = function(event) {
      var code = event.code;
      var reason = event.reason;
      var wasClean = event.wasClean;
      // handle close event
    };
    
    ws.addEventListener("close", function(event) {
      var code = event.code;
      var reason = event.reason;
      var wasClean = event.wasClean;
      // handle close event
    });

    3.3 webSocket.onmessage

    实例对象的onmessage属性,用于指定收到服务器数据后的回调函数。

    ws.onmessage = function(event) {
      var data = event.data;
      // 处理数据
    };
    
    ws.addEventListener("message", function(event) {
      var data = event.data;
      // 处理数据
    });

     

     

    注意,服务器数据可能是文本,也可能是二进制数据(blob对象或Arraybuffer对象)。

    ws.onmessage = function(event){
      if(typeof event.data === String) {
        console.log("Received data string");
      }
    
      if(event.data instanceof ArrayBuffer){
        var buffer = event.data;
        console.log("Received arraybuffer");
      }
    }

    除了动态判断收到的数据类型,也可以使用binaryType属性,显式指定收到的二进制数据类型。

    // 收到的是 blob 数据
    ws.binaryType = "blob";
    ws.onmessage = function(e) {
      console.log(e.data.size);
    };
    
    // 收到的是 ArrayBuffer 数据
    ws.binaryType = "arraybuffer";
    ws.onmessage = function(e) {
      console.log(e.data.byteLength);
    };

    3.4 webSocket.onerror

    实例对象的onerror属性,用于指定报错时的回调函数。

    socket.onerror = function(event) {
      // handle error event
    };
    
    socket.addEventListener("error", function(event) {
      // handle error event
    });

     

    4、WebSocket 方法

    方法描述
    WebSocket.send()

    使用连接发送数据

    WebSocket.close()

    关闭连接

    4.1 webSocket.send()

    实例对象的send()方法用于向服务器发送数据。

    发送文本的例子

    ws.send('your message');

     

     

    发送 Blob 对象的例子。

    var file = document
      .querySelector('input[type="file"]')
      .files[0];
    ws.send(file);

    发送 ArrayBuffer 对象的例子。

    // Sending canvas ImageData as ArrayBuffer
    var img = canvas_context.getImageData(0, 0, 400, 320);
    var binary = new Uint8Array(img.data.length);
    for (var i = 0; i < img.data.length; i++) {
      binary[i] = img.data[i];
    }
    ws.send(binary.buffer);

    4.2 webSocket.close()

    断开连接

    ws.close();

    五、WebSocket服务端的实现

    1、flask服务端实现的示例

    from flask import Flask, request, render_template, abort
    from geventwebsocket.handler import WebSocketHandler
    from gevent.pywsgi import WSGIServer
    
    
    app = Flask(__name__)
    
    @app.route('/')
    def index():
        return render_template('index.html')
    
    
    @app.route('/foobar')
    def echo():
        if request.environ.get('wsgi.websocket'):
            ws = request.environ['wsgi.websocket']
            if ws is None:
                abort(404)
            else:
                while True:
                    if not  ws.closed:
                        message = ws.receive()
                        ws.send(message)
    if __name__ == '__main__':
        http_server = WSGIServer(('127.0.0.1',5000), app, handler_class=WebSocketHandler)
        http_server.serve_forever()

    2、前后端 websocket 实现的对比

    1. Flask没有websocket,需要安装包  pip install gevent-websocket
    
    
    2. 后端怎样建立一个支持websocket协议连接
    from geventwebsocket.handler import WebSocketHandler
    from gevent.pywsgi import WSGIServer
    
    # 拿到websocket对象
    ws = request.environ.get("wsgi.websocket")
    # 后端发送数据
    ws.send(xxx)
    # 后端接收数据
    ws.receive()
    
    if __name__ == '__main__':
        # app.run()
        # 即支持HTTP 也支持websocket
        http_server = WSGIServer(('0.0.0.0', 5000), app, handler_class=WebSocketHandler)
        http_server.serve_forever()
    
    
    3. 前端怎么发起websocket连接
    let ws = new WebSocket("ws://127.0.0.1:5000")
    # 前端发送数据
    ws.send("xxx")
    # 前端接收数据
    ws.onmessage = function(event){
        # 注意数据数据类型的转换        
        let data = event.data
    }
    
    
    4. 收发消息
    1, 前端发送数据给后端
        前端发送:ws.send('数据')
        后端接收:ws.receive()
        
    
    2, 后端发送数据给前端
        后端发送:ws.send('数据')
        前端接收:ws.onmessage = function(event){
            let data = event.data
        }
        

    3、Demo

    1.后端代码
    from flask import Flask, render_template, request
    from geventwebsocket.handler import WebSocketHandler
    from gevent.pywsgi import WSGIServer
    import json
    
    
    app = Flask(__name__)
    
    USERS = {
        1: {'name': '明凯', 'count': 300},
        2: {'name': '厂长', 'count': 200},
        3: {'name': '7酱', 'count': 600},
    }
    
    WEBSOCKET_LIST = []
    
    
    @app.route('/')
    def index():
        return render_template('websocket.html', users=USERS)
    
    
    @app.route('/vote')
    def vote():
        # 处理websocket
        # 判断是什么类型的请求,HTTP还是websocket
        # 看能否获取得到websocket的对象
        ws = request.environ.get("wsgi.websocket")
        if not ws:
            return "这是HTTP协议的请求"
        # 把所有用户的ws对象存到一个列表
        WEBSOCKET_LIST.append(ws)
        while True:
            # 获取前端传过来的uid,给打野票数 +1
            uid = ws.receive()
            # 如果前端主动断开连接
            # 那么后端也关闭与前端的连接
            if not uid:
                WEBSOCKET_LIST.remove(ws)
                ws.close()
                break
            uid = int(uid)
            USERS[uid]["count"] += 1
            data = {
                "uid": uid,
                "name": USERS[uid]["name"],
                "count": USERS[uid]["count"]
            }
            for ws in WEBSOCKET_LIST:
                # 给前端发送新的数据
                ws.send(json.dumps(data))
    
    
    if __name__ == '__main__':
        # app.run()
        # 这样启服务的意思是:即支持HTTP协议,也支持websocket协议
        http_server = WSGIServer(('127.0.0.1', 5000), app, handler_class=WebSocketHandler)
        http_server.serve_forever()
    2.前端代码
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>投票系统</title>
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css">
        <script src="https://cdn.bootcss.com/axios/0.19.0-beta.1/axios.js"></script>
    </head>
    <style>
        .my-li {
            list-style: none;
            margin-bottom: 20px;
            font-size: 18px;
        }
    </style>
    <body>
    <div class="container">
        <div class="row">
            <div class="col-md-6 col-md-offset-3">
                <h1>LPL第一打野投票</h1>
                {% for (uid, user) in users.items() %}
                    <button class="btn btn-success" onclick="vote({{ uid }})">投票</button>
                    <li class="list-group-item-info my-li" id="{{ uid }}">{{ user.name }}目前的票数是: {{ user.count }}</li>
                {% endfor %}
            </div>
        </div>
    </div>
    
    <script>
        // 向后端发送一个websocket连接请求
        let ws = new WebSocket('ws://127.0.0.1:5000/vote');
        function vote(uid) {
            // 向后端发数据
            ws.send(uid)
        }
        ws.onmessage = function (event) {
            let data = JSON.parse(event.data);
            let liEle = document.getElementById(data.uid);
            liEle.innerText = `${data.name}目前的票数是: ${data.count}`
        }
    </script>
    
    </body>
    </html>
  • 相关阅读:
    状压dp大总结1 [洛谷]
    集训日记(暑期第二周)【真正的集训,真正的考试】
    集训日记(暑期第一周)【6.22始】
    集训模拟赛3【啥也不会的一天】
    P2194 HXY烧情侣【Tarjan】
    6.28-集训模拟赛2【水爆但有进步的半天】
    Linux基础管道管理
    Linux基础进程管理优先级
    awk文本处理
    sed流编辑器
  • 原文地址:https://www.cnblogs.com/Zzbj/p/10222887.html
Copyright © 2011-2022 走看看