u
This commit is contained in:
1 parent
2db3eb2408
commit
76957e637d
179 files changed
+11901
-2178
No files matched your search
@@ -0,0 +1,288 @@
|
||||
<!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>
|
||||
Reference in new issue
Block a user