前言

本人于年初基于Flask+echarts+mysql+ajax实现了自动刷新,但由于ajax轮询的弊端(请求必须由客户端向服务端发起,然后服务端进行响应),想看ajax实现的朋友可以看我写的这篇Flask+echarts+mysql+自动刷新。现改用双向传输、推送消息方面能够做到灵活、简便、高效实现方案,即数据库收到数据立刻向客户端发送数据,无需客户端先向数据库发送请求。

一、环境准备

网上已经有许多教程,但由于websocket版本匹配和引入库版本等问题,大部分截至我发文阶段都无法直接实现,经过本人测试更改,以下为实现代码,另附上我的相关版本,以免读者走弯路,如需要查看其他版本的匹配方案,见官方文档 以下为官方包版本匹配说明:

flask 动态执行python脚本 flask 页面动态刷新_echarts


以下为本人操作案例版本:

flask 动态执行python脚本 flask 页面动态刷新_flask_02

二、Flask+websocket

服务端app.py:

from flask import Flask, render_template
from flask_socketio import SocketIO, emit
from threading import Lock
import random

async_mode = None
app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret!'
socketio = SocketIO(app)
thread = None
thread_lock = Lock()


@app.route('/')
def index():
    return render_template('test.html')


@socketio.on('connect', namespace='/test_conn')
def test_connect():
    global thread
    with thread_lock:
        if thread is None:
            thread = socketio.start_background_task(target=background_thread)


def background_thread():
    while True:
        socketio.sleep(5)
        t = random.randint(1, 100)
        socketio.emit('server_response',
                      {'data': t}, namespace='/test_conn')


if __name__ == '__main__':
    socketio.run(app, debug=True)

客户端test.html:
注意以下引入版本,如果报错基本都是版本不匹配的问题。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title></title>
    <script type="text/javascript" src="//cdn.bootcss.com/jquery/3.1.1/jquery.min.js"></script>
    <script type="text/javascript" src="//cdn.bootcss.com/socket.io/1.5.1/socket.io.min.js"></script>
</head>
<body>
<h2 id="t"></h2>
<script type="text/javascript">
    $(document).ready(function() {
        namespace = '/test_conn';
        var socket = io.connect(location.protocol + '//' + document.domain + ':' + location.port + namespace);
        socket.on('server_response', function(res) {
            console.log(res.data)
            var t = res.data;
            $("#t").text(t);
        });

    });
</script>
</body>
</html>

实现结果:

flask 动态执行python脚本 flask 页面动态刷新_数据_03


附上参考文献

三、Flask+echarts+websocket

服务端:

# encoding:utf-8
# !/usr/bin/env python
import psutil
import time
from threading import Lock
from flask import Flask, render_template
from flask_socketio import SocketIO

async_mode = None
app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret!'
socketio = SocketIO(app, async_mode=async_mode)
thread = None
thread_lock = Lock()


# 后台线程 产生数据,即刻推送至前端
def background_thread():
    count = 0
    while True:
        socketio.sleep(5)
        count += 1
        t = time.strftime('%M:%S', time.localtime())
        # 获取系统时间(只取分:秒)
        cpus = psutil.cpu_percent(interval=None, percpu=True)
        # 获取系统cpu使用率 non-blocking
        socketio.emit('server_response',
                      {'data': [t, cpus], 'count': count},
                      namespace='/test')
        # 注意:这里不需要客户端连接的上下文,默认 broadcast = True


@app.route('/')
def index():
    return render_template('test.html', async_mode=socketio.async_mode)


@socketio.on('connect', namespace='/test')
def test_connect():
    global thread
    with thread_lock:
        if thread is None:
            thread = socketio.start_background_task(target=background_thread)


if __name__ == '__main__':
    socketio.run(app, debug=True)

客户端:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title></title>
    <script type="text/javascript" src="//cdn.bootcss.com/jquery/3.1.1/jquery.min.js"></script>
    <script type="text/javascript" src="//cdn.bootcss.com/socket.io/1.5.1/socket.io.min.js"></script>
    <!-- ECharts  引入 -->
    <script src="https://cdn.staticfile.org/echarts/4.3.0/echarts.min.js"></script>
