Files
workspace/code/link/link-backend/target/classes/test.html
T
2025-10-11 17:31:56 +08:00

289 lines
11 KiB
HTML

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>WebSocket Demo — Netty</title>
<style>
:root{--bg:#f6f8fa;--card:#ffffff;--muted:#6b7280;--accent:#2563eb}
body{font-family:system-ui,-apple-system,Segoe UI,Roboto,"Helvetica Neue",Arial; background:var(--bg); margin:0;padding:24px;color:#0f172a}
.wrap{max-width:980px;margin:0 auto}
h1{font-size:20px;margin:0 0 16px}
.card{background:var(--card);padding:14px;border-radius:10px;box-shadow:0 1px 4px rgba(2,6,23,0.06);margin-bottom:12px}
.row{display:flex;gap:8px;align-items:center}
input[type="text"], input[type="number"], textarea, select {
padding:8px;border:1px solid #e6e9ef;border-radius:6px;font-size:13px;
}
button{background:var(--accent);color:#fff;border:0;padding:8px 12px;border-radius:8px;cursor:pointer}
button.secondary{background:#e6eefc;color:#08306b}
button.warn{background:#ef4444}
.controls{display:flex;gap:8px;flex-wrap:wrap}
.col{flex:1;min-width:180px}
.log{height:320px;overflow:auto;padding:8px;border:1px solid #eef2ff;border-radius:6px;background:#fbfdff;font-family:monospace;font-size:13px}
.msg-in{color:#0b5; margin-bottom:6px}
.msg-out{color:#2563eb; margin-bottom:6px}
.meta{color:var(--muted);font-size:12px}
label{font-size:13px;color:#111827}
.small{font-size:12px;color:var(--muted)}
.flex-between{display:flex;justify-content:space-between;align-items:center}
.right{margin-left:auto}
.footer{font-size:12px;color:var(--muted);margin-top:8px}
</style>
</head>
<body>
<div class="wrap">
<h1>WebSocket Demo — Netty</h1>
<div class="card">
<div class="row" style="gap:12px;">
<div class="col">
<label>WebSocket URL</label>
<input id="url" type="text" value="ws://localhost:8090/ws" />
</div>
<div style="width:120px">
<label>Heartbeat (s)</label>
<input id="heartbeatSec" type="number" min="0" value="0" />
</div>
<div style="width:160px">
<label>Auto reconnect</label>
<select id="autoReconnect">
<option value="0">关闭</option>
<option value="2000">2s</option>
<option value="5000" selected>5s</option>
<option value="10000">10s</option>
</select>
</div>
<div style="width:140px">
<label>JSON mode</label>
<select id="jsonMode">
<option value="false" selected>文本</option>
<option value="true">JSON (send as {"msg":...})</option>
</select>
</div>
</div>
<div class="row" style="margin-top:10px;">
<div class="controls">
<button id="btnConnect">Connect</button>
<button id="btnDisconnect" class="secondary" disabled>Disconnect</button>
<button id="btnClear" class="secondary">Clear Log</button>
</div>
<div class="right small meta" id="status">状态: <strong>未连接</strong></div>
</div>
</div>
<div class="card">
<div class="row" style="gap:10px;margin-bottom:8px">
<div style="flex:1">
<label>Message</label>
<textarea id="message" rows="3" style="width:100%"></textarea>
</div>
<div style="width:180px">
<label>Target (可选)</label>
<input id="target" type="text" placeholder="目标ID (若服务端支持)" />
</div>
</div>
<div class="row" style="margin-top:8px">
<button id="btnSend">Send</button>
<button id="btnSendJson" class="secondary">Send Example JSON</button>
<div class="right small meta">Sends: <span id="sentCount">0</span>, Received: <span id="recvCount">0</span></div>
</div>
</div>
<div class="card">
<div class="flex-between" style="margin-bottom:8px">
<div><strong>Message Log</strong></div>
<div class="small meta">Time is local browser time</div>
</div>
<div id="log" class="log"></div>
<div class="footer small">提示: 如果你的 Netty 服务要求子协议或 token,请把 URL 改为类似: <code>ws://host:port/ws?token=xxx</code></div>
</div>
</div>
<script>
(function () {
let ws = null;
let reconnectTimer = null;
let sent = 0, recv = 0;
let heartbeatTimer = null;
const $ = id => document.getElementById(id);
const logEl = $('log');
const statusEl = $('status');
const sentCountEl = $('sentCount');
const recvCountEl = $('recvCount');
function log(msg, type='info') {
const time = new Date().toLocaleTimeString();
const div = document.createElement('div');
div.className = type === 'in' ? 'msg-in' : (type === 'out' ? 'msg-out' : '');
div.innerHTML = `<span class="small meta">[${time}]</span> ${escapeHtml(msg)}`;
logEl.appendChild(div);
logEl.scrollTop = logEl.scrollHeight;
}
function setStatus(text, color) {
statusEl.innerHTML = '状态: <strong style="color:' + (color||'#111') + '">' + text + '</strong>';
}
function escapeHtml(s) {
return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
function connect() {
const url = $('url').value.trim();
if (!url) { alert('请输入 WebSocket URL'); return; }
try {
ws = new WebSocket(url);
} catch (e) {
log('创建 WebSocket 失败: ' + e.message);
return;
}
setStatus('连接中...', '#f59e0b');
$('btnConnect').disabled = true;
$('btnDisconnect').disabled = false;
ws.onopen = () => {
setStatus('已连接', '#10b981');
log('CONNECTED → ' + url);
if (heartbeatEnabled()) startHeartbeat();
};
ws.onmessage = (evt) => {
recv++;
recvCountEl.textContent = recv;
log(evt.data, 'in');
};
ws.onclose = (evt) => {
setStatus('已断开', '#ef4444');
log('CLOSED (' + (evt.code || '') + ')');
stopHeartbeat();
if (shouldReconnect()) scheduleReconnect();
$('btnConnect').disabled = false;
$('btnDisconnect').disabled = true;
};
ws.onerror = (err) => {
log('ERROR: ' + (err.message || JSON.stringify(err)));
};
}
function disconnect() {
if (ws) {
stopReconnect();
ws.close();
ws = null;
setStatus('已断开', '#ef4444');
$('btnConnect').disabled = false;
$('btnDisconnect').disabled = true;
}
}
function sendMessage(payload) {
if (!ws || ws.readyState !== WebSocket.OPEN) {
alert('WebSocket 未连接');
return;
}
ws.send(payload);
sent++;
sentCountEl.textContent = sent;
log(payload, 'out');
}
function startHeartbeat() {
stopHeartbeat();
const sec = parseInt($('heartbeatSec').value, 10);
if (!sec || sec <= 0) return;
heartbeatTimer = setInterval(() => {
if (ws && ws.readyState === WebSocket.OPEN) {
const hb = JSON.stringify({type:'ping', ts: Date.now()});
ws.send(hb);
log('(heartbeat) ' + hb, 'out');
}
}, sec * 1000);
}
function stopHeartbeat() {
if (heartbeatTimer) { clearInterval(heartbeatTimer); heartbeatTimer = null; }
}
function heartbeatEnabled() {
const sec = parseInt($('heartbeatSec').value, 10);
return sec && sec > 0;
}
function shouldReconnect() {
return parseInt($('autoReconnect').value, 10) > 0;
}
function scheduleReconnect() {
stopReconnect();
const delay = parseInt($('autoReconnect').value, 10);
if (!delay) return;
log('尝试在 ' + (delay/1000) + 's 后重连...');
reconnectTimer = setTimeout(() => {
log('重连中...');
connect();
}, delay);
}
function stopReconnect() {
if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
}
function clearLog() {
logEl.innerHTML = '';
}
// events
$('btnConnect').addEventListener('click', () => { connect(); });
$('btnDisconnect').addEventListener('click', () => { disconnect(); });
$('btnClear').addEventListener('click', () => { clearLog(); });
$('btnSend').addEventListener('click', () => {
const jsonMode = $('jsonMode').value === 'true';
const target = $('target').value.trim();
const text = $('message').value;
if (!text) return alert('请输入要发送的消息');
if (jsonMode) {
const obj = { type: 'message', msg: text };
if (target) obj.to = target;
sendMessage(JSON.stringify(obj));
} else {
// 如果有 target,也拼一个简单的前缀,取决于服务端如何解析
if (target) {
// 约定: "to:TARGET|payload"
sendMessage('to:' + target + '|' + text);
} else {
sendMessage(text);
}
}
});
$('btnSendJson').addEventListener('click', () => {
const sample = { type: 'message', msg: 'hello from web demo', ts: Date.now() };
$('message').value = JSON.stringify(sample, null, 2);
});
// close on page unload
window.addEventListener('beforeunload', () => {
try { if (ws) ws.close(); } catch(e){ }
});
// helper: update heartbeat when changed
$('heartbeatSec').addEventListener('change', () => {
if (ws && ws.readyState === WebSocket.OPEN) {
if (heartbeatEnabled()) startHeartbeat(); else stopHeartbeat();
}
});
})();
</script>
</body>
</html>