前言
本篇博客旨在描述三種實現方式,在具體項目中如何運用可以去搜搜其他文章
顯然相比其他兩種方式, 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>
... 其他代碼