</head>
<body>
<div id="main" style="height:500px;border:1px solid #ccc;padding:10px;"></div>

    <script type="text/javascript">

    var myChart = echarts.init(document.getElementById('main'));

    myChart.setOption({
        title: {
            text: '系统监控走势图'
        },
        tooltip: {},
        legend: {
            data:['cpu']
        },
        xAxis: {
            data: []
        },
        yAxis: {},
        series: [{
            name: 'cpu',
            type: 'line',
            data: []
        }]
    });


    var time = ["","","","","","","","","",""],
        cpu = [0,0,0,0,0,0,0,0,0,0]


    //准备好统一的 callback 函数
    var update_mychart = function (res) {
    //res是json格式的response对象

        // 隐藏加载动画
        myChart.hideLoading();

        // 准备数据
        time.push(res.data[0]);
        cpu.push(parseFloat(res.data[1]));
        if (time.length >= 10){
            time.shift();
            cpu.shift();
        }

        // 填入数据
        myChart.setOption({
            xAxis: {
                data: time
            },
            series: [{
                name: 'cpu', // 根据名字对应到相应的系列
                data: cpu
            }]
        });

    };

    // 首次显示加载动画
    myChart.showLoading();


    // 建立socket连接,等待服务器“推送”数据,用回调函数更新图表
    $(document).ready(function() {
        namespace = '/test';
        var socket = io.connect(location.protocol + '//' + document.domain + ':' + location.port + namespace);

        socket.on('server_response', function(res) {
            update_mychart(res);
        });

    });

    </script>

</body>
</html>

效果:

flask 动态执行python脚本 flask 页面动态刷新_数据_04


参考文献

四、添加数据库测试

思路和第三节差不多,写一个线程不断往数据库中插入数据,客户端显示横轴为数据库插入时间,纵轴为相应数据。
本人参考这几篇文章Flask 操作Mysql数据库Flask-SQLAlchemy详解查询结果集转json

  1. 创建数据库
    这一步可以直接从数据库可视化软件navicate、mysql命令行或者是flask框架的拓展包来进行,为了方便后续操作,在这里使用flask框架的数据库拓展包Flask-SQLAlchemy,创建测试类数据库代码如下:
    db_create.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
import pymysql

pymysql.install_as_MySQLdb()

app = Flask(__name__)


class Config(object):
    """配置参数"""
    # 设置连接数据库的URL
    user = 'root'
    password = 自己数据库的密码
    database = 'test'
    SQLALCHEMY_DATABASE_URI = 'mysql://%s:%s@127.0.0.1:3306/%s' % (user, password, database)

    # 设置sqlalchemy自动更跟踪数据库
    SQLALCHEMY_TRACK_MODIFICATIONS = True

    # 查询时会显示原始SQL语句
    SQLALCHEMY_ECHO = True

    # 禁止自动提交数据处理
    SQLALCHEMY_COMMIT_ON_TEARDOWN = False


# 读取配置
app.config.from_object(Config)

# 创建数据库sqlalchemy工具对象
db = SQLAlchemy(app)


class Test(db.Model):
    # 定义表名
    __tablename__ = 'sea_data'
    # 定义字段
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)  # id 主键、自增
    record_t = db.Column(db.DateTime, unique=True)  # record_t 上传时间
    temperature = db.Column(db.Float)  # 气温数值


if __name__ == '__main__':
    # 删除所有表
    db.drop_all()
    # 创建所有表
    db.create_all()

执行该脚本python db_create.py,看到表创建完成

flask 动态执行python脚本 flask 页面动态刷新_flask 动态执行python脚本_05

  1. 定时插入数据
    参考多线程定时器,以下为对db_create.py的补充:
def insert():
    print("定时器启动了")
    print(threading.current_thread())  # 查看当前线程
    record_t = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
    temperature = round(random.uniform(0, 40), 2)  # 产生一个0-40之间的数字,保留两位小数
    print('hello')
    ins = Test(record_t=record_t, temperature=temperature)
    db.session.add(ins)
    db.session.commit()
    print('插入成功!')
    timer = threading.Timer(5, insert)  # 在run函数结束之前我再开启一个定时器
    timer.start()
if __name__ == '__main__':
    # 删除所有表
    db.drop_all()
    # 创建所有表
    db.create_all()
    t1 = threading.Timer(5, function=insert)  # 过5s之后我执行后面的一个函数,开启一个线程
    t1.start()

    # 设置一个多线程
    # while True:
    #     time.sleep(10)  # 延时10s
    #     print('主线程')

运行python db_create.py结果,5秒插入一次随机数据:

flask 动态执行python脚本 flask 页面动态刷新_echarts_06

  1. 查询数据
    想到两种方式,一种是通过查询数据库取最近十条记录传给前端渲染显示;另一种是只查询最近的一条,通过前端将最前一条数据挤出去显示。
    在这里我们使用第二种方法,就查询最近1条显示,前端会不断地将以往的数据挤出去并填充。
def query():
    # 查询最近一条数据
    # 只有最后加.all()才能读到实例,order_by和limit是条件查询
    new = db.session.query(Test).order_by(Test.id.desc()).limit(1).all()
    print(new)
    print(
        class_to_dict(new))  # [{'temperature': 23.18, 'id': 5, 'record_t': datetime.datetime(2022, 10, 8, 10, 41, 35)}]


