「All right reserved, any unauthorized reproduction or transfer is prohibitted」
需求背景
领导要求对测试环境下 websocket 里面的数据进行监控,为此,做了一个小工具,方便直观监控数据的推送情况。
Flask 代理服务
# !/usr/bin/python
# -*- coding: utf-8 -*-
"""
@Author : xxxxxx
@Contact : xxxxxx
@File : flask_proxy.py
@Create Time: 2025/4/27 14:16
@Description: flask代理服务
"""
from flask import Flask, request, jsonify
from flask_cors import CORS
import requests
app = Flask(__name__)
CORS(app) # 启用CORS
@app.route('/', methods=['POST'])
def proxy():
# 获取请求数据
data = {
'username': 'xxxxxx',
'password': 'xxxxxx',
'client_secret': 'xxxxxx',
'client_id': 'xxxxxx',
'grant_type': 'xxxxxx'
}
# 转发请求到目标API
response = requests.post(
'https://xxxxxx/protocol/openid-connect/token',
data=data
)
# 返回响应
return jsonify(response.json()), response.status_code
if __name__ == '__main__':
app.run(port=5000, debug=True)
绘制 HTML 监控网页
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>设备监控工具</title>
<style>
body {
font-family: 'Microsoft YaHei', Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
color: #333;
}
.container {
max-width: 900px;
margin: 0 auto;
background-color: white;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 20px;
display: flex;
flex-direction: column;
height: 90vh;
}
h1 {
text-align: center;
color: #2c3e50;
margin-bottom: 20px;
}
.input-section {
display: flex;
gap: 10px;
margin-bottom: 20px;
padding-bottom: 20px;
border-bottom: 1px solid #eee;
}
input {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 16px;
font-family: 'Microsoft YaHei', sans-serif;
}
button {
padding: 10px 20px;
background-color: #3498db;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s;
font-family: 'Microsoft YaHei', sans-serif;
}
button:hover {
background-color: #2980b9;
}
.clear-btn {
background-color: #e74c3c;
}
.clear-btn:hover {
background-color: #c0392b;
}
.start-monitoring {
background-color: #27ae60;
}
.start-monitoring:hover {
background-color: #219653;
}
.stop-monitoring {
background-color: #e74c3c;
}
.stop-monitoring:hover {
background-color: #c0392b;
}
.scroll-area {
flex: 1;
overflow-y: auto;
border: 1px solid #ddd;
border-radius: 5px;
padding: 15px;
background-color: #f9f9f9;
margin-bottom: 20px;
}
.log-entry {
display: flex;
padding: 12px 15px;
border-radius: 6px;
margin-bottom: 10px;
font-family: 'Microsoft YaHei', monospace;
font-size: 14px;
line-height: 1.5;
background-color: white;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
border-left: 4px solid #3498db;
}
.log-entry:last-child {
margin-bottom: 0;
}
.log-content {
flex: 1;
display: flex;
flex-direction: column;
}
.timestamp {
color: #7f8c8d;
font-weight: bold;
display: inline-block;
width: 80px;
}
.message-wrapper {
display: flex;
align-items: flex-start;
margin-top: 5px;
}
.message-header {
margin-left: 10px;
flex: 1;
font-weight: bold;
}
.message-data {
margin-left: 10px;
flex: 1;
white-space: pre-wrap;
}
.status {
margin-left: auto;
padding: 3px 10px;
border-radius: 10px;
font-size: 12px;
font-weight: bold;
}
.status-success {
background-color: #e1f5e1;
color: #2e7d32;
border: 1px solid #c8e6c9;
}
.status-error {
background-color: #ffebee;
color: #c62828;
border: 1px solid #ffcdd2;
}
.status-info {
background-color: #e3f2fd;
color: #1565c0;
border: 1px solid #bbdefb;
}
.equipment-number {
padding: 2px 5px;
border-radius: 3px;
font-weight: bold;
}
.equipment-number-self {
background-color: #e8f5e9;
color: #2e7d32;
}
.equipment-number-other {
background-color: #fff3e0;
color: #e65100;
}
.button-group {
display: flex;
gap: 10px;
}
.data-received {
font-weight: bold;
color: #fff;
background-color: #555;
padding: 3px 8px;
border-radius: 4px;
margin-right: 10px;
}
.message-block {
display: inline-block;
padding: 3px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
margin-right: 10px;
}
.message-blue {
background-color: #3498db;
color: white;
}
.message-orange {
background-color: #e67e22;
color: white;
}
</style>
</head>
<body>
<div class="container">
<h1>设备监控工具</h1>
<div class="input-section">
<input type="text" id="equipmentId" placeholder="请输入设备号">
<button id="connectBtn">连接</button>
<button id="clearBtn" class="clear-btn">清除</button>
</div>
<div class="scroll-area" id="logArea">
<!-- 日志内容将在这里显示 -->
</div>
<div class="button-group">
<button id="startMonitorBtn">开始监控</button>
<button id="stopMonitorBtn" disabled>停止监控</button>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const equipmentIdInput = document.getElementById('equipmentId');
const connectBtn = document.getElementById('connectBtn');
const clearBtn = document.getElementById('clearBtn');
const logArea = document.getElementById('logArea');
const startMonitorBtn = document.getElementById('startMonitorBtn');
const stopMonitorBtn = document.getElementById('stopMonitorBtn');
let websocket = null;
let isMonitoring = false;
let currentEquipmentId = null;
let retryCount = 0;
const maxRetryAttempts = 5;
const retryDelay = 3000;
let isConnected = false; // 连接状态
let isUserDisconnected = false; // 是否是用户主动断开
function addLog(message, type = 'info', equipmentNumber = null, messageValue = null) {
const now = new Date();
const options = {
timeZone: 'Asia/Shanghai',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
};
const timestamp = now.toLocaleTimeString('zh-CN', options);
const logEntry = document.createElement('div');
logEntry.className = 'log-entry';
const timestampSpan = document.createElement('span');
timestampSpan.className = 'timestamp';
timestampSpan.textContent = timestamp;
const logContent = document.createElement('div');
logContent.className = 'log-content';
const messageWrapper = document.createElement('div');
messageWrapper.className = 'message-wrapper';
const messageHeader = document.createElement('span');
messageHeader.className = 'message-header';
messageHeader.textContent = message;
if (type !== 'info') {
const statusSpan = document.createElement('span');
statusSpan.className = `status status-${type}`;
statusSpan.textContent = type.toUpperCase();
messageWrapper.appendChild(statusSpan);
}
messageWrapper.prepend(messageHeader);
logContent.appendChild(messageWrapper);
if (equipmentNumber !== null) {
const equipmentSpan = document.createElement('span');
equipmentSpan.className = `equipment-number ${equipmentNumber === currentEquipmentId ? 'equipment-number-self' : 'equipment-number-other'}`;
equipmentSpan.textContent = `设备 ${equipmentNumber}`;
messageWrapper.appendChild(equipmentSpan);
}
if (messageValue !== null && messageValue !== undefined) {
const messageBlock = document.createElement('span');
messageBlock.className = 'message-block message-blue';
messageBlock.textContent = `消息: ${messageValue}`;
messageWrapper.appendChild(messageBlock);
}
logEntry.appendChild(timestampSpan);
logEntry.appendChild(logContent);
logArea.appendChild(logEntry);
logArea.scrollTop = logArea.scrollHeight;
}
function createLogEntry(timestamp, messageInfo, messageData) {
const logEntry = document.createElement('div');
logEntry.className = 'log-entry';
const timestampSpan = document.createElement('span');
timestampSpan.className = 'timestamp';
timestampSpan.textContent = timestamp;
const logContent = document.createElement('div');
logContent.className = 'log-content';
logContent.appendChild(messageInfo);
logContent.appendChild(messageData);
logEntry.appendChild(timestampSpan);
logEntry.appendChild(logContent);
return logEntry;
}
clearBtn.addEventListener('click', function() {
logArea.innerHTML = '';
addLog('日志已清除');
});
function isValidEquipmentId(equipmentId) {
return typeof equipmentId === 'string' && equipmentId.trim() !== '';
}
async function connectWebSocket() {
const equipmentId = equipmentIdInput.value.trim();
if (!isValidEquipmentId(equipmentId)) {
addLog('请输入有效的设备号', 'error');
return;
}
currentEquipmentId = equipmentId;
try {
const wsUrl = 'wss://xxxxxx/kc3ip-realtime/websocket';
addLog(`连接到WebSocket URL: ${wsUrl}`);
websocket = new WebSocket(wsUrl);
websocket.onopen = async function() {
retryCount = 0;
addLog('WebSocket连接已建立', 'success');
isConnected = true;
connectBtn.textContent = '已连接';
connectBtn.style.backgroundColor = '#27ae60';
const tokenResponse = await fetch('http://localhost:5000/', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
username: 'xxxxxx',
password: 'xxxxxx',
client_secret: 'xxxxxx',
client_id: 'xxxxxx',
grant_type: 'xxxxxx'
})
});
if (!tokenResponse.ok) {
const body = await tokenResponse.text();
addLog(
`获取access_token失败: ${tokenResponse.statusText}, Response: ${body}`,
'error'
);
throw new Error('获取access_token失败');
}
const tokenData = await tokenResponse.json();
const accessToken = tokenData.access_token;
addLog('成功获取access_token', 'success');
const authMsg = {
message: "authorization",
accessToken: accessToken
};
websocket.send(JSON.stringify(authMsg));
addLog('已发送授权信息');
};
websocket.onmessage = function(event) {
handleMessage(event.data);
};
websocket.onerror = function(error) {
addLog(`WebSocket错误: ${error.message}`, 'error');
if (websocket) {
websocket.close();
}
retryConnection();
};
websocket.onclose = function() {
addLog('WebSocket连接已关闭');
websocket = null;
isMonitoring = false;
if (!isUserDisconnected) {
retryConnection();
} else {
isConnected = false;
connectBtn.textContent = '连接';
connectBtn.style.backgroundColor = '#3498db';
}
};
} catch (error) {
addLog(`连接失败: ${error.message}`, 'error');
if (websocket) {
websocket.close();
}
retryConnection();
}
}
function retryConnection() {
if (!isUserDisconnected && retryCount < maxRetryAttempts) {
retryCount++;
addLog(`连接丢失,正在尝试重新连接 (${retryCount}/${maxRetryAttempts})...`);
setTimeout(connectWebSocket, retryDelay);
} else if (!isUserDisconnected) {
addLog('达到最大重试次数,连接失败', 'error');
}
}
function findEquipmentNumber(obj) {
// 优先提取 equipmentNumber 字段,其次才是 equipment 字段
if (obj.equipmentNumber !== undefined) {
return obj.equipmentNumber;
}
if (obj.equipment !== undefined) {
return obj.equipment;
}
for (const key in obj) {
if (typeof obj[key] === 'object') {
const found = findEquipmentNumber(obj[key]);
if (found !== undefined) {
return found;
}
}
}
return '未知设备';
}
function handleMessage(data) {
try {
const message = JSON.parse(data);
const equipmentNumber = findEquipmentNumber(message);
const messageValue = message.message; // 提取message字段
// 创建日志条目
const logEntry = document.createElement('div');
logEntry.className = 'log-entry';
// 添加时间戳
const now = new Date();
const options = {
timeZone: 'Asia/Shanghai',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
};
const timestamp = now.toLocaleTimeString('zh-CN', options);
const timestampSpan = document.createElement('span');
timestampSpan.className = 'timestamp';
timestampSpan.textContent = timestamp;
// 创建日志内容容器
const logContent = document.createElement('div');
logContent.className = 'log-content';
// 创建消息信息容器
const messageInfo = document.createElement('div');
messageInfo.className = 'message-wrapper';
messageInfo.style.display = 'flex';
messageInfo.style.alignItems = 'center';
// 添加"接收到设备数据"文本
const dataReceived = document.createElement('span');
dataReceived.className = 'data-received';
dataReceived.textContent = '接收到设备数据';
// 添加消息部分(蓝色背景)
const messageBlock = document.createElement('span');
messageBlock.className = 'message-block message-blue';
messageBlock.textContent = `消息: ${messageValue}`;
// 添加设备号部分(橙色背景)
const equipmentBlock = document.createElement('span');
equipmentBlock.className = 'message-block message-orange';
equipmentBlock.textContent = `设备号: ${equipmentNumber}`;
// 组装消息信息
messageInfo.appendChild(dataReceived);
messageInfo.appendChild(messageBlock);
messageInfo.appendChild(equipmentBlock);
// 添加消息数据
const messageData = document.createElement('span');
messageData.className = 'message-data';
messageData.textContent = JSON.stringify(message, null, 2);
// 组装日志内容
logContent.appendChild(messageInfo);
logContent.appendChild(messageData);
// 组装日志条目
logEntry.appendChild(timestampSpan);
logEntry.appendChild(logContent);
// 添加到日志区域
logArea.appendChild(logEntry);
logArea.scrollTop = logArea.scrollHeight;
} catch (error) {
addLog(`处理消息失败: ${error.message}`, 'error');
}
}
startMonitorBtn.addEventListener('click', function() {
if (!websocket || websocket.readyState !== WebSocket.OPEN) {
addLog('请先建立WebSocket连接', 'error');
return;
}
const equipmentId = equipmentIdInput.value.trim();
if (!isValidEquipmentId(equipmentId)) {
addLog('请输入有效的设备号', 'error');
return;
}
const monitorMsg = {
message: "filterEquipment",
param: {
equipments: [equipmentId]
}
};
websocket.send(JSON.stringify(monitorMsg));
addLog(`已发送设备监控请求,设备号: ${equipmentId}`);
isMonitoring = true;
// 更新按钮状态和颜色
startMonitorBtn.disabled = true;
startMonitorBtn.style.backgroundColor = '#27ae60';
startMonitorBtn.textContent = '正在监控';
stopMonitorBtn.disabled = false;
stopMonitorBtn.style.backgroundColor = '#e74c3c';
});
stopMonitorBtn.addEventListener('click', function() {
if (!websocket || websocket.readyState !== WebSocket.OPEN) {
addLog('没有活动的WebSocket连接', 'error');
return;
}
if (!isMonitoring) {
addLog('当前没有进行中的监控', 'error');
return;
}
const stopMsg = {
message: "filterEquipment",
param: {
equipments: []
}
};
websocket.send(JSON.stringify(stopMsg));
addLog('已发送停止监控请求');
isMonitoring = false;
// 更新按钮状态和颜色
startMonitorBtn.disabled = false;
startMonitorBtn.style.backgroundColor = '';
startMonitorBtn.textContent = '开始监控';
stopMonitorBtn.disabled = true;
stopMonitorBtn.style.backgroundColor = '';
});
// 修改连接按钮点击事件处理
connectBtn.addEventListener('click', function() {
if (isConnected) {
// 如果已连接,执行断开操作
isUserDisconnected = true; // 标记为用户主动断开
if (websocket && websocket.readyState === WebSocket.OPEN) {
websocket.close();
}
addLog('已手动断开WebSocket连接', 'info');
isConnected = false;
connectBtn.textContent = '连接';
connectBtn.style.backgroundColor = '#3498db';
} else {
// 如果未连接,执行连接操作
retryCount = 0; // 重置重试计数
connectWebSocket();
}
});
});
</script>
</body>
</html>
开始连接,并进行监控
停止监控,并断开连接
TesterHome 为用户提供「保留所有权利,禁止转载」的选项。
除非获得原作者的单独授权,任何第三方不得转载标注了「All right reserved, any unauthorized reproduction or transfer is prohibitted」的内容,否则均视为侵权。
具体请参见TesterHome 知识产权保护协议。
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
No Reply at the moment.