482 lines
14 KiB
HTML
482 lines
14 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>WebSocket 测试工具</title>
|
||
<style>
|
||
* {
|
||
box-sizing: border-box;
|
||
margin: 0;
|
||
padding: 0;
|
||
font-family: 'Arial', 'Microsoft YaHei', sans-serif;
|
||
}
|
||
|
||
body {
|
||
max-width: 1200px;
|
||
margin: 20px auto;
|
||
padding: 0 20px;
|
||
background-color: #f5f7fa;
|
||
}
|
||
|
||
.container {
|
||
background: white;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||
padding: 25px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
h1 {
|
||
color: #2c3e50;
|
||
margin-bottom: 20px;
|
||
font-size: 24px;
|
||
border-bottom: 2px solid #3498db;
|
||
padding-bottom: 10px;
|
||
}
|
||
|
||
.status-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 15px;
|
||
margin-bottom: 20px;
|
||
padding: 12px 15px;
|
||
background-color: #f8f9fa;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.status-label {
|
||
font-weight: bold;
|
||
color: #495057;
|
||
}
|
||
|
||
.status-value {
|
||
padding: 4px 10px;
|
||
border-radius: 4px;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.status-connected {
|
||
background-color: #d4edda;
|
||
color: #155724;
|
||
}
|
||
|
||
.status-disconnected {
|
||
background-color: #f8d7da;
|
||
color: #721c24;
|
||
}
|
||
|
||
.status-connecting {
|
||
background-color: #fff3cd;
|
||
color: #856404;
|
||
}
|
||
|
||
.btn {
|
||
padding: 8px 16px;
|
||
border: none;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
transition: background-color 0.2s;
|
||
}
|
||
|
||
.btn-primary {
|
||
background-color: #3498db;
|
||
color: white;
|
||
}
|
||
|
||
.btn-primary:hover {
|
||
background-color: #2980b9;
|
||
}
|
||
|
||
.btn-danger {
|
||
background-color: #e74c3c;
|
||
color: white;
|
||
}
|
||
|
||
.btn-danger:hover {
|
||
background-color: #c0392b;
|
||
}
|
||
|
||
.btn-success {
|
||
background-color: #2ecc71;
|
||
color: white;
|
||
}
|
||
|
||
.btn-success:hover {
|
||
background-color: #27ae60;
|
||
}
|
||
|
||
.control-group {
|
||
display: flex;
|
||
gap: 15px;
|
||
margin-bottom: 20px;
|
||
align-items: center;
|
||
}
|
||
|
||
.input-group {
|
||
display: flex;
|
||
gap: 10px;
|
||
align-items: center;
|
||
}
|
||
|
||
.input-group label {
|
||
color: #495057;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.input-group input, .input-group select {
|
||
padding: 8px 12px;
|
||
border: 1px solid #ced4da;
|
||
border-radius: 4px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.message-area {
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.message-input {
|
||
width: 100%;
|
||
height: 100px;
|
||
padding: 12px;
|
||
border: 1px solid #ced4da;
|
||
border-radius: 6px;
|
||
resize: none;
|
||
font-size: 14px;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.log-area {
|
||
width: 100%;
|
||
height: 300px;
|
||
padding: 15px;
|
||
border: 1px solid #ced4da;
|
||
border-radius: 6px;
|
||
background-color: #f8f9fa;
|
||
overflow-y: auto;
|
||
font-size: 14px;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.log-item {
|
||
margin-bottom: 8px;
|
||
padding-bottom: 8px;
|
||
border-bottom: 1px dashed #e9ecef;
|
||
}
|
||
|
||
.log-time {
|
||
color: #6c757d;
|
||
font-size: 12px;
|
||
margin-right: 10px;
|
||
}
|
||
|
||
.log-send {
|
||
color: #2980b9;
|
||
}
|
||
|
||
.log-receive {
|
||
color: #27ae60;
|
||
}
|
||
|
||
.log-status {
|
||
color: #856404;
|
||
}
|
||
|
||
.log-error {
|
||
color: #e74c3c;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<h1>WebSocket 测试工具</h1>
|
||
|
||
<!-- 连接状态区 -->
|
||
<div class="status-bar">
|
||
<div class="status-label">连接状态:</div>
|
||
<div id="connectionStatus" class="status-value status-disconnected">未连接</div>
|
||
<div class="status-label">服务地址:</div>
|
||
<div id="wsUrl" class="status-value">ws://192.168.110.25:8000/ws</div>
|
||
<div class="status-label">连接时间:</div>
|
||
<div id="connectTime" class="status-value">-</div>
|
||
</div>
|
||
|
||
<!-- 控制按钮区 -->
|
||
<div class="control-group">
|
||
<button id="connectBtn" class="btn btn-primary">建立连接</button>
|
||
<button id="disconnectBtn" class="btn btn-danger" disabled>断开连接</button>
|
||
|
||
<!-- 心跳控制 -->
|
||
<div class="input-group">
|
||
<label>自动心跳:</label>
|
||
<select id="autoHeartbeat">
|
||
<option value="on">开启</option>
|
||
<option value="off">关闭</option>
|
||
</select>
|
||
<label>间隔(秒):</label>
|
||
<input type="number" id="heartbeatInterval" value="30" min="10" max="120" style="width: 80px;">
|
||
<button id="sendHeartbeatBtn" class="btn btn-success">手动发送心跳</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 自定义消息发送区 -->
|
||
<div class="message-area">
|
||
<h3>发送自定义消息</h3>
|
||
<textarea id="messageInput" class="message-input"
|
||
placeholder='示例:{"type":"test","content":"Hello WebSocket"}'>{"type":"test","content":"Hello WebSocket"}</textarea>
|
||
<button id="sendMessageBtn" class="btn btn-primary" disabled>发送消息</button>
|
||
</div>
|
||
|
||
<!-- 日志显示区 -->
|
||
<div class="message-area">
|
||
<h3>消息日志</h3>
|
||
<div id="logContainer" class="log-area">
|
||
<div class="log-item"><span class="log-time">[加载完成]</span> 请点击「建立连接」开始测试</div>
|
||
</div>
|
||
<button id="clearLogBtn" class="btn btn-primary" style="margin-top: 10px;">清空日志</button>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// 全局变量
|
||
let ws = null;
|
||
let heartbeatTimer = null;
|
||
const wsUrl = "ws://192.168.110.25:8000/ws";
|
||
|
||
// DOM 元素
|
||
const connectionStatus = document.getElementById('connectionStatus');
|
||
const connectTime = document.getElementById('connectTime');
|
||
const connectBtn = document.getElementById('connectBtn');
|
||
const disconnectBtn = document.getElementById('disconnectBtn');
|
||
const sendMessageBtn = document.getElementById('sendMessageBtn');
|
||
const sendHeartbeatBtn = document.getElementById('sendHeartbeatBtn');
|
||
const autoHeartbeat = document.getElementById('autoHeartbeat');
|
||
const heartbeatInterval = document.getElementById('heartbeatInterval');
|
||
const messageInput = document.getElementById('messageInput');
|
||
const logContainer = document.getElementById('logContainer');
|
||
const clearLogBtn = document.getElementById('clearLogBtn');
|
||
|
||
// 工具函数:添加日志
|
||
function addLog(content, type = 'status') {
|
||
const now = new Date().toLocaleString('zh-CN', {
|
||
year: 'numeric', month: '2-digit', day: '2-digit',
|
||
hour: '2-digit', minute: '2-digit', second: '2-digit'
|
||
});
|
||
const logItem = document.createElement('div');
|
||
logItem.className = 'log-item';
|
||
|
||
let logClass = '';
|
||
switch (type) {
|
||
case 'send':
|
||
logClass = 'log-send';
|
||
break;
|
||
case 'receive':
|
||
logClass = 'log-receive';
|
||
break;
|
||
case 'error':
|
||
logClass = 'log-error';
|
||
break;
|
||
default:
|
||
logClass = 'log-status';
|
||
}
|
||
|
||
logItem.innerHTML = `<span class="log-time">[${now}]</span> <span class="${logClass}">${content}</span>`;
|
||
logContainer.appendChild(logItem);
|
||
// 滚动到最新日志
|
||
logContainer.scrollTop = logContainer.scrollHeight;
|
||
}
|
||
|
||
// 工具函数:格式化JSON(便于日志显示)
|
||
function formatJson(jsonStr) {
|
||
try {
|
||
const obj = JSON.parse(jsonStr);
|
||
return JSON.stringify(obj, null, 2);
|
||
} catch (e) {
|
||
return jsonStr; // 非JSON格式直接返回
|
||
}
|
||
}
|
||
|
||
// 建立WebSocket连接
|
||
function connectWebSocket() {
|
||
if (ws) {
|
||
addLog('已存在连接,无需重复建立', 'error');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
ws = new WebSocket(wsUrl);
|
||
|
||
// 连接成功
|
||
ws.onopen = function () {
|
||
connectionStatus.className = 'status-value status-connected';
|
||
connectionStatus.textContent = '已连接';
|
||
const now = new Date().toLocaleString('zh-CN');
|
||
connectTime.textContent = now;
|
||
addLog(`连接成功!服务地址:${wsUrl}`, 'status');
|
||
|
||
// 更新按钮状态
|
||
connectBtn.disabled = true;
|
||
disconnectBtn.disabled = false;
|
||
sendMessageBtn.disabled = false;
|
||
|
||
// 开启自动心跳(默认开启)
|
||
if (autoHeartbeat.value === 'on') {
|
||
startAutoHeartbeat();
|
||
}
|
||
};
|
||
|
||
// 接收消息
|
||
ws.onmessage = function (event) {
|
||
const message = event.data;
|
||
addLog(`收到消息:\n${formatJson(message)}`, 'receive');
|
||
};
|
||
|
||
// 连接关闭
|
||
ws.onclose = function (event) {
|
||
connectionStatus.className = 'status-value status-disconnected';
|
||
connectionStatus.textContent = '已断开';
|
||
addLog(`连接断开!代码:${event.code},原因:${event.reason || '未知'}`, 'status');
|
||
|
||
// 清除自动心跳
|
||
stopAutoHeartbeat();
|
||
|
||
// 更新按钮状态
|
||
connectBtn.disabled = false;
|
||
disconnectBtn.disabled = true;
|
||
sendMessageBtn.disabled = true;
|
||
|
||
// 重置WebSocket对象
|
||
ws = null;
|
||
};
|
||
|
||
// 连接错误
|
||
ws.onerror = function (error) {
|
||
addLog(`连接错误:${error.message || '未知错误'}`, 'error');
|
||
};
|
||
|
||
} catch (e) {
|
||
addLog(`建立连接失败:${e.message}`, 'error');
|
||
ws = null;
|
||
}
|
||
}
|
||
|
||
// 断开WebSocket连接
|
||
function disconnectWebSocket() {
|
||
if (!ws) {
|
||
addLog('当前无连接,无需断开', 'error');
|
||
return;
|
||
}
|
||
|
||
ws.close(1000, '手动断开连接');
|
||
}
|
||
|
||
// 发送心跳消息(符合约定格式:{"timestamp":xxxxx, "type":"heartbeat"})
|
||
function sendHeartbeat() {
|
||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||
addLog('发送心跳失败:当前无有效连接', 'error');
|
||
return;
|
||
}
|
||
|
||
const heartbeatMsg = {
|
||
timestamp: Date.now(), // 当前毫秒时间戳
|
||
type: "heartbeat"
|
||
};
|
||
const msgStr = JSON.stringify(heartbeatMsg);
|
||
|
||
ws.send(msgStr);
|
||
addLog(`发送心跳:\n${formatJson(msgStr)}`, 'send');
|
||
}
|
||
|
||
// 开启自动心跳
|
||
function startAutoHeartbeat() {
|
||
// 先停止已有定时器
|
||
stopAutoHeartbeat();
|
||
|
||
const interval = parseInt(heartbeatInterval.value) * 1000;
|
||
if (isNaN(interval) || interval < 10000) {
|
||
addLog('自动心跳间隔无效,已重置为30秒', 'error');
|
||
heartbeatInterval.value = 30;
|
||
return startAutoHeartbeat();
|
||
}
|
||
|
||
addLog(`开启自动心跳,间隔:${heartbeatInterval.value}秒`, 'status');
|
||
heartbeatTimer = setInterval(sendHeartbeat, interval);
|
||
}
|
||
|
||
// 停止自动心跳
|
||
function stopAutoHeartbeat() {
|
||
if (heartbeatTimer) {
|
||
clearInterval(heartbeatTimer);
|
||
heartbeatTimer = null;
|
||
addLog('已停止自动心跳', 'status');
|
||
}
|
||
}
|
||
|
||
// 发送自定义消息
|
||
function sendCustomMessage() {
|
||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||
addLog('发送消息失败:当前无有效连接', 'error');
|
||
return;
|
||
}
|
||
|
||
const msgStr = messageInput.value.trim();
|
||
if (!msgStr) {
|
||
addLog('发送消息失败:消息内容不能为空', 'error');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 验证JSON格式(可选,仅提示不强制)
|
||
JSON.parse(msgStr);
|
||
ws.send(msgStr);
|
||
addLog(`发送自定义消息:\n${formatJson(msgStr)}`, 'send');
|
||
} catch (e) {
|
||
addLog(`JSON格式错误:${e.message},仍尝试发送原始内容`, 'error');
|
||
ws.send(msgStr);
|
||
addLog(`发送自定义消息(非JSON):\n${msgStr}`, 'send');
|
||
}
|
||
}
|
||
|
||
// 绑定按钮事件
|
||
connectBtn.addEventListener('click', connectWebSocket);
|
||
disconnectBtn.addEventListener('click', disconnectWebSocket);
|
||
sendMessageBtn.addEventListener('click', sendCustomMessage);
|
||
sendHeartbeatBtn.addEventListener('click', sendHeartbeat);
|
||
clearLogBtn.addEventListener('click', () => {
|
||
logContainer.innerHTML = '<div class="log-item"><span class="log-time">[日志已清空]</span> 请继续操作...</div>';
|
||
});
|
||
|
||
// 自动心跳开关变更事件
|
||
autoHeartbeat.addEventListener('change', function () {
|
||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||
if (this.value === 'on') {
|
||
startAutoHeartbeat();
|
||
} else {
|
||
stopAutoHeartbeat();
|
||
}
|
||
} else {
|
||
addLog('需先建立有效连接才能控制自动心跳', 'error');
|
||
// 重置选择
|
||
this.value = 'off';
|
||
}
|
||
});
|
||
|
||
// 心跳间隔变更事件(实时生效)
|
||
heartbeatInterval.addEventListener('change', function () {
|
||
if (autoHeartbeat.value === 'on' && ws && ws.readyState === WebSocket.OPEN) {
|
||
startAutoHeartbeat();
|
||
}
|
||
});
|
||
|
||
// 快捷键支持(Ctrl+Enter发送消息)
|
||
messageInput.addEventListener('keydown', function (e) {
|
||
if (e.ctrlKey && e.key === 'Enter') {
|
||
sendCustomMessage();
|
||
e.preventDefault();
|
||
}
|
||
});
|
||
</script>
|
||
</body>
|
||
</html> |