前言
本人于年初基于Flask+echarts+mysql+ajax实现了自动刷新,但由于ajax轮询的弊端(请求必须由客户端向服务端发起,然后服务端进行响应),想看ajax实现的朋友可以看我写的这篇Flask+echarts+mysql+自动刷新。现改用双向传输、推送消息方面能够做到灵活、简便、高效实现方案,即数据库收到数据立刻向客户端发送数据,无需客户端先向数据库发送请求。
一、环境准备
网上已经有许多教程,但由于websocket版本匹配和引入库版本等问题,大部分截至我发文阶段都无法直接实现,经过本人测试更改,以下为实现代码,另附上我的相关版本,以免读者走弯路,如需要查看其他版本的匹配方案,见官方文档
以下为官方包版本匹配说明:
以下为本人操作案例版本:
二、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+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 操作Mysql数据库、Flask-SQLAlchemy详解、查询结果集转json。
-
创建数据库
这一步可以直接从数据库可视化软件navicate、mysql命令行或者是flask框架的拓展包来进行,为了方便后续操作,在这里使用flask框架的数据库拓展包Flask-SQLAlchemy,创建测试类数据库代码如下:db_create.py
扫描二维码关注公众号,回复: 14531961 查看本文章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:%[email protected]: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
,看到表创建完成
-
定时插入数据
参考多线程定时器,以下为对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秒插入一次随机数据:
-
查询数据
想到两种方式,一种是通过查询数据库取最近十条记录传给前端渲染显示;另一种是只查询最近的一条,通过前端将最前一条数据挤出去显示。
在这里我们使用第二种方法,就查询最近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
-
websocket查询数据
在从数据库获取最新数据的时候出现了一个问题,插入的数据无法被获取,收到的数据都很旧,经过查询资料,这篇文章SQLAlchemy为了加快查速度,因而存在"缓存"机制解释了为什么,需要先清空缓存,再查询:#清空缓存 db_session.commit()
在这里实现源码,注释我都写在源码里了,在这里不过多做赘述,需要的朋友直接copy,这是demo结构:
后端 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:%[email protected]: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实现。
如果对您有帮助的话,点个赞呗,欢迎各位老爷打赏!