一、轮询
在一些需要进行实时查询的场景下应用
比如投票系统:
大家一起在一个页面上投票
在不刷新页面的情况下,实时查看投票结果
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 表示这个连接的状态,可以是以下值:
|
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>