Видеозвонок постоянно "заикается"? В онлайн-игре персонаж замирает, а потом "телепортируется" через полкарты? Чаще всего мы виним в этом высокий "пинг", но на самом деле за качественную связь в реальном времени отвечают два других, более важных параметра: джиттер (jitter) и потеря пакетов (packet loss).
В этой статье мы разберемся, что это такое, почему они важнее пинга для видео, игр и VoIP, и создадим собственную систему мониторинга этих параметров с помощью Node.js и VizIoT.
Все данные в интернете передаются небольшими порциями — пакетами. Пинг (Ping, RTT) — это время, за которое один пакет доходит до сервера и возвращается обратно. Но что, если пакеты приходят с разной задержкой?
Джиттер (Jitter) — это неравномерность задержки пакетов. Проще говоря, это "дрожание" пинга.
Аналогия: Если пинг — это среднее время в пути для автобуса по маршруту, то джиттер — это то, насколько сильно он отклоняется от расписания. Автобус, который всегда приезжает с опозданием на 5 минут, лучше, чем тот, который приезжает то на 10 минут раньше, то на 20 минут позже.
Потеря пакетов (Packet Loss) — это процент пакетов, которые так и не дошли до получателя. Они просто теряются по пути из-за перегрузок сети, плохого оборудования или помех.
Для приложений реального времени, таких как видеозвонок или игра, потерянный пакет — это катастрофа. Система не может ждать его вечно и вынуждена либо проигнорировать этот фрагмент данных (что приводит к "выпадению" звука или "замиранию" картинки), либо пытаться угадать, что в нём было.
Медленный ответ (высокий пинг) почти всегда лучше, чем отсутствие ответа (потеря пакетов).
Мы создадим систему, которая будет постоянно отправлять тестовые пакеты данных и анализировать, как они возвращаются. Результаты будут отображаться на интерактивном виджете в VizIoT, который покажет вам полную картину стабильности вашего соединения.
Наше приложение будет состоять из двух частей:
Клиент, получая пакеты обратно, измеряет время их возвращения, сравнивает задержки между пакетами (вычисляя джиттер) и считает, сколько пакетов не вернулось (потеря пакетов).
В качестве сервера вы можете использовать:
app.viziot.com:54321. Пример по умолчанию настроен на него.Наш скрипт написан на Node.js. Мы будем работать в Linux (например, Ubuntu/Debian), но вы можете адаптировать это для любой ОС.
Рекомендуется устанавливать Node.js через NVM (менеджер версий).
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
\. "$HOME/.nvm/nvm.sh"
nvm install 24
node -v
npm -v
mkdir viziot_udp_jitter
cd viziot_udp_jitter/
npm init -y
npm i viziot-mqtt-client-nodejs
nano client.js
client.js)Вставьте следующий код. Не забудьте указать ключ и пароль вашего устройства VizIoT. Если вы еще не знаете как работает VizIoT советуем ознакомится с этой статьей Знакомство с VizIoT.
// client.js
const { fork } = require('child_process');
const VizIoTMQTT = require('viziot-mqtt-client-nodejs');
// ---------------------------------------
// Настройки
// ---------------------------------------
// 1. Введите ключ и пароль вашего устройства VizIoT
const keyDevice = '____________';
const passDevice = '_________________';
// Данные сервера (публичный сервер VizIoT)
const HOST = 'app.viziot.com';
const PORT = 54321;
// Настройки теста
const TEST_INTERVAL = 30000; // Интервал запуска теста (30 секунд)
const COUNT_PACKETS = 500; // Количество пакетов в одном тесте
const PACKET_INTERVAL = 10; // Задержка между отправкой пакетов (10 мс)
// ---------------------------------------
// VizIoT MQTT
// ---------------------------------------
let isConnected = false;
const viziotMQTTClient = new VizIoTMQTT(keyDevice, passDevice);
viziotMQTTClient.connect(() => {
isConnected = true;
console.log("Успешно подключено к VizIoT MQTT.");
});
// ---------------------------------------
// Логика работы
// ---------------------------------------
if (COUNT_PACKETS * PACKET_INTERVAL > TEST_INTERVAL * 0.8) {
console.error('Ошибка конфигурации: время теста превышает интервал между тестами!');
process.exit(1);
}
function runTestCycle() {
console.log(`\n▶ Запуск нового теста (пакетов: ${COUNT_PACKETS})`);
worker.send({ cmd: 'startTest' });
let seq = 0;
const sendInterval = setInterval(() => {
if (seq >= COUNT_PACKETS) {
clearInterval(sendInterval);
setTimeout(() => {
worker.send({ cmd: 'stopTest' });
}, 2000); // Даем 2 секунды на возврат последних пакетов
return;
}
worker.send({
cmd: 'sendPacket',
idBuffer: Buffer.from(keyDevice, 'utf8'),
seq: seq++,
});
}, PACKET_INTERVAL);
}
function showResults(stats) {
const avgPing = stats.rtts.length ? (stats.rtts.reduce((a, b) => a + b, 0) / stats.rtts.length).toFixed(3) : 0;
const minPing = stats.rtts.length ? Math.min(...stats.rtts).toFixed(3) : 0;
const maxPing = stats.rtts.length ? Math.max(...stats.rtts).toFixed(3) : 0;
const avgJitter = stats.jitters.length ? (stats.jitters.reduce((a, b) => a + b, 0) / stats.jitters.length).toFixed(3) : 0;
const lossPercent = stats.sent > 0 ? (((stats.sent - stats.received) / stats.sent) * 100).toFixed(2) : 0;
console.log(`
📊 ОТЧЕТ
Отправлено: ${stats.sent}
Получено: ${stats.received}
Потери: ${lossPercent}%
Ping (ср): ${avgPing} мс
Ping (мин): ${minPing} мс
Ping (макс):${maxPing} мс
Jitter (ср):${avgJitter} мс
`);
let packet = {
date: Math.floor(Date.now() / 1000),
sent: stats.sent,
recv: stats.received,
loss_perc: lossPercent,
ping_avg: avgPing,
ping_min: minPing,
ping_max: maxPing,
jitter_avg: avgJitter,
};
// Отправка в VizIoT
if (isConnected) {
viziotMQTTClient.sendDataToVizIoT(packet, (err) => {
if (err) console.log('Ошибка отправки в VizIoT:', err);
});
}
}
// Работа с UDP вынесена в отдельный поток для точности измерений
const worker = fork('./udp_worker.js');
worker.on('message', (msg) => {
switch (msg.cmd) {
case 'ready':
worker.send({ cmd: 'config', host: HOST, port: PORT });
break;
case 'configured':
runTestCycle(); // Первый тест сразу при запуске
setInterval(runTestCycle, TEST_INTERVAL); // Запускаем цикл тестов
break;
case 'report':
showResults(msg.stats);
break;
}
});
udp_worker.js)Чтобы отправка и приём пакетов не влияли на точность таймеров, мы вынесем всю работу с сетью в отдельный процесс.
nano udp_worker.js
Вставьте в него этот код:
// udp_worker.js
const dgram = require('dgram');
const socket = dgram.createSocket('udp4');
let SERVER_HOST = null;
let SERVER_PORT = null;
let isTesting = false;
let stats = {};
function resetStats() {
stats = {
sent: 0,
received: 0,
rtts: [],
jitters: [],
lastRtt: null,
};
}
socket.on('message', (msg) => {
if (!isTesting || msg.length < 28) return;
const recvTime = process.hrtime.bigint();
try {
const msgSendTime = msg.readBigUInt64BE(20);
const rtt = Number(recvTime - msgSendTime) / 1e6; // в миллисекундах
stats.rtts.push(rtt);
stats.received++;
if (stats.lastRtt !== null) {
stats.jitters.push(Math.abs(rtt - stats.lastRtt));
}
stats.lastRtt = rtt;
} catch (e) {
console.error('Ошибка парсинга пакета', e);
}
});
process.on('message', (msg) => {
switch (msg.cmd) {
case 'config':
SERVER_HOST = msg.host;
SERVER_PORT = msg.port;
process.send({ cmd: 'configured' });
break;
case 'startTest':
resetStats();
isTesting = true;
break;
case 'sendPacket': {
const { idBuffer, seq } = msg;
const buf = Buffer.allocUnsafe(28);
idBuffer.copy(buf, 0);
buf.writeUInt32BE(seq, 16);
buf.writeBigUInt64BE(process.hrtime.bigint(), 20);
socket.send(buf, SERVER_PORT, SERVER_HOST, (err) => {
if (!err) stats.sent++;
});
break;
}
case 'stopTest':
isTesting = false;
process.send({ cmd: 'report', stats });
break;
}
});
socket.bind(() => {
process.send({ cmd: 'ready' });
});
Чтобы скрипт работал 24/7, используем менеджер процессов PM2.
npm install pm2 -gpm2 start client.js --name "jitter-monitor"pm2 startup (и выполните команду, которую он предложит)pm2 saveЕсли вы хотите измерять качество связи между двумя своими устройствами (например, до VPS), запустите на втором устройстве этот простой эхо-сервер.
# На сервере
sudo ufw allow 54321/udp
mkdir udp_server && cd udp_server
npm init -y
nano server.js
Код для server.js:
const dgram = require('dgram');
const server = dgram.createSocket('udp4');
const PORT = 54321;
server.on('message', (msg, rinfo) => {
// Просто отправляем пакет обратно
server.send(msg, rinfo.port, rinfo.address);
});
server.bind(PORT, () => {
console.log(`UDP Эхо-сервер запущен на порту ${PORT}`);
});
Запустите его также через PM2: pm2 start server.js --name "udp-echo-server". Не забудьте поменять HOST и PORT в client.js.
После первого запуска скрипта данные начнут поступать в ваше устройство VizIoT. Осталось создать красивый виджет.
Создайте виджет типа "График" и добавьте на него параметры типа линия jitter_avg, ping_min, ping_max, ping_avg. А для параметра loss_perc отлично подойдет тип бар.
Самое важное — получать уведомления о проблемах. Создайте правило в разделе "Уведомления": если loss_perc больше (>) 5 , отправить уведомление в Telegram. Так вы узнаете о проблемах со связью раньше, чем вам начнут жаловаться пользователи.
Теперь у вас есть не просто "измеритель пинга", а профессиональный инструмент для анализа качества и стабильности вашего сетевого соединения.