数据实时更新实现-轮询-长轮询-websocket
前言
本篇博客旨在描述三种实现方式,在具体项目中如何运用可以去搜搜其他文章
显然相比其他两种方式, websocket 将会是以后的趋势
轮询
实现原理:每隔一段时间发一次请求来获取最新数据
- 定时器发送 ajax 请求,DOM 操作更新页面数据
缺点
-
对服务器造成的压力比较大,耗费资源
请求太多太频繁,如果是访问量比较大的网站,就会造成压力了
-
会有延迟,数据的实时性不高
并不是数据刚更新就能拿到并更新的,需要请求正好能拿到数据
-
数据看起来可能会有紊乱,同一时间你看到的数据和别人的不一样
页面打开开始计算的请求定时器开始时间不一样,对方拿到的可能是刚刚刷新的数据,而你还没去获取最新数据
代码实现
实时性很低,体验很不好
test.py
from flask import Flask, render_template, request, jsonify
app = Flask(__name__)
USERS = { # 模拟数据
1: {"name": "github", "count": 0},
2: {"name": "gitee", "count": 0},
3: {"name": "gitlab", "count": 0},
}
@app.route("/")
def index():
return render_template("index.html", users=USERS)
@app.route("/vote", methods=["POST"])
def vote():
uid = request.json.get("uid")
USERS[uid]["count"] += 1
return "投票成功"
@app.route("/get_vote")
def get_vote():
return jsonify(USERS)
if __name__ == '__main__':
app.run()
templates/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://cdn.bootcss.com/axios/0.19.0-beta.1/axios.js"></script>
</head>
<body>
<h1>投票系统</h1>
<ul>
{% for key, value in users.items()%}
<li id="{{key}}" onclick="vote({{key}})">{{value.name}} ({{value.count}})</li>
{% endfor %}
</ul>
<script>
function vote(uid) {
axios.request({
url: "/vote",
method: "POST",
data: {
"uid": uid
}
}).then(function (res) {
console.log(res.data)
})
}
function get_vote() {
axios.request({
url: "/get_vote",
method: "GET"
}).then(function (res) {
console.log(res)
for(let key in res.data){
let liEle = document.getElementById(key);
let username = res.data[key]["name"]
let count = res.data[key]["count"]
liEle.innerText = `${username} (${count})`
}
})
}
window.onload = function () {
setInterval(get_vote, 2000)
}
</script>
</body>
</html>
长轮询
实现原理:请求进来,有数据就返回,没有就夯住(先不把请求响应给前端),直到有数据或者超时再返回(然后立即再发起一个请求过来)
在前端的表现就是请求处于 pending 状态
如网页版微信就是利用长轮询实现的:登录微信后会有一个请求发到后端,一直等待(请求处于 pending 状态)后端返回数据,拿到后端数据之后又立马再发一个请求同样等待数据,就这样不停地等着拿数据(后端可能用的是 Queue,q.get()
取数据时没有数据就会夯在那里,等有数据了就接着执行后面的代码,响应给前端),如此往复也就能实现数据的实时获取了
这样做其实不太好,但网页版微信这么做是为了做兼容,ie 还不能很好地兼容 H5 的特性
好处
- 可以降低延迟(设置一个超时时间,在这段时间内,一有数据就返回)
减少了一定的请求次数,把单纯依靠请求来获取数据变成等待数据主动返回、超时返回相结合(返回了立即再次发起请求等着获取最新数据)
缺点
- 对服务器造成的压力依旧比较大,耗费资源
代码实现
这个实现方式一般觉察不出什么,相对延迟较低
思路:利用 queue 对象实现请求拿到数据了再响应,每个请求都会生成一个 q 对象,如果有人投票,给所有的 q 对象都 put 一份最新投票数据,让其拿到(都是从自己的 q 对象里拿的)后再去页面更新,然后再发起一个请求等待最新数据
test.py
from flask import Flask, render_template, request, jsonify, session
import queue
import uuid
app = Flask(__name__)
app.secret_key = "lajdgia"
USERS = { # 模拟数据
1: {"name": "github", "count": 0},
2: {"name": "gitee", "count": 0},
3: {"name": "gitlab", "count": 0},
}
# 为每个用户建立一个 q 对象,以用户的 uuid 为 key 值为q对象
# 有数据更新时要往这个 q 对象列表中所有 q 对象中放入最新数据,然后 q.get() 即可拿到最新数据往下执行
Q_DICT = {}
@app.route("/")
def index():
# 每次用户访问首页都视作登录了
user_uuid = str(uuid.uuid4())
session["user_uuid"] = user_uuid
# 为改用户创建一个 q 对象,把用户 uuid 作为键,放入 q 对象列表中
Q_DICT[user_uuid] = queue.Queue()
return render_template("index.html", users=USERS)
@app.route("/vote", methods=["POST"])
def vote():
# 投票 循环q对象的dict 给每个q对象返回值
uid = request.json.get("uid")
USERS[uid]["count"] += 1
for q in Q_DICT.values():
# 票数更新了,往所有 q 对象中放入新票数信息,q.get() 处即可拿到值,返回给前端
q.put(USERS)
return "投票成功"
@app.route("/get_vote", methods=["POST", "GET"])
def get_vote():
# 获取投票结果,去自己的 q 对象里取值,没有值 q.get() 会夯住,代码不往下执行
# 直到有值或者超时返回才会往下执行
user_uuid = session.get("user_uuid")
q = Q_DICT[user_uuid]
try:
users = q.get(timeout=30) # 30秒超时,超时了就报错
except queue.Empty:
users = "" # 这是超时的响应,前端需过滤这个
return jsonify(users)
if __name__ == '__main__':
app.run()
templates/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<!-- 使用 axios 来发起请求 -->
<script src="https://cdn.bootcss.com/axios/0.19.0-beta.1/axios.js"></script>
</head>
<body>
<h1>投票系统</h1>
<ul>
{% for key, value in users.items()%}
<li id="{{key}}" onclick="vote({{key}})">{{value.name}} ({{value.count}})</li>
{% endfor %}
</ul>
<script>
function vote(uid) {
axios.request({
url: "/vote",
method: "POST",
data: {
"uid": uid
}
}).then(function (res) {
console.log(res.data)
})
}
// 向后台获取票数接口发送请求,等待拿回最新数据
function get_votes() {
axios.request({
url: "/get_vote",
method: "POST"
}).then(function (res) {
console.log(res);
// 过滤后台 get 超时传过来的信息
if (res.data != "") {
// 拿到数据就更新页面,让用户看到最新信息
for (let key in res.data) {
let liEle = document.getElementById(key);
let username = res.data[key]["name"]
let count = res.data[key]["count"]
liEle.innerText = `${username} (${count})`
}
}
// 立即再发起一个请求,等着获取最新数据
get_votes()
})
}
// 页面加载完成自动触发 get_votes 函数
window.onload = function () {
get_votes()
}
</script>
</body>
</html>
websocket
websocket 是 H5 出的一个新协议( 请求格式:ws://xxxxx
) ,也是基于 TCP/UDP 传输的,和 HTTP 是同层级的协议
让客户端与服务端建立长连接
协议规定
- 连接的时候需要握手(是基于 HTTP 来发起握手的)
- 发送的数据需要加密(根据 websocket 协议去发送数据)
- 保持链接不断开
django 实现(dwebsocket
)
参考博客:Django实现websocket完成实时通讯、聊天室、在线客服等
- 首先,你需要先安装 dwebsocket:
pip3 install dwebsocket
- 下面代码 2019-12-18 21:41 亲测可用,根据自己的业务需求改写即可
配置 settings.py
INSTALLED_APPS = [
.....
.....
'dwebsocket',
]
MIDDLEWARE_CLASSES = [
......
......
# 'dwebsocket.middleware.WebSocketMiddleware' # 为所有的URL提供websocket,如果只是单独的视图需要可以不选 ---> 填了这个会报错,也不知道为什么
]
WEBSOCKET_ACCEPT_ALL=True # 可以允许每一个单独的视图实用websockets
app01/views.py
视图文件
from django.shortcuts import render,HttpResponse
# Create your views here.
def login(request):
return render(request,'login.html')
from dwebsocket.decorators import accept_websocket
@accept_websocket
def path(request):
if request.is_websocket():
print(1)
request.websocket.send('下载完成'.encode('utf-8'))
dwebsocket_test_demo/urls.py
路由文件
from django.conf.urls import url
from django.contrib import admin
from app01 import views
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^login/', views.login),
url(r'^path/', views.path),
]
login.html
前端页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<button onclick="WebSocketTest()"> 点我测试</button>
</body>
<script>
function WebSocketTest() {
if ("WebSocket" in window) {
alert("您的浏览器支持 WebSocket!");
// 1.打开一个 web socket,与后台建立连接
ws = new WebSocket("ws://127.0.0.1:8000/path/");
// 2.web socket 建立好连接会自动触发这个函数
ws.onopen = function () {
// Web Socket 已连接上,使用 send() 方法发送数据
ws.send("发送数据");
alert("数据发送中...");
};
// 3.可写自己的函数,触发事件等,主动向服务端推送消息
function myfunc(uid) {
ws.send("mymessage.");
}
// 4.一收到服务端传来的消息就会自动触发这个
ws.onmessage = function (evt) {
var received_msg = evt.data;
alert("数据已接收...");
alert("数据:" + received_msg)
};
// 5.web socket 断开连接会自动触发这个函数
ws.onclose = function () {
// 关闭 websocket
alert("连接已关闭...");
};
} else {
// 浏览器不支持 WebSocket
alert("您的浏览器不支持 WebSocket!");
}
}
</script>
</html>
其他用法
# dwebsocket有两种装饰器:require_websocket、accept_websocekt
# 使用 require_websocket 装饰器会导致视图函数无法接收导致正常的 http 请求,一般情况使用 accept_websocket 方式就可以了
#
# dwebsocket 的一些内置方法:
# request.is_websocket():判断请求是否是 websocket 方式,是返回 true,否则返回 false
# request.websocket:当请求为 websocket 的时候,会在 request 中增加一个 websocket 属性,
# WebSocket.wait():返回客户端发送的一条消息,没有收到消息则会导致阻塞
# WebSocket.read() 和 wait 一样:可以接受返回的消息,只是这种是非阻塞的,没有消息返回 None
# WebSocket.count_messages():返回消息的数量
# WebSocket.has_messages():返回是否有新的消息过来
# WebSocket.send(message):向客户端发送消息,message 为 byte 类型
flask 实现(gevent-websocket
)
首先,你需要先安装 gevent-websocket:
pip install gevent-websocket
备注:项目启动没报错就代表已经启动了(别以为是卡住了)
test.py
后台代码
from flask import Flask, request, render_template
from geventwebsocket.handler import WebSocketHandler
from gevent.pywsgi import WSGIServer
import json
app = Flask(__name__)
USERS = { # 模拟数据
1: {"name": "github", "count": 0},
2: {"name": "gitee", "count": 0},
3: {"name": "gitlab", "count": 0},
}
@app.route("/")
def index():
return render_template("index.html", users=USERS)
WEBSOCKET_LIST = []
# ws://127.0.0.1:5000/vote 这个路由会匹配到这个视图函数,然后执行
@app.route("/vote")
def vote():
# 这里可以根据 request.environ.get("wsgi.websocket") 是否有值判断其是不是一个 websocket 请求
# 如果是 websocket 请求,在 websocket 源码中就帮我们处理并建立好连接了,后面只是拿着 websocket 对象进行收发消息,或者关闭资源
ws = request.environ.get("wsgi.websocket")
# print(ws)
# HTTP 请求这里会打印 None,因为 .get(...) 没取到值
# 如果是 websocket 请求,会打印这样一个结果 <geventwebsocket.websocket.WebSocket object at 0x0000023412FFAE80>
if not ws:
return "这是 HTTP 协议的请求"
WEBSOCKET_LIST.append(ws) # 将该 websocket 对象加入到 websocket 通信列表中,方便后续统一推送消息
# 死循环接收消息(websocket 之间的通信消息不止一条,所以得不断地保持监听)
while True:
uid = ws.receive() # 在这里 等待 接收前端发来的投票信息(投给谁),等到了,代码接着往下走
if not uid: # 如果前端断开 websocket 连接,这里会接收到一个 None(即代表断开 websocket 链接)
WEBSOCKET_LIST.remove(ws)
ws.close() # 关闭该 websocket 连接
break # 跳出死循环,结束 websocket 通信
uid = int(uid)
USERS[uid]["count"] += 1
name = USERS[uid]["name"]
new_count = USERS[uid]["count"]
# 更新数据,发给所有建立了 websocket 连接的前端(遍历建立 websocket 连接了的列表)
for client in WEBSOCKET_LIST:
# 接收到了投票数据,更新完票数,代码走到了这里
# 由服务端向客户端推送最新投票数据
client.send(json.dumps({"uid": uid, "name": name, "count": new_count}))
# 消息到达前端会触发前端 ws.onmessage = function (event) {...} 绑定的函数,由前端来修改页面 DOM 完成数据更新
if __name__ == '__main__':
# 这样起服务才能接收到 websocket 请求,这个写法既支持 websocket 协议的请求,也能支持 HTTP 协议的请求 ********
http_server = WSGIServer(('127.0.0.1', 5000), app, handler_class=WebSocketHandler)
http_server.serve_forever()
templates/index.html
前端代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<!-- 使用 axios 来发起请求 -->
<script src="https://cdn.bootcss.com/axios/0.19.0-beta.1/axios.js"></script>
<style type="text/css">
h1{
margin-left: 30px;
}
</style>
</head>
<body>
<h1>投票系统</h1>
<ol>
<!-- 将后台获取到的数据渲染出来 -->
{% for (key, value) in users.items() %}
<li onclick="vote(`{{key}}`)" id="{{key}}">{{value.name}} ({{value.count}})</li>
{% endfor%}
</ol>
<p>点击列表中的文字即可投票,会自动向发送消息,后端接收到后向前端推送,前端接收到后执行 js 函数,更新页面信息</p>
<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>
提炼
在前端执行
let ws = new WebSocket('ws://127.0.0.1:5000/vote');
之后,后端就可以直接给客户端发消息了 发过去会自动触发前端的ws.onmessage = function (event) {...}
(我测试过,是可以的,别看着代码误以为只能前端来消息了,后端再处理) --> 2019-12-19 17:13
后端核心代码
@app.route("/vote")
def vote():
# 判断其是不是一个 websocket 请求,如果是 websocket 请求,再进行下面的处理
ws = request.environ.get("wsgi.websocket")
if ws:
# 业务逻辑
# 接收客户端发来的消息 并 判断前端是否关闭 websocket 连接
uid = ws.receive() # 在这里 等待 接收前端发来的消息,等到了,代码接着往下走
if not uid: # 如果前端断开 websocket 连接,这里会接收到一个 None(即代表断开 websocket 链接)
# 业务逻辑
ws.close() # 关闭该 websocket 连接
# 业务逻辑
# 向客户端发送消息
client.send(json.dumps({"description": "要发送的消息。。。"}))
# 业务逻辑
if __name__ == '__main__':
# 开启服务,必须这样写(来支持 websocket 请求)
http_server = WSGIServer(('127.0.0.1', 5000), app, handler_class=WebSocketHandler)
http_server.serve_forever()
前端核心代码
... 其他代码
<script>
// 建立连接
let ws = new WebSocket('ws://127.0.0.1:5000/vote');
// 主动发送消息给后端(自己把这个函数与事件绑定起来,触发它)
function vote(uid) {
// 业务逻辑
ws.send(uid); // 将数据 uid 发送给后端
// 业务逻辑
}
// 等待接收服务端的信息,收到信息会自动触发其绑定的函数
ws.onmessage = function (event) {
// 业务逻辑
let data = JSON.parse(event.data); // 这里将拿到后端传过来的 json 格式数据,转化一下(具体后端传来什么格式自己处理)
// 业务逻辑
}
</script>
... 其他代码
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构