目录

  • 前言
  • 轮询
  • 代码实现
  • 长轮询
  • 代码实现
  • websocket
  • 协议规定
  • django 实现(dwebsocket)
  • 其他用法
  • flask 实现(gevent-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


  • 首先,你需要先安装 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

备注:项目启动没报错就代表已经启动了(别以为是卡住了)

实时数据更新页面 java源码_ios

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>

... 其他代码