flaskio+echarts实现图表实时更新
项目概述
本人毕业设计是一个基于WSN的环境监控系统,实现流程如下:
硬件端采集数据(包括温湿度,光照强度,当前位置经纬度等),并通过WiFi模块,上传至后端,后端采用JAVA编写,后端接收数据并对其进行存储,同时转发至客户端(Android,Web端)
具体流程图如下:
由于最近在看Flask,所以打算吧JAVA后端这部分移植到Flask上面
遇到的问题
在移植过程中遇到的最大的坑,普通的Flask不支持Websocket
我在JAVA里面,使用的是Tomcat进行部署,他是支持Websocket的,但是在Flask中是不支持的。
Websocket 概述
WebSocket是HTML5的一个协议,他与HTTP协议有交集,但不是全部。
1.首先,相对于HTTP这种非持久的协议来说,Websocket是一个持久化的协议。
一般来讲,HTTP协议就是一个 Request 和一个 Response ,由客户端发起Request ,服务器返回Response ,完成请求后就会断开。
在HTTP1.1中进行了改进,使得有一个keep-alive,也就是说,在一个HTTP连接中,可以发送多个Request,接收多个Response。 在HTTP中永远一个request只能有一个response。而且这个response也是被动的,不能主动发起。而对于WebSocket,他是一种长连接的,全双工通信协议,即在建立连接后,保持长连接,服务端可以主动向客户端发送数据,客户端也可以主动向服务端发数据,是一个双向过程。
2.WebSocket是基于HTTP协议的,或者说借用了HTTP协议来完成一部分握手
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com
其中和两个关键字,就是告诉服务器,发起的是websocket协议。
Upgrade: websocket
Connection: Upgrade
这个key就相当于验证的作用
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
这个就相当于客户端主动与服务端建立WebSocket请求。(通过HTTP的方式建立)
接下来消息收发就按照WebSocket自身独特的协议部分进行处理
使用WebSocket协议的原因
在我的功能需求中,当有新的数据上传至服务器时,需要在客户端(Android,Web)中及时地动态显示,保证数据的实时性。
传统的方法是:使用HTTP,客户端每隔一段时间向服务器进行轮询,是否有新数据到来。
该方法,每隔一段时间就要进行轮询,并且数据的时效性也不能保证,若时间间隔设置得短,那么资源消耗就比较大,若时间间隔设置过长,时效性就差
使用WebSocket,客户端上线后(与服务端建立了WebSocketl连接),与服务端保持长连接,当服务端有新数据到来时,就将数据转发至客户端,客户端(Android、Web)即能及时收到更新的数据。
Flask实现WebSocket(全双工通信)
经过查阅资料,Flask可以使用2种方式实现,WebSocket
方法一、使用flask-sockets(经过测试,无法连接)
安装flask-sockets
pip install flask-sockets
服务端
from flask import Flask
from flask_sockets import Sockets
import datetime
import time
app = Flask(__name__)
sockets = Sockets(app)
@sockets.route('/websocket')
def echo_socket(ws):
while not ws.closed:
now = datetime.datetime.now().isoformat() + 'Z'
ws.send(now) #发送数据
@app.route('/')
def hello():
return 'Hello World!'
if __name__ == "__main__":
from gevent import pywsgi
from geventwebsocket.handler import WebSocketHandler
server = pywsgi.WSGIServer(('', 5000), app, handler_class=WebSocketHandler)
print('server start')
server.serve_forever()
Web端
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://cdn.bootcss.com/jquery/3.2.0/jquery.js"></script>
</head>
<body>
<div id="time" style="width: 300px;height: 50px;background-color: #0C0C0C;
color: white;text-align: center;line-height: 50px;margin-left: 40%;font-size: 20px"></div>
<script>
var ws = new WebSocket("ws://127.0.0.1:5000/websocket"); #连接server
ws.onmessage = function (event) {
content = document.createTextNode(event.data); # 接收数据
$("#time").html(content);
};
</script>
</body>
</html>
flask-sockets的另一种实现方式(测试无法连接)
服务端
from flask import Flask,request
from geventwebsocket.websocket import WebSocket # 导入这个其实没用,只是用来在敲代码的时候能有提示。
from gevent.pywsgi import WSGIServer
from geventwebsocket.handler import WebSocketHandler
app = Flask(__name__)
@app.route('/ws')
def ws():
print(request.environ.get('wsgi.websocket'))
print(request.environ)
user_socket = request.environ.get('wsgi.websocket') # type: WebSocket
while 1:
msg = user_socket.receive() # 接受消息
print(msg)
user_socket.send(msg) # 发送消息
if __name__ == '__main__':
http_serv = WSGIServer(('0.0.0.0',9527),app,handler_class=WebSocketHandler)
http_serv.serve_forever()
客户端(Web)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script type="text/javascript">
var ws = new WebSocket('ws://127.0.0.1:9527/ws');
ws.onmessage = function (data) {
alert(data)
}
</script>
</body>
</html>
通过flask-socket实现Websocket,试了很多次都没用,可能是用的包比较新,找的教程都是很久比较老的
方法二、通过 flask-sockeio
实现全双工通信(可用)
安装flask-socketio
pip install flask-socketio
socketio
是一种以广播的形式,发布和订阅消息
服务端
import json
from flask import Flask, render_template, request, redirect, sessions
from flask_socketio import SocketIO, emit #导入socketio包
name_space = '/websocket'
app = Flask(__name__)
app.secret_key = 'jzw'
socketio = SocketIO(app)
client_query = []
@socketio.on('connect', namespace=name_space)# 有客户端连接会触发该函数
def on_connect():
# 建立连接 sid:连接对象ID
client_id = request.sid
client_query.append(client_id)
# emit(event_name, broadcasted_data, broadcast=False, namespace=name_space, room=client_id) #指定一个客户端发送消息
# emit(event_name, broadcasted_data, broadcast=True, namespace=name_space) #对name_space下的所有客户端发送消息
print('有新连接,id=%s接加入, 当前连接数%d' % (client_id, len(client_query)))
@socketio.on('disconnect', namespace=name_space)# 有客户端断开WebSocket会触发该函数
def on_disconnect():
# 连接对象关闭 删除对象ID
client_query.remove(request.sid)
print('有连接,id=%s接退出, 当前连接数%d' % (request.sid, len(client_query)))
# on('消息订阅对象', '命名空间区分')
@socketio.on('message', namespace=name_space)
def on_message(message):
""" 服务端接收消息 """
print('从id=%s客户端中收到消息,内容如下:' % request.sid)
print(message)
emit('my_response_message', "我收到了你的信息", broadcast=False, namespace=name_space, room=client_id) #指定一个客户端发送消息
# emit('my_response_message', broadcasted_data, broadcast=True, namespace=name_space) #对name_space下的所有客户端发送消息
@app.route('/') #初始化页面
def a():
return render_template("test.html")
if __name__ == '__main__':
socketio.run(app, host='0.0.0.0', port=5000, debug=False)
# app.run()
开启服务后
客户端
前提,需添加标签导入socket.io.js
<script type="text/javascript" src="https://cdn.bootcdn.net/ajax/libs/socket.io/2.3.0/socket.io.js"></script>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script type="text/javascript" src="https://cdn.bootcdn.net/ajax/libs/socket.io/2.3.0/socket.io.js"></script>
</head>
<body>
<div><button onclick="ws()">连接服务端</button></div>
<div><button onclick="clos_con()">断开连接</button></div>
<div><button onclick="send_msg()">发送消息给服务端</button></div>
<script type="text/javascript">
windw_socket=null
function ws(){
namespace = '/websocket';
var websocket_url = location.protocol+'//' + document.domain + ':' + location.port + namespace;
var socket=io.connect(websocket_url);
// socket.emit('connect2', {'param':'value'}); //发送消息
// socket.close()
socket.on('connect',function(data){
console.log('connecte:'+data);
alert("建立连接成功")
windw_socket=socket
});
socket.on('disconnect',function(data){
alert("连接已断开")
console.log('disconnecte:'+data);
});
socket.on('my_response_message',function(data){
console.log('my_response_message:'+data);
alert("收到服务端的回复:"+data)
});
}
function clos_con(){
if(windw_socket!=null){
windw_socket.close()
}
}
window.onbeforeunload= function(event) {
if (windw_socket!=null && !windw_socket.closed){
// confirm(windw_socket.closed)
windw_socket.close()
}
}
window.onunload= function(event) {
if (windw_socket!=null && !windw_socket.closed){
//confirm(windw_socket.closed)
windw_socket.close()
}
}
function send_msg(){
if(windw_socket!=null){
windw_socket.emit('message', "这里是客户端");
}
}
</script>
</body>
</html>
三个按钮
点击‘连接服务端’
(测试连接成功)
点击发送消息给服务端
(消息测试成功)
断开连接
综上已完成flask-socketio的测试
成功实现全双工通信。
JAVA移植至Python
项目结构
自定义工具包
parse_local
是自定义解析经纬度的包,使用百度地图API
import requests
import json
AK = "私人AK,通过百度地图官方注册获取";
URL = "http://api.map.baidu.com/reverse_geocoding/v3/?ak=" + AK + "&output=json&coordtype=wgs84ll&location=";
class Location(object):
def __init__(self, formatted_address, country=None, province=None, city=None, district=None, street=None):
self.formatted_address = formatted_address
self.country = country
self.province = province
self.city = city
self.district = district
self.street = street
def get_addr(local):
url_full=URL+local
re = json.loads(requests.get(url_full).text)
if re["status"] == 0:
result = re["result"]
address_component = result["addressComponent"]
stree = address_component["district"] + address_component["town"] + \
address_component["street"] + address_component["street_number"]
return Location(formatted_address=result["formatted_address"], country=address_component["country"],
province=address_component["province"], city=address_component["city"],street=stree)
return "no_addr"
sql_manger
是 自定义的数据库操作包,用于数据库的数据存取,以及类封装
import pymysql
import datetime
import traceback
con_options = {'host': '数据库ip',
'port': 3306,
'user': '用户名',
'password': '密码',
'database': '数据库名称',
'charset': 'utf8'}
class DataManger(object):
def _connect(self): #单下划线-保护类型方法,打开数据库连接
try:
conn = pymysql.connect(host=con_options['host'], port=con_options['port'],
user=con_options['user'], password=con_options['password'],
database=con_options['database'], charset=con_options['charset'])
cursor = conn.cursor(cursor=pymysql.cursors.DictCursor)
except Exception:
print("连接数据库失败")
conn.close()
traceback.print_exc()
return None
return conn, cursor
def _closeAll(self, conn, cursor):
try:
conn.close()
cursor.close()
print('关闭数据库连接成功')
except Exception:
traceback.print_exc()
print('关闭数据库连接失败')
def _executeAltSql(self, conn, cursor, sql_str, values):#SQL修改/删除语句,不需要返回结果
try:
cursor.execute(sql_str, values)
conn.commit()
return True
except Exception:
print("执行修改失败")
traceback.print_exc()
finally:
self._closeAll(conn, cursor)
def _executeSerchSql(self, conn, cursor, sql_str, values):#SQL查询语句,需返回结果
try:
cursor.execute(sql_str, values)
values = cursor.fetchall()
return values
except Exception:
print("执行查询失败")
traceback.print_exc()
finally:
self._closeAll(conn, cursor)
class UserHead(DataManger):
def __init__(self, user_id, user_name="none", user_password="123"):
super().__init__()
self._user_id = user_id
self._user_name = user_name
self._user_password = user_password
@property
def user_id(self):
return self._user_id
@property
def user_name(self):
return self._user_name
@property
def user_password(self):
return self._user_password
def serch_user(self):
conn, cursor = self._connect()
sql = 'select * from users where user_id = %s'
return self._executeSerchSql(conn, cursor, sql, self._user_id)
def insert_user(self):
conn, cursor = self._connect()
sql = 'insert into users (user_id,user_name,user_passwd,create_date) values ' \
'(%s,%s,%s,%s)'
return self._executeAltSql(conn, cursor, sql,
(self._user_id, self._user_name, self._user_password, datetime.datetime.now()))
def del_user(self):
conn, cursor = self._connect()
sql = "DELETE FROM `users` WHERE user_id=%s"
return self._executeAltSql(conn, cursor, sql, self._user_id)
def is_in_db(self):
return len(self.serch_user()) > 0
class UserData(UserHead):
def __init__(self, user_head, localtion="未指定", temperature=None, humidity=None, co_2=None, o_2=None, ch_4=None, light=None):
super().__init__(user_head.user_id, user_head.user_name, user_head.user_password)
self._localtion = localtion
self._temperature = temperature
self._humidity = humidity
self._co_2 = co_2
self._o_2 = o_2
self._ch_4 = ch_4
self._light = light
def serch_data_least_n(self, n=1):
conn, cursor = self._connect()
sql = 'select * from data where user_id = %s order by data_time desc limit %s'
data_result_list = self._executeSerchSql(conn, cursor, sql, (self._user_id, n))
for data_obj in data_result_list:
data_obj['data_time'] = data_obj['data_time'].strftime('%Y-%m-%d %H:%M:%S')
return data_result_list
def inser_data(self):
if not self.is_in_db():
print("用户不存存在,自动注册")
self.insert_user()
conn, cursor = self._connect()
sql = 'insert into data (user_id,user_name,data_time,localtion,temperature,humidity,CO2_con,O2_con,CH4_CON,Light_Inte) values ' \
'(%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)'
return self._executeAltSql(conn, cursor, sql,
(self._user_id, self._user_name, datetime.datetime.now(), self._localtion,
self._temperature, self._humidity, self._co_2, self._o_2, self._ch_4, self._light))
HTML/JS文件
HTML文件,导入所需要的包,图表使用开源框架Echarts
<!DOCTYPE html>
<html lang="en" style="height: 100%">
<head>
<link rel="shortcut icon" href= "{{ url_for('static',filename='images/bitbug_favicon.ico') }}" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.js" type="text/javascript" charset="utf-8"></script>
<!-- 引入echarts.js -->
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/echarts/dist/echarts.min.js"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/echarts-gl/dist/echarts-gl.min.js"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/echarts-stat/dist/ecStat.min.js"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/echarts/dist/extension/dataTool.min.js"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/echarts/map/js/china.js"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/echarts/map/js/world.js"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/echarts/dist/extension/bmap.min.js"></script>
<!-- 引入socketio.js -->
<script type="text/javascript" src="https://cdn.bootcdn.net/ajax/libs/socket.io/2.3.0/socket.io.js"></script>
<title>数据检测可视化</title>
<style>
div{float:left}
</style>
</head>
<body style="height: 100%; margin: 0">
<!-- 定义一个div存放可视化图表 -->
<div id="line" style="height:100%; Width:100%"></div>
<!-- 引入自己编写的ajaxEcharts.js文件 -->
<script src="{{ url_for('static',filename='js/ajaxEcharts.js') }}" type="text/javascript" charset="utf-8">
</script>
</body>
</html>
JS文件
/* ajaxEcharts是自己编写 */
var windw_socket=null
//1.折线图可视化化
//2.折线样式设置(这里面的属性都是Echarts,可以根据自己想法从里面选取属性)
var option = {
title: {
text: '数据监测',
subtext: 'xxx'
},
tooltip: {
trigger: 'axis'
},
legend: {
data:['湿度','温度','光照强度','二氧化碳']
},
toolbox: {
show: true,
feature: {
dataZoom: {
yAxisIndex: 'none'
},
dataView: {readOnly: false},
magicType: {type: ['line','bar']},
restore: {},
saveAsImage: {}
}
},
xAxis: {
type: 'category',
boundaryGap: false,
data:[]
},
yAxis: {
type:'value',
axisLabel: {
formatter: '{value}'
}
},
series: [
{
name:'湿度',
type:'line',
data:[],
markPoint: {
data: [
{type: 'max', name: '最高值'},
{type: 'min', name: '最低值'}
]
},
markLine: {
data: [
{type: 'average', name:'平均值'}
]
}
},
{
name:'温度',
type:'line',
data:[],
markPoint: {
data: [
{type: 'max', name: '最高值'},
{type: 'min', name: '最低值'}
]
},
markLine: {
data: [
{type: 'average', name:'平均值'}
]
}
},{
name:'光照强度',
type:'line',
data:[],
markPoint: {
data: [
{type: 'max', name: '最高值'},
{type: 'min', name: '最低值'}
]
},
markLine: {
data: [
{type: 'average', name:'平均值'}
]
}
},{
name:'二氧化碳',
type:'line',
data:[],
markPoint: {
data: [
{type: 'max', name: '最高值'},
{type: 'min', name: '最低值'}
]
},
markLine: {
data: [
{type: 'average', name:'平均值'}
]
}
}
]
};
var myChart;
myChart = echarts.init(document.getElementById('line')); //pie是jsp里面的div的id属性
//myChart.setOption(option);
window.onload = function () {
intChar();
ws();
}
var values=[];
function intChar(){
myChart.showLoading();
option.xAxis.data.length=0;
for(var i=0;i<option.series.length;i++)
option.series[i].data.length=0;
$.ajax({
type : "get",
async : false, //异步请求(同步请求将会锁住浏览器,用户其他操作必须等待请求完成才可以执行)
url : "/get_data?count_n=10&product_id=001",//请求发送到url处
// data : {Cont:'10',product_id:'001'},
dataType :"json", //返回数据形式为json
success: function(result) {
//请求成功时执行该函数内容,result即为服务器返回的json对象
var re=result.DataObj;
var len=re.length;
for(var i=len-1;i>=0;i--){
var data=re[i];
var dateTime=new Date(data.data_time).toLocaleTimeString();
option.xAxis.data.push(dateTime);
option.series[0].data.push(data.humidity);
option.series[1].data.push(data.temperature);
option.series[2].data.push(data.Light_Inte);
option.series[3].data.push(data.CH4_CON);
}
myChart.setOption(option);
//隐藏加载动画略
myChart.hideLoading();
},
error : function(errorMsg) {
//请求失败时执行该函数
alert("请求数据失败!");
myChart.hideLoading();
}
});
}
function ws() {
namespace = '/websocket';
var websocket_url = location.protocol+'//' + document.domain + ':' + location.port + namespace;
var socket=io.connect(websocket_url);
// socket.emit('connect2', {'param':'value'}); //发送消息
// socket.close()
socket.on('connect',function(data){
console.log('connecte:'+data);
windw_socket=socket
});
socket.on('disconnect',function(data){
console.log('disconnecte:'+data);
});
socket.on('response',function(data){
console.log('response:'+data);
});
socket.on('updateView',function(data){
console.log('message:'+data);
parse_data(data)
});
}
function parse_data(data) {
var axisData=(new Date()).toLocaleTimeString();
var mdata=JSON.parse(data).DataObj;
if(option.xAxis.data.length<10){
option.xAxis.data.push(axisData);
option.series[0].data.push(mdata.Humi);
option.series[1].data.push(mdata.Temp);
option.series[2].data.push(mdata.Light);
option.series[3].data.push(mdata.CH4);
}else {
option.xAxis.data.shift();
option.series[0].data.shift();
option.series[1].data.shift();
option.series[2].data.shift();
option.series[3].data.shift();
option.xAxis.data.push(axisData);
option.series[0].data.push(mdata.Humi);
option.series[1].data.push(mdata.Temp);
option.series[2].data.push(mdata.Light);
option.series[3].data.push(mdata.CH4);
}
myChart.setOption(option);
}
window.onbeforeunload= function(event) {
if (windw_socket!=null && !windw_socket.closed){
// confirm(windw_socket.closed)
windw_socket.close()
}
}
window.onunload= function(event) {
if (windw_socket!=null && !windw_socket.closed){
//confirm(windw_socket.closed)
windw_socket.close()
}
}
// window.onunload = function(event) {
// return confirm("确定离开此页面吗?");
// }
Python Flask-socketio
后台
import json
from flask import Flask, render_template, request, redirect, sessions
from flask_socketio import SocketIO, emit
from py_src.utils import sql_manger
from py_src.utils import parse_local
name_space = '/websocket'
app = Flask(__name__)
app.secret_key = 'jiangzhiwei'
socketio = SocketIO(app)
client_query = []
@app.route('/')
def index():
return redirect('/dataView')
@app.route('/login', methods=['GET', 'POST'])#还未写html
def login():
if request.method == 'GET':
return render_template('login.html')
@app.route('/dataView', methods=['GET', 'POST']) # 添加路由的第一种方式
def data_view():
if request.method == 'GET':
return render_template('dataView.html')
if request.method == 'POST':
json_str = request.get_data(as_text=True)
json_object = json.loads(json_str)
user_name = json_object["USER_NAME"]
user_id = json_object["USER_ID"]
data_obj = json_object["DataObj"]
location = parse_local.get_addr(data_obj["Loca"]).formatted_address
user_head = sql_manger.UserHead(user_id=user_id, user_name=user_name)
user_data = sql_manger.UserData(user_head=user_head, localtion=location,
temperature=data_obj["Temp"], humidity=data_obj["Humi"], ch_4=data_obj["CH4"],
light=data_obj["Light"])
print(json_str)
if user_data.inser_data():
emit('updateView', json_str, broadcast=True, namespace=name_space)
return ""
@app.route('/get_data', methods=['GET']) # 添加路由的第一种方式
def get_data():
if request.method == 'GET':
count_n = int(request.args.get("count_n"))
product_id = request.args.get("product_id")
head = sql_manger.UserHead(product_id)
data = sql_manger.UserData(head)
data_result_list = data.serch_data_least_n(count_n)
re_dict = {}
re_dict["DataObj"] = data_result_list
print(re_dict)
return re_dict
# on('消息订阅对象', '命名空间区分')
@socketio.on('message', namespace=name_space)
def on_message(message):
""" 服务端接收消息 """
print('从id=%s客户端中收到消息,内容如下:' % request.sid)
print(message)
@socketio.on('connect', namespace=name_space)
def on_connect():
# 建立连接 sid:连接对象ID
client_id = request.sid
client_query.append(client_id)
# emit(event_name, broadcasted_data, broadcast=False, namespace=name_space, room=client_query[0]) #指定一个客户端发送消息
print('有新连接,id=%s接加入, 当前连接数%d' % (client_id, len(client_query)))
@socketio.on('disconnect', namespace=name_space)
def on_disconnect():
# 连接对象关闭 删除对象ID
client_query.remove(request.sid)
print('有连接,id=%s接退出, 当前连接数%d' % (request.sid, len(client_query)))
if __name__ == '__main__':
socketio.run(app, host='0.0.0.0', port=5000, debug=False)
# app.run()