# 查询结果转为字典
def class_to_dict(obj):
    is_list = obj.__class__ == [].__class__
    is_set = obj.__class__ == set().__class__
    if is_list or is_set:
        obj_arr = []
        for o in obj:
            dict = {}
            a = o.__dict__
            if "_sa_instance_state" in a:
                del a['_sa_instance_state']
            dict.update(a)
            obj_arr.append(dict)
        return obj_arr
    else:
        dict = {}
        a = obj.__dict__
        if "_sa_instance_state" in a:
            del a['_sa_instance_state']
        dict.update(a)
        return dict
  1. websocket查询数据
    在从数据库获取最新数据的时候出现了一个问题,插入的数据无法被获取,收到的数据都很旧,经过查询资料,这篇文章SQLAlchemy为了加快查速度,因而存在"缓存"机制解释了为什么,需要先清空缓存,再查询:
#清空缓存
    db_session.commit()

在这里实现源码,注释我都写在源码里了,在这里不过多做赘述,需要的朋友直接copy,这是demo结构:

flask 动态执行python脚本 flask 页面动态刷新_flask_07

后端 app_dbchart.py:

# encoding:utf-8
# !/usr/bin/env python
import psutil
import time
from threading import Lock
from flask import Flask, render_template
from flask_socketio import SocketIO
import threading

from db_create import insert, query

async_mode = None
app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret!'
socketio = SocketIO(app, async_mode=async_mode)
thread = None
thread_lock = Lock()


# 后台线程 产生数据,即刻推送至前端
def background_thread():
    count = 0

    while True:
        socketio.sleep(5)
        count += 1
        #  目前问题,数据库无法读取到最新数据 query方法
        print(query())
        temperature = query()['temperature']
        record_t = query()['record_t']
        # t = time.strftime('%Y%M:%S', time.localtime())
        # # 获取系统时间(只取分:秒)
        # cpus = psutil.cpu_percent(interval=None, percpu=True)
        # # 获取系统cpu使用率 non-blocking
        socketio.emit('server_response',
                      {'data': [record_t, temperature], 'count': count},
                      namespace='/test')
        # 注意:这里不需要客户端连接的上下文,默认 broadcast = True


@app.route('/')
def index():
    return render_template('test_dbchart.html', async_mode=socketio.async_mode)


@socketio.on('connect', namespace='/test')
def test_connect():
    global thread
    with thread_lock:
        if thread is None:
            thread = socketio.start_background_task(target=background_thread)


if __name__ == '__main__':
    # 定时插入后来个定时画图
    socketio.run(app, debug=True)

db_create.py:

import json
import random
import time

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
import pymysql
import datetime
import threading

pymysql.install_as_MySQLdb()

app = Flask(__name__)

class Config(object):
	"""配置参数"""
	# 设置连接数据库的URL
	user = 'root'
	password = 自己的密码
	database = 'test'
	SQLALCHEMY_DATABASE_URI = 'mysql://%s:%s@127.0.0.1:3306/%s' % (user, password, database)
	# 设置sqlalchemy自动更跟踪数据库
	SQLALCHEMY_TRACK_MODIFICATIONS = True
	# 查询时会显示原始SQL语句
	SQLALCHEMY_ECHO = True
	# 禁止自动提交数据处理
	SQLALCHEMY_COMMIT_ON_TEARDOWN = False
	ENV = 'development'
	DEBUG = True

# 读取配置
app.config.from_object(Config)

# 创建数据库sqlalchemy工具对象
db = SQLAlchemy(app)


class Test(db.Model):
    # 定义表名
    __tablename__ = 'sea_data'
    # 定义字段
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)  # id 主键、自增
    record_t = db.Column(db.DateTime, unique=True)  # record_t 上传时间
    temperature = db.Column(db.Float)  # 气温数值


def insert():
    print("定时器启动了")
    print(threading.current_thread())  # 查看当前线程
    record_t = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
    temperature = round(random.uniform(0, 40), 2)  # 产生一个0-40之间的数字,保留两位小数
    ins = Test(record_t=record_t, temperature=temperature)
    db.session.add(ins)
    db.session.commit()
    print('插入成功!')
    timer = threading.Timer(5, insert)  # 在insert函数结束之前我再开启一个定时器
    timer.start()


def create():
    # 创建所有表
    db.create_all()


def drop():
    # 删除所有表
    db.drop_all()


