289 lines
11 KiB
HTML
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,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
}
|
|
|
|
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>
|