前言
It has been a while since I last updated my blogs.
使用Tornado开发1个实时监控信息系统,其中包括 CUP、内存、网卡、磁盘使用率。
涉及技术
编程语言:Python
关系型数据库:MySQL
Web框架:Tornado
数据库连接驱动:mysql-connector-python
数据库ORM:sqlalchemy
服务端websocket通信:sockjs-tornado
客户端websocket通信:sockejs-client
前端:HTML、CSS、JS
图表可视化:pyecharts==0.5.11 #新版本和旧版本差异较大
获取硬件信息工具:psutil模块
pip install -i https://pypi.douban.com/simple --trusted-host pypi.douban.com -r requirements.txt
-i:指定国内安装源 --trusted-host:指定信任主机 -r指定依赖文件
Tornado项目目录结构设计
Monitor
-------->app01
------>models
------>static
------->template
------->tools
------->views
manage.py
from sqlalchemy.ext.declarative import declarative_base #模型继承的父类 from sqlalchemy.dialects.mssql import BIGINT,DECIMAL,DATE,TIME,DATETIME#导入数据库字段 from sqlalchemy import Column#用于创建字段的类 Base=declarative_base()#调用 metadata=Base.metadata#创建元类 class Mem(Base): __tablename__='mem' id=Column(BIGINT,primary_key=True) precent=Column(DECIMAL(6,2))#小数类型 保留6位数字 2位小数 total=Column(DECIMAL(8,2))#总量 used=Column(DECIMAL(8,2))#使用率 free=Column(DECIMAL(8,2))#剩余率 create_date=Column(DATE)#创建的日期 create_time=Column(TIME)#c创建的时间 create_dt=Column(DATETIME)#创建的日期+时间 class Swap(Base): __tablename__='swap' id = Column(BIGINT,primary_key=True) precent = Column(DECIMAL(6,2)) # 小数类型 保留6位数字 2位小数 total = Column(DECIMAL(8,2)) # 总量 used = Column(DECIMAL(8,2)) # 使用率 free = Column(DECIMAL(8,2)) # 剩余率 create_date = Column(DATE) # 创建的日期 create_time = Column(TIME) # c创建的时间 create_dt = Column(DATETIME) # 创建的日期+时间 class CPU(Base): __tablename__ = 'cpu' id = Column(BIGINT, primary_key=True) precent = Column(DECIMAL(6, 2)) # 小数类型 保留6位数字 2位小数 create_date = Column(DATE) # 创建的日期 create_time = Column(TIME) # c创建的时间 create_dt = Column(DATETIME) # 创建的日期+时间 if __name__ == '__main__': from sqlalchemy import create_engine mysql_configs={ "db_host":'192.168.56.128', "db_name":"web", "db_port":3306, "db_user":"web", "db_pwd":"123.com" } link="mysql+mysqlconnector://{db_user}:{db_pwd}@{db_host}:{db_port}/{db_name}".format(**mysql_configs) engine=create_engine(link,encoding="utf-8",echo=True) metadata.create_all(engine)#调用元类
一、WebSocket协议
WebSocket 协议是1种全双工通信协议,基于TCP 连接,旨在通过单1的长连接在客户端和服务器之间实现实时、低延迟的数据传输。
WebSocket协议最初由HTML5标准定义,用于解决传统 HTTP 请求无法高效处理实时通信的问题。
1.特点
-
全双工通信(Full-Duplex)
客户端和服务器可以同时发送和接收消息,无需等待对方完成操作。 -
单一持久连接
WebSocket 使用一个长期保持的 TCP 连接,避免了 HTTP 协议中每次通信都需要重新建立连接的开销。 -
实时性
由于不需要每次发送消息都创建新的连接,WebSocket 非常适合实时性要求较高的场景,如聊天应用、在线游戏、股票行情等。 -
轻量化协议头
相比 HTTP 的冗长头部,WebSocket 的头部较小,数据传输效率更高。
2.工作流程
2.1.握手阶段
WebSocket 使用 HTTP 协议完成初始握手:
- 客户端发起 HTTP请求,包含
Upgrade: websocket
和Connection: Upgrade
等头信息,表明希望升级到WebSocket协议。 - 服务器如果支持WebSocket,则返回状态码
101 Switching Protocols
,升级成功后切换到WebSocket协议。
握手示例
2.1.1.客户端发送HTTP请求
客户端在握手请求中包含1个随机的Sec-WebSocket-Key,Sec-WebSocket-Ket是由客户端生成的Base64编码的随机值。
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
2.1.2.服务端响应HTTP请求
服务器接收到客户的Sec-WebSocket-Key
后,将其与服务端的MagicString拼接。
dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11
对该拼接字符串进行SHA-1哈希计算,然后将结果进行Base64 编码。
得到的编码值就是 Sec-WebSocket-Accept
:
s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
服务器将生成的Sec-WebSocket-Accept
返回给客户端:
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
客户端验证Sec-WebSocket-Accept
是否正确?
客户端使用它发送的Sec-WebSocket-Key和MagicString,按照相同的方式计算出Sec-WebSocket-Accept。
如果计算结果与服务器返回的匹配,握手成功,进入全双工通信。
2.1.3.Magic String
MagicString是WebSocket协议的客户端和服务端,双方约定的固定字符串,固定值为258EAFA5-E914-47DA-95CA-C5AB0DC85B11;
- 防伪造:MagicString确保客户端和服务器的握手具有唯一性,即便有人拦截了握手请求,也无法伪造有效的响应。
- 协议验证:服务器根据MagicString生成响应值,客户端验证后确保连接双方都遵循WebSocket协议。
- 安全性:MagicString的固定值和随机的
Sec-WebSocket-Key
结合,形成了类似于 “挑战-响应” 的认证机制,防止简单重放攻击。
2.2.连接关闭
双方可以通过发送Close
帧来关闭连接,TCP 连接随之释放。
3.WebSocket 的数据帧格式
WebSocket通过帧(Frame)传输数据,帧格式如下:
- 固定部分(2字节):
- FIN(1 bit):表示消息是否结束。
- Opcode(4 bits):定义帧类型(如文本、二进制、关闭等)。
- Mask(1 bit):客户端发送的帧必须是掩码加密的。
- Payload Length(7 bits/16 bits/64 bits):表示数据长度。
- 可选部分:
- Masking Key(4 bytes):用于解码消息内容(仅客户端帧有)。
- 数据部分:
- Payload Data:实际传输的数据。
4.WebSocket的应用场景
-
实时聊天
比如即时通讯(如微信、Slack)、客服系统等,要求消息快速交互。 -
在线游戏
游戏客户端与服务器之间需要实时更新状态。 -
股票行情
实时更新股票价格和交易信息。 -
协同编辑
多用户实时协作编辑文档或绘图。 -
物联网(IoT)
设备与服务器之间需要低延迟数据传输。
二、使用WebSocket协议
前端使用Sockjs-client插件与Tornado的建立WebSocket连接;
1.前端使用:sockejs-client插件
//1.定义长连接 var conn = null; //2.定义连接函数 function connect() { disconnect();//把之前的连接关闭掉,在创建新的连接 //0.定义协议 var transports = ["websocket"]; //1.创建连接对象 conn = new SockJS("http://" + window.location.host + "/real/time",transports); //2.建立连接 conn.onopen = function () { console.log('连接成功') }; //3.建立发送消息 conn.onmessage = function (e) { console.log(e.data); }; //4.建立关闭连接 conn.onclose = function (e) { console.log("断开连接"); }; setInterval(function () { conn.send("system") },1000) } function disconnect() { if (conn != null) { conn.close(); conn = null; } } if (conn == null) { connect(); } else { disconnect(); }
2.Tornado使用:sockjs
from sockjs.tornado import SockJSConnection#专门生成web_socket服务 class RealTimeHandler(SockJSConnection): waiters=set()#定义1个客户端连接池,所有客户端共用1个集合 #1.建立连接(不存在重复的连接) def on_open(self, request): try: self.waiters.add(self) except Exception as e: print(e) #2.发送消息 def on_message(self, message):#接收客户端消息 try: self.broadcast(self.waiters,message)#把消息广播给所有连接的客户端 except Exception as e: print(e) #3.关闭连接 def on_close(self): try: self.waiters.remove(self) except Exception as e: print(e)
三、前端数据实时更新
1.CPU信息实时更新(水球图)
Tornado调用pyecharts生成前端代码(html+js+css);
import datetime from pyecharts import Liquid, Gauge, Pie, Line #水球图、 class Chart(object): def liquid_html(self,chart_id,title,val):#水球图 liquid = Liquid( title="{}-{}".format(self.dt, title), title_pos="center", width="100%", title_color="white", title_text_size=14, height=300 ) liquid.chart_id=chart_id#指定 chart_id liquid.add("",[round(val/100,4)])#添加数据 return liquid.render_embed()#返回html图表代码 @property def dt(self): return datetime.datetime.now().strftime('%Y%m%dT%H:%M:%S')
Tornado把pyecharts生成的前端代码(html+js+css),响应给浏览器渲染;
<div id="cpu_avg" style="width:100%;height:300px;"></div> <script type="text/javascript"> var myChart_cpu_avg = echarts.init(document.getElementById('cpu_avg'), 'light', {renderer: 'canvas'}); var option_cpu_avg = { "title": [ { "text": "20190910T15:13:19-cpu\u5e73\u5747\u4f7f\u7528\u7387", "left": "center", "top": "auto", "textStyle": { "color": "white", "fontSize": 14 }, "subtextStyle": { "fontSize": 12 } } ], "toolbox": { "show": true, "orient": "vertical", "left": "95%", "top": "center", "feature": { "saveAsImage": { "show": true, "title": "save as image" }, "restore": { "show": true, "title": "restore" }, "dataView": { "show": true, "title": "data view" } } }, "series_id": 8147274, "tooltip": { "trigger": "item", "triggerOn": "mousemove|click", "axisPointer": { "type": "line" }, "textStyle": { "fontSize": 14 }, "backgroundColor": "rgba(50,50,50,0.7)", "borderColor": "#333", "borderWidth": 0 }, "series": [ { "type": "liquidFill", "data": [ 0.167 ], "waveAnimation": true, "animationDuration": 2000, "animationDurationUpdate": 1000, "color": [ "#294D99", "#156ACF", "#1598ED", "#45BDFF" ], "shape": "circle", "outline": { "show": true } } ], "legend": [ { "data": [], "selectedMode": "multiple", "show": true, "left": "center", "top": "top", "orient": "horizontal", "textStyle": { "fontSize": 12 } } ], "animation": true, "color": [ "#c23531", "#2f4554", "#61a0a8", "#d48265", "#749f83", "#ca8622", "#bda29a", "#6e7074", "#546570", "#c4ccd3", "#f05b72", "#ef5b9c", "#f47920", "#905a3d", "#fab27b", "#2a5caa", "#444693", "#726930", "#b2d235", "#6d8346", "#ac6767", "#1d953f", "#6950a1", "#918597", "#f6f5ec" ] }; myChart_cpu_avg.setOption(option_cpu_avg); </script> </div>
浏览器发送web socket请求给Tornado server端
Tornado不断响应浏览器pyecharts生成前端代码(html+js+css)
import json from sockjs.tornado import SockJSConnection#专门生成web_socket服务 from app01.tools.monitor_tools import Monitor class RealTimeHandler(SockJSConnection): waiters=set()#定义1个客户端连接池,所有客户端共用1个集合 #1.建立连接(不存在重复的连接) def on_open(self, request): try: self.waiters.add(self) except Exception as e: print(e) #2.发送消息 def on_message(self, message):#接收客户端消息,根据客户端发送过来的消息返回一些数据! try: if message =="system": m=Monitor() data={"mem":m.mem(),"swap":m.swap(),"cpu":m.cpu(),"disk":m.disk(),"net":m.net(),'dt':m.dt} self.broadcast(self.waiters,json.dumps(data,ensure_ascii=False))#把消息广播给所有连接的客户端 except Exception as e: print(e) #3.关闭连接 def on_close(self): try: self.waiters.remove(self) except Exception as e: print(e)
浏览器通过JS代码不断修改pyechart生成的前端代码
//进度条变化 function progress_status(val) { var data = ""; if (val >= 0 && val < 25) { data = " bg-success"; } else if (val >= 25 && val < 50) { data = ""; } else if (val >= 50 && val < 75) { data = " bg-warning"; } else if (val >= 75 && val <= 100) { data = " bg-success"; } return data } function update_ui(e) { var data=JSON.parse(e.data); //因为pyechart 声明了变量option_cpu_avg所有我们只需要修改现有的变量option_cpu_avg即可 option_cpu_avg.series[0].data[0]=(data['cpu']['percent_avg']/100).toFixed(4); //保留4位小数 option_cpu_avg.title[0].text=data['dt']+'CPU平均使用率'; //保证对option_cpu_avg的修改生效 myChart_cpu_avg.setOption(option_cpu_avg); //------------------------------------------------------- var cpu_per = ""; for (var k in data['cpu']['percent_per']) { var num = parseInt(k); cpu_per += "<tr><td class='text-primary' style='width: 30%'>CPU" + num + "</td>"; cpu_per += "<td><div class='progress'><div class='progress-bar progress-bar-striped progress-bar-animated" + progress_status(data['cpu']['percent_per'][k]) + "' role='progressbar' aria-valuenow='" + data['cpu']['percent_per'][k] + "' aria-valuemin='0' aria-valuemax='100' style='width: " + data['cpu']['percent_per'][k] + "%'>" + data['cpu']['percent_per'][k] + "%</div></div></td></tr>"; } document.getElementById("tb_cpu_per").innerHTML = cpu_per; } //1.定义长连接 var conn = null; //2.定义连接函数 function connect() { disconnect();//把之前的连接关闭掉,在创建新的连接 //0.定义协议 var transports = ["websocket"]; //1.创建连接对象 conn = new SockJS("http://" + window.location.host + "/real/time",transports); //2.建立连接 conn.onopen = function () { console.log('连接成功') }; //3.建立接收消息 conn.onmessage = function (e) { // console.log(e.data); update_ui(e) }; //4.建立关闭连接 conn.onclose = function (e) { console.log("断开连接"); }; setInterval(function () { //2.1 建立连接发送消息 conn.send("system") },1000) } function disconnect() { if (conn != null) { conn.close(); conn = null; } } if (conn == null) { connect(); } else { disconnect(); }
2.内存+交互分区信息实时展示(仪表图)
import tornado.web from app01.views.views_commen import CommnHardler from app01.tools.monitor_tools import monitor_obj from app01.tools.chart import Chart #定义1个首页的视图 class IndexHandler(CommnHardler): def get(self,*args,**kwargs): cpu_info = monitor_obj.cpu() cpu_percent_avg_info=cpu_info ['percent_avg']#CPU平均使用率 cpu_percent_per=cpu_info ['percent_per'] #每个CPU使用率 mem_info=monitor_obj.mem() swap_info=monitor_obj.swap() print(swap_info) c=Chart() liquid_html=c.liquid_html(chart_id='cpu_avg',title="cpu平均使用率",val=cpu_percent_avg_info) self.render("index.html",**{ "liquid_html":liquid_html, "cpu_percent_per_info" :cpu_percent_per, "mem_html":c.guage_html("mem","内存使用率",mem_info['percent']), "swap_html":c.guage_html('swap',"交互分区使用率",swap_info['percent']), "mem_info":mem_info,"swap_info": swap_info })
//进度条变化 function progress_status(val) { var data = ""; if (val >= 0 && val < 25) { data = " bg-success"; } else if (val >= 25 && val < 50) { data = ""; } else if (val >= 50 && val < 75) { data = " bg-warning"; } else if (val >= 75 && val <= 100) { data = " bg-success"; } return data } function update_ui(e) { var data=JSON.parse(e.data); //因为pyechart 声明了变量option_cpu_avg所有我们只需要修改现有的变量option_cpu_avg即可 option_cpu_avg.series[0].data[0]=(data['cpu']['percent_avg']/100).toFixed(4); //保留4位小数 option_cpu_avg.title[0].text=data['dt']+'CPU平均使用率'; //保证对option_cpu_avg的修改生效 myChart_cpu_avg.setOption(option_cpu_avg); //------------------------------------------------------- var cpu_per = ""; for (var k in data['cpu']['percent_per']) { var num = parseInt(k); cpu_per += "<tr><td class='text-primary' style='width: 30%'>CPU" + num + "</td>"; cpu_per += "<td><div class='progress'><div class='progress-bar progress-bar-striped progress-bar-animated" + progress_status(data['cpu']['percent_per'][k]) + "' role='progressbar' aria-valuenow='" + data['cpu']['percent_per'][k] + "' aria-valuemin='0' aria-valuemax='100' style='width: " + data['cpu']['percent_per'][k] + "%'>" + data['cpu']['percent_per'][k] + "%</div></div></td></tr>"; } document.getElementById("tb_cpu_per").innerHTML = cpu_per; /*内存实时更新*/ option_mem.series[0].data[0].value = data['mem']['percent']; option_mem.title[0].text = data["dt"] + "-内存使用率"; myChart_mem.setOption(option_mem); document.getElementById("mem_percent").innerText = data['mem']['percent']; document.getElementById("mem_total").innerText = data['mem']['total']; document.getElementById("mem_used").innerText = data['mem']['used']; document.getElementById("mem_free").innerText = data['mem']['free']; /*交换分区实时更新*/ option_swap.series[0].data[0].value = data['swap']['percent']; option_swap.title[0].text = data["dt"] + "-交换分区使用率"; myChart_swap.setOption(option_swap); document.getElementById("swap_percent").innerText = data['swap']['percent']; document.getElementById("swap_total").innerText = data['swap']['total']; document.getElementById("swap_used").innerText = data['swap']['used']; document.getElementById("swap_free").innerText = data['swap']['free']; } //1.定义长连接 var conn = null; //2.定义连接函数 function connect() { disconnect();//把之前的连接关闭掉,在创建新的连接 //0.定义协议 var transports = ["websocket"]; //1.创建连接对象 conn = new SockJS("http://" + window.location.host + "/real/time",transports); //2.建立连接 conn.onopen = function () { console.log('连接成功') }; //3.建立接收消息 conn.onmessage = function (e) { // console.log(e.data); update_ui(e) }; //4.建立关闭连接 conn.onclose = function (e) { console.log("断开连接"); }; setInterval(function () { //2.1 建立连接发送消息 conn.send("system") },1000) } function disconnect() { if (conn != null) { conn.close(); conn = null; } } if (conn == null) { connect(); } else { disconnect(); }
{% extends "layout.html" %}<!--继承布局文件--> {% block head %} <script src="{{ static_url('echarts-liquidfill/echarts-liquidfill.min.js') }}"></script> {% end %} {% block content %} <div class="row"> <div class="col-md-12"> <div class="card text-white bg-dark mb-3"> <div class="card-header">CPU信息</div> <div class="card-body"> <div class="row"> <div class="col-md-6 pad-left"> <table class="table table-responsive-sm table-bordered"> <thead> <th colspan="2">所有CPU使用率</th> </thead> <tbody id="tb_cpu_per"> {% for k,v in enumerate(cpu_percent_per_info) %} <tr> <td class="text-primary" style="width: 30%"> CPU{{ k }} </td> <td> <div class="progress"> <!---进度条!--> <div class="progress-bar progress-bar-striped progress-bar-animated {{ handler.progress_status(v)}}" role="progressbar" aria-valuenow="{{ v }}" aria-valuemin="0" aria-valuemax="100" style="width:{{ v }}%">{{ v }}% </div> </div> </td> </tr> {% end %} </tbody> </table> </div> <div class="col-md-6 pad-right"> <div class="border border-white">{% raw liquid_html%}</div> </div> </div> </div> </div> </div> <div class="col-md-12"> <div class="card text-white bg-dark mb-3"> <div class="card-header">内存/交互分区信息</div> <div class="card-body"> <div class="row"> <div class="col-md-6 pad-left"> <div class="border border-white">{% raw mem_html %}</div> <table class="table table-sm table-bordered"> <tr> <td class="text-primary" style="width: 30%">使用率(%)</td> <td id="mem_percent" class="text-danger">{{ mem_info['percent'] }}</td> </tr> <tr> <td class="text-primary" style="width: 30%">总量(GB)</td> <td id="mem_total" class="text-danger">{{ mem_info['total'] }}</td> </tr> <tr> <td class="text-primary" style="width: 30%">使用量(GB)</td> <td id="mem_used" class="text-danger">{{ mem_info['used'] }}</td> <tr> <tr> <td class="text-primary" style="width: 30%">剩余量(GB)</td> <td id="mem_free" class="text-danger">{{ mem_info['free'] }}</td> </tr> </table> </div> <div class="col-md-6 pad-right"> {% raw swap_html %} <table class="table table-sm table-bordered"> <tr> <td class="text-primary" style="width: 30%">使用率(%)</td> <td id="swap_percent" class="text-danger">{{swap_info['percent']}}</td> </tr> <tr> <td class="text-primary" style="width: 30%">总量(GB)</td> <td id="swap_total" class="text-danger" >{{swap_info['total']}}</td> </tr> <tr> <td class="text-primary" style="width: 30%">使用量(GB)</td> <td id="swap_used" class="text-danger">{{swap_info[ 'used']}}</td> <tr> <tr> <td class="text-primary" style="width: 30%">剩余量(GB)</td> <td id="swap_free" class="text-danger">{{swap_info['free']}}</td> </tr> </table> </div> </div> </div> </div> </div> </div> {% end %}
--------------支持web socket的协议
import json from sockjs.tornado import SockJSConnection#专门生成web_socket服务 from app01.tools.monitor_tools import Monitor class RealTimeHandler(SockJSConnection): waiters=set()#定义1个客户端连接池,所有客户端共用1个集合 #1.建立连接(不存在重复的连接) def on_open(self, request): try: self.waiters.add(self) except Exception as e: print(e) #2.发送消息 def on_message(self, message):#接收客户端消息,根据客户端发送过来的消息返回一些数据! try: if message =="system": m=Monitor() data={"mem":m.mem(),"swap":m.swap(),"cpu":m.cpu(),"disk":m.disk(),"net":m.net(),'dt':m.dt} self.broadcast(self.waiters,json.dumps(data,ensure_ascii=False))#把消息广播给所有连接的客户端 except Exception as e: print(e) #3.关闭连接 def on_close(self): try: self.waiters.remove(self) except Exception as e: print(e)
258EAFA5-E914-47DA-95CA-C5AB0DC85B11