def query():
    # 清空缓存
    db.session.commit()
    # 查询最近一条数据
    # 只有最后加.all()才能读到实例,order_by和limit是条件查询
    new = db.session.query(Test).order_by(Test.id.desc()).limit(1).all()
    # [{'temperature': 23.18, 'id': 5, 'record_t': datetime.datetime(2022, 10, 8, 10, 41, 35)}]  list
    result = class_to_dict(new)
    # 取的时间json.dumps无法对字典中的datetime时间格式数据进行转化。因此需要添加特殊日期格式转化
    result[0]['record_t'] = json.dumps(result[0]['record_t'], cls=DateEncoder)
    # print(result[0])  # {'temperature': 23.18, 'id': 5, 'record_t': '"2022-10-08 10:41:35"'}
    return result[0]  # {'temperature': 23.18, 'id': 5, 'record_t': '"2022-10-08 10:41:35"'}
    # timer = threading.Timer(5, query)  # 在insert函数结束之前我再开启一个定时器
    # timer.start()
    # tem = result[0]['temperature']  # 23.18
    # return result[0]  # 应当返回这个字典,再按需取值


# 查询结果转为字典
def class_to_dict(obj):
    is_list = obj.__class__ == [].__class__
    is_set = obj.__class__ == set().__class__
    if is_list or is_set:
        obj_arr = []
        for o in obj:
            dict = {}
            a = o.__dict__
            if "_sa_instance_state" in a:
                del a['_sa_instance_state']
            dict.update(a)
            obj_arr.append(dict)
        return obj_arr
    else:
        dict = {}
        a = obj.__dict__
        if "_sa_instance_state" in a:
            del a['_sa_instance_state']
        dict.update(a)
        return dict


# 将json时间格式化
class DateEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, datetime.datetime):
            return obj.strftime("%Y-%m-%d %H:%M:%S")
        else:
            return json.JSONEncoder.default(self, obj)


if __name__ == '__main__':
    # print(query())
    # 创建一个定时器,在程序运行在之后我开启一个insert函数
    t1 = threading.Timer(5, function=insert)  # 第一个参数是时间,例:过5s之后我执行后面的一个函数,开启一个线程
    t1.start()
    # print(query())
    # t2 = threading.Timer(5, function=query)  # 第一个参数是时间,例:过5s之后我执行后面的一个函数,开启一个线程
    # t2.start()

    # # 设置一个多线程
    # while True:
    #     time.sleep(10)  # 延时10s
    #     print('主线程')

前端test_dbchart.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title></title>
    <script type="text/javascript" src="//cdn.bootcss.com/jquery/3.1.1/jquery.min.js"></script>
    <script type="text/javascript" src="//cdn.bootcss.com/socket.io/1.5.1/socket.io.min.js"></script>
    <!-- ECharts  引入 -->
    <script src="https://cdn.staticfile.org/echarts/4.3.0/echarts.min.js"></script>
</head>
<body>
<div id="main" style="height:500px;border:1px solid #ccc;padding:10px;"></div>

    <script type="text/javascript">

    var myChart = echarts.init(document.getElementById('main'));

    myChart.setOption({
        title: {
            text: '系统监控走势图'
        },
        tooltip: {},
        legend: {
            data:['temperature']
        },
        xAxis: {
            data: []
        },
        yAxis: {},
        series: [{
            name: 'temperature',
            type: 'line',
            data: []
        }]
    });


    var record_t = ["","","","","","","","","",""],
        temperature = [0,0,0,0,0,0,0,0,0,0]


    //准备好统一的 callback 函数
    var update_mychart = function (res) {
    //res是json格式的response对象

        // 隐藏加载动画
        myChart.hideLoading();

        // 准备数据
        record_t.push(res.data[0]);
        temperature.push(parseFloat(res.data[1]));
        console.log(temperature)
        if (record_t.length >= 10){
            record_t.shift();
            temperature.shift();
        }

        // 填入数据
        myChart.setOption({
            xAxis: {
                data: record_t
            },
            series: [{
                name: 'temperature', // 根据名字对应到相应的系列
                data: temperature
            }]
        });

    };

    // 首次显示加载动画
    myChart.showLoading();


    // 建立socket连接,等待服务器“推送”数据,用回调函数更新图表
    $(document).ready(function() {
        namespace = '/test';
        var socket = io.connect(location.protocol + '//' + document.domain + ':' + location.port + namespace);

        socket.on('server_response', function(res) {
            update_mychart(res);
        });

    });

    </script>

</body>
</html>

后端运行不分顺序,实现如视频:

websocket+flask+mysql数据实时传输可视化

结语

websocket相对于ajax在实时监控等场景可以较好的应用,相比较于我之前写的ajax数据传输时延大大降低,后续两个工作:
1、物联网接收数据至服务器(数据库)后,自动向客户端发送数据形成成监控和统计图表。
2、进一步写前后端分离的vue+flask+websocket实现。