數據實時更新實現-輪詢-長輪詢-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 是同層級的協議

讓客戶端與服務端建立長連接

協議規定

  1. 連接的時候需要握手(是基於 HTTP 來發起握手的)
  2. 發送的數據需要加密(根據 websocket 協議去發送數據)
  3. 保持鏈接不斷開

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>

... 其他代碼


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM