ws-client.html
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>WebSocket Test Client</title>
<style>
/* базовые стили контролов (без оформления body/html, чтобы не конфликтовать с Bitrix) */
input,
textarea,
select,
button {
font-family: inherit;
}
input[type="text"],
textarea,
select,
input[type="number"] {
width: 100%;
border: 1px solid #ccc;
border-radius: 6px;
padding: 8px 10px;
font-size: 14px;
box-sizing: border-box;
}
input:focus,
textarea:focus,
select:focus {
outline: none;
border-color: #2684ff;
box-shadow: 0 0 0 3px rgba(38, 132, 255, 0.2);
}
button {
border: 1px solid #999;
border-radius: 6px;
padding: 8px 14px;
font-size: 14px;
cursor: pointer;
background: #f2f2f2;
transition: background 0.2s, border-color 0.2s;
}
button:hover {
background: #e6e6e6;
}
button.primary {
background: #2684ff;
color: white;
border-color: #0f5ad3;
}
button.primary:hover {
background: #0f5ad3;
}
button.danger {
background: #e74c3c;
color: white;
border-color: #c0392b;
}
button.danger:hover {
background: #c0392b;
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.panel {
border: 1px solid #ddd;
border-radius: 8px;
padding: 1rem;
margin-bottom: 1rem;
background: #fff;
}
.controls {
display: flex;
gap: .5rem;
flex-wrap: wrap;
margin-top: .5rem;
}
.row {
display: flex;
gap: .5rem;
align-items: center;
flex-wrap: wrap;
margin-top: .5rem;
}
.muted {
opacity: .75;
}
#log {
font-family: monospace;
background: #f9f9f9;
border: 1px solid #ccc;
border-radius: 6px;
padding: .5rem;
height: 300px;
overflow-y: auto;
white-space: pre-wrap;
}
/* заголовок со статусом справа */
.header {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 12px;
margin-bottom: 8px;
}
.header h2 {
margin: 0;
}
/* бейдж статуса */
.status {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
border-radius: 6px;
border: 1px solid #ccc;
background: #f9f9f9;
font-weight: 600;
font-size: 14px;
}
.status .dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: #999;
display: inline-block;
}
.status.connected .dot {
background: #2ecc71;
}
.status.connecting .dot {
background: #f1c40f;
}
.status.disconnected .dot {
background: #e74c3c;
}
</style>
</head>
<body>
<div class="header">
<h2>WebSocket Test Client</h2>
<div id="status" class="status disconnected" title="Статус соединения">
<span class="dot"></span>
<span id="state">DISCONNECTED</span>
</div>
</div>
<section class="panel">
<label>URL WebSocket:</label>
<input id="url" type="text" value="wss://domain.ru/services/main_ws_service?X-StatefulSocketId=123" />
<label style="margin-top:8px;">Субпротоколы (через запятую):</label>
<input id="protocols" type="text" placeholder="Например: json, chat, mqtt" />
<div class="controls">
<button id="connect" class="primary">Подключиться</button>
<button id="disconnect" class="danger" disabled>Отключиться</button>
</div>
</section>
<!-- Блок ping/pong -->
<section class="panel">
<label><input id="enablePing" type="checkbox" checked /> Включить ping</label>
<div class="row">
<label for="pingInterval">Интервал (сек):</label>
<input id="pingInterval" type="number" min="5" step="1" value="30" style="max-width:120px" />
<button id="sendPing">Отправить ping сейчас</button>
</div>
<div class="row muted">
<div>последний ping: <span id="lastPing">—</span></div>
<div>•</div>
<div>последний pong: <span id="lastPong">—</span></div>
</div>
</section>
<section class="panel">
<label>Сообщение:</label>
<textarea id="payload" placeholder="Введите сообщение..."></textarea>
<div class="controls">
<button id="sendText" class="primary">Отправить</button>
<button id="clear">Очистить лог</button>
</div>
</section>
<section class="panel">
<h3>Журнал</h3>
<div id="log"></div>
</section>
<script>
(() => {
const url = document.getElementById('url');
const protocols = document.getElementById('protocols');
const connectBtn = document.getElementById('connect');
const disconnectBtn = document.getElementById('disconnect');
const payload = document.getElementById('payload');
const sendText = document.getElementById('sendText');
const clearBtn = document.getElementById('clear');
const log = document.getElementById('log');
const stateEl = document.getElementById('state');
const statusBox = document.getElementById('status');
// ping/pong UI
const enablePing = document.getElementById('enablePing');
const pingIntervalInput = document.getElementById('pingInterval');
const sendPingBtn = document.getElementById('sendPing');
const lastPingEl = document.getElementById('lastPing');
const lastPongEl = document.getElementById('lastPong');
let ws = null;
let pingTimer = null;
const now = () => new Date().toLocaleTimeString();
function logMsg(msg, type = 'info') {
const el = document.createElement('div');
el.textContent = `[${now()}] ${msg}`;
if (type === 'error') el.style.color = 'red';
if (type === 'recv') el.style.color = 'blue';
log.appendChild(el);
log.scrollTop = log.scrollHeight;
// console mirror
if (type === 'error') console.error(msg); else console.log(msg);
}
function setState(s) {
stateEl.textContent = s;
statusBox.classList.remove('connected', 'connecting', 'disconnected');
if (s === 'CONNECTED') statusBox.classList.add('connected');
else if (s === 'CONNECTING') statusBox.classList.add('connecting');
else statusBox.classList.add('disconnected');
}
function clearHeartbeat() {
if (pingTimer) {
clearInterval(pingTimer);
pingTimer = null;
}
}
function startHeartbeat() {
clearHeartbeat();
if (!enablePing.checked) return;
let interval = Number(pingIntervalInput.value);
if (!Number.isFinite(interval) || interval < 5) interval = 30;
pingTimer = setInterval(() => {
sendPing();
}, interval * 1000);
}
function sendPing() {
if (!ws || ws.readyState !== WebSocket.OPEN) return;
try {
// application-level ping
const frame = { type: 'ping', t: Date.now() };
ws.send(JSON.stringify(frame));
lastPingEl.textContent = now();
logMsg('→ ping ' + JSON.stringify(frame));
} catch (e) {
logMsg('Ошибка при ping: ' + e.message, 'error');
}
}
function maybeAutoPong(data) {
// Автоответ на ping от сервера (если сервер шлёт application-level ping)
try {
const obj = JSON.parse(data);
if (obj && obj.type === 'ping') {
ws && ws.send(JSON.stringify({ type: 'pong', t: obj.t ?? Date.now() }));
logMsg('← ping (сервер) • → pong (клиент)');
return true;
}
if (obj && obj.type === 'pong') {
lastPongEl.textContent = now();
logMsg('← pong');
return true;
}
} catch (_) {
// не JSON — попробуем распознать простые строки
if (typeof data === 'string') {
const s = data.trim().toLowerCase();
if (s === 'ping') {
ws && ws.send('pong');
logMsg('← "ping" • → "pong"');
return true;
}
if (s === 'pong') {
lastPongEl.textContent = now();
logMsg('← "pong"');
return true;
}
}
}
return false;
}
connectBtn.onclick = () => {
if (ws) ws.close();
const protoList = protocols.value.split(',').map(p => p.trim()).filter(Boolean);
try {
ws = protoList.length ? new WebSocket(url.value, protoList) : new WebSocket(url.value);
} catch (e) {
logMsg('Ошибка: ' + e.message, 'error');
return;
}
setState('CONNECTING');
ws.onopen = () => {
setState('CONNECTED');
logMsg('Соединение установлено');
connectBtn.disabled = true;
disconnectBtn.disabled = false;
startHeartbeat();
};
ws.onmessage = e => {
// если это ping/pong — обработаем, иначе покажем как есть
if (!maybeAutoPong(e.data)) {
logMsg('Получено: ' + e.data, 'recv');
}
};
ws.onerror = e => logMsg('Ошибка WebSocket', 'error');
ws.onclose = e => {
setState('DISCONNECTED');
logMsg(`Закрыто: code=${e.code}, reason=${e.reason}`);
connectBtn.disabled = false;
disconnectBtn.disabled = true;
clearHeartbeat();
};
};
disconnectBtn.onclick = () => { if (ws) ws.close(1000, 'manual'); };
sendText.onclick = () => {
if (!ws || ws.readyState !== WebSocket.OPEN) { logMsg('Нет активного соединения', 'error'); return; }
ws.send(payload.value);
logMsg('Отправлено: ' + payload.value);
};
clearBtn.onclick = () => { log.innerHTML = ''; };
// Управление пингом
enablePing.addEventListener('change', () => {
if (ws && ws.readyState === WebSocket.OPEN) startHeartbeat();
});
pingIntervalInput.addEventListener('change', () => {
if (ws && ws.readyState === WebSocket.OPEN) startHeartbeat();
});
sendPingBtn.addEventListener('click', sendPing);
})();
</script>
</body>
</html>