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> |