轮询、长轮询和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对象实现请求hold住
  每个请求进来都要生成一个q对象
  如果有人投票 给所有的q对象put数据
  拿数据请求从自己的q对象get数据

 

三、websocket介绍

1.对比

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

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

websocket
  是HTML5下一种新的协议,是基于TCP的应用层协议,只需要一次连接,便可以实现持久性的全双工通信,客户端和服务端可以相互主动发送消息。

       客户端进行监听,并对响应的消息处理显示。


  解决轮询问题
  特点:

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

 

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>

 

posted @ 2019-01-04 22:35  我用python写Bug  阅读(1419)  评论(0编辑  收藏  举报