В предыдущей статье, мы описали процесс установки и базовой настройки службы WebSocket Server.
Служба имеет большие возможности, и в этом руководстве мы рассмотрим некоторые из них. В предыдущей статье мы показали лишь базовую конфигурацию, когда мы обращаемся к скрипту и непрерывно передаем данные через WebSocket. Такой подход не всегда эффективен, когда речь идет о настройке продакшн сервера.
1. Передача данных только если они изменились
В этом коде отправка данных клиентам происходит только если JSON-строка новых данных отличается от предыдущей.
const newJSON = JSON.stringify(live);
if (newJSON !== previousLiveJSON) {
previousLiveJSON = newJSON;
latestLiveData = live;
broadcast();
console.log(`Обновлены Live данные (${live.length} визитов)`);
}
Такой подход существенно экономит ресурсы клиента, особенно когда данные передаются на микроконтроллер, в таких случаях это существенно снижает нагрузку.
2. Объединение двух запросов в один
Вы можете построить код таким образом, что оба запроса будут выполняться одновременно и передавать необходимые данные, в данном коде мы получаем 2 разных типа данных: Live и Summary
/root/websocket-server/server.js
import express from "express";
import { WebSocketServer } from "ws";
import fetch from "node-fetch";
import https from "https";
import fs from "fs";
import url from "url";
import dotenv from "dotenv";
dotenv.config();
const PORT = process.env.PORT || 8080;
const MATOMO_URL = process.env.MATOMO_URL;
const MATOMO_TOKEN = process.env.MATOMO_TOKEN;
const ID_SITE = process.env.MATOMO_SITE_ID || 1;
const FETCH_INTERVAL = parseInt(process.env.FETCH_INTERVAL) || 1000;
const VALID_TOKENS = process.env.VALID_TOKENS.split(",").map(t => t.trim());
const SSL_KEY = process.env.SSL_KEY;
const SSL_CERT = process.env.SSL_CERT;
const TIMEZONE_OFFSET = 3; // твой +3 часовой пояс
const options = {
key: fs.readFileSync(SSL_KEY),
cert: fs.readFileSync(SSL_CERT)
};
const app = express();
const server = https.createServer(options, app);
const wss = new WebSocketServer({ server });
// === Глобальные переменные ===
let latestLiveData = [];
let latestSummary = {};
let previousLiveJSON = "";
let previousSummaryJSON = "";
// ===== Получаем Live статистику (последние визиты) =====
async function fetchMatomoLive() {
const apiUrl = `${MATOMO_URL}index.php?module=API&method=Live.getLastVisitsDetails&format=JSON&period=day&date=today&idSite=${ID_SITE}`;
const res = await fetch(apiUrl, {
method: "POST",
body: new URLSearchParams({
token_auth: MATOMO_TOKEN,
filter_limit: "5",
expanded: "1"
}),
headers: { "Content-Type": "application/x-www-form-urlencoded" }
});
const data = await res.json();
const visits = [];
if (Array.isArray(data)) {
for (const visit of data) {
if (
visit.latitude &&
visit.longitude &&
!isNaN(visit.latitude) &&
!isNaN(visit.longitude)
) {
let pageTitle = "";
if (Array.isArray(visit.actionDetails)) {
for (const action of visit.actionDetails) {
if (action.pageTitle) {
pageTitle = action.pageTitle;
break;
}
}
}
let localTime = "";
if (visit.lastActionDateTime) {
try {
const date = new Date(visit.lastActionDateTime.replace(" ", "T") + "Z");
const shifted = new Date(date.getTime() + TIMEZONE_OFFSET * 3600000);
localTime = shifted.toISOString().replace("T", " ").substring(0, 19);
} catch {
localTime = visit.lastActionDateTime;
}
}
visits.push({
latitude: parseFloat(visit.latitude),
longitude: parseFloat(visit.longitude),
city: visit.city || "",
countryCode: visit.countryCode || "",
country: visit.country || "",
actions: visit.actions || "",
visitIp: visit.visitIp || "",
operatingSystem: visit.operatingSystem || "",
browserName: visit.browserName || "",
source: visit.referrerName || "",
lastActionDateTime: localTime
});
}
}
}
return visits;
}
// ===== Получаем общую статистику (VisitsSummary) =====
async function fetchSummaryData() {
async function getMetric(method, date) {
const url = `${MATOMO_URL}index.php?module=API&method=${method}&format=JSON&period=day&date=${date}&idSite=${ID_SITE}`;
const res = await fetch(url, {
method: "POST",
body: new URLSearchParams({ token_auth: MATOMO_TOKEN }),
headers: { "Content-Type": "application/x-www-form-urlencoded" }
});
const data = await res.json();
return data?.value ? parseInt(data.value) : 0;
}
async function getDayData(date) {
const visits = await getMetric("VisitsSummary.getVisits", date);
const timeSec = await getMetric("VisitsSummary.getSumVisitsLength", date);
const h = Math.floor(timeSec / 3600);
const m = Math.floor((timeSec % 3600) / 60);
const s = timeSec % 60;
const formattedTime = `${h.toString().padStart(2, "0")}:${m
.toString()
.padStart(2, "0")}:${s.toString().padStart(2, "0")}`;
return { visits, time_spent: formattedTime };
}
const today = new Date(Date.now() + TIMEZONE_OFFSET * 3600000);
const yesterday = new Date(today.getTime() - 86400000);
const dayBefore = new Date(today.getTime() - 2 * 86400000);
function fmt(d) {
return d.toISOString().substring(0, 10);
}
const [todayStats, yStats, dbStats] = await Promise.all([
getDayData("today"),
getDayData(fmt(yesterday)),
getDayData(fmt(dayBefore))
]);
return {
today: todayStats,
yesterday: yStats,
day_before: dbStats
};
}
// ===== Основная функция обновления =====
async function fetchMatomoData() {
try {
const [live, summary] = await Promise.all([
fetchMatomoLive(),
fetchSummaryData()
]);
// --- Проверка изменений ---
const newLiveJSON = JSON.stringify(live);
const newSummaryJSON = JSON.stringify(summary);
const liveChanged = newLiveJSON !== previousLiveJSON;
const summaryChanged = newSummaryJSON !== previousSummaryJSON;
// --- Обновляем кэши ---
if (liveChanged) {
previousLiveJSON = newLiveJSON;
latestLiveData = live;
console.log(`Live обновлён (${live.length} визитов)`);
} else {
console.log("⏸ Live без изменений");
}
if (summaryChanged) {
previousSummaryJSON = newSummaryJSON;
latestSummary = summary;
console.log("Summary обновлён");
} else {
console.log("⏸ Summary без изменений");
}
// --- Отправляем клиентам только если есть изменения ---
if (liveChanged || summaryChanged) {
const combined = JSON.stringify({
visits_live: latestLiveData,
summary: latestSummary
});
wss.clients.forEach(client => {
if (client.readyState === 1) {
client.send(combined);
}
});
console.log("Отправлены обновлённые данные клиентам");
} else {
console.log("Изменений нет — отправка пропущена");
}
} catch (err) {
console.error("Ошибка получения данных:", err.message);
}
}
// ===== Запускаем цикл =====
setInterval(fetchMatomoData, FETCH_INTERVAL);
fetchMatomoData();
// ===== WebSocket =====
wss.on("connection", (ws, req) => {
const query = url.parse(req.url, true).query;
const token = query.token;
if (!VALID_TOKENS.includes(token)) {
console.log(`Отклонено, подключение с токеном: ${token}`);
ws.close(4001, "Invalid token");
return;
}
console.log(`Клиент подключен (${token})`);
if (latestLiveData.length > 0 || Object.keys(latestSummary).length > 0) {
ws.send(JSON.stringify({ visits_live: latestLiveData, summary: latestSummary }));
}
ws.on("close", () => {
console.log(`Клиент ${token} отключился`);
});
});
// ===== HTTP Debug =====
app.get("/debug", (req, res) => {
res.setHeader("Content-Type", "application/json; charset=utf-8");
res.send(
JSON.stringify({ visits_live: latestLiveData, summary: latestSummary }, null, 2)
);
});
server.listen(PORT, () => {
console.log(`HTTPS сервер запущен на порту ${PORT}`);
});
В данном примере мы напрямую запрашиваем данные по двум разным API запросам и передаем их в одном ответе, минуя PHP скрипты.
3. Безопасная настройка авторизации
В коде выше используются переменные, в которых хранятся данные авторизации, для этого необходимо установить пакет:
npm install dotenv
Файл конфигурации выглядит так:
nano /root/websocket-server/.env
# === Конфигурация API ===
MATOMO_URL=https://domain.tld/path/
MATOMO_TOKEN=YOUR_API_KEY
MATOMO_SITE_ID=1
# === Конфигурация сервера ===
PORT=8080
FETCH_INTERVAL=1000
# === WebSocket доступы ===
VALID_TOKENS=WSS_CLIENT_TOKEN_1,WSS_CLIENT_TOKEN_1
# === Пути к SSL ===
SSL_KEY=/etc/letsencrypt/live/wss.domain.tld/privkey.pem
SSL_CERT=/etc/letsencrypt/live/wss.domain.tld/fullchain.pem
Как Вы уже заметили, авторизация происходит как к обращению по API так и для клиентов, при подключении к WebSocket серверу.
Для подключения к WSS, используйте адрес с токеном
/?token=WSS_CLIENT_TOKEN_1
4. Регулярность запросов данных по API
Настройка производится в строке:
const FETCH_INTERVAL = parseInt(process.env.FETCH_INTERVAL) || 1000;
Параметр указывается в миллисекундах, где 1000 = 1 секунде.
Для 10 секунд будет выглядеть так:
const FETCH_INTERVAL = parseInt(process.env.FETCH_INTERVAL) || 1000;
Исходя из нашего кода, параметр фигурирует как по умолчанию и если не задан в .env. Так как он задан в .env редактировать его нужно именно там.
Для 12 часов измените параметр в .env:
FETCH_INTERVAL=43200000
5. Итог
Такая настройка позволяет получать статистику в реальном времени минуя PHP скрипты и при этом учитывая изменения в получаемых данных для дальнейшей передачи WSS клиентам.
Для применения настроек не забудьте перезапустить службу:
cd /root/websocket-server
pm2 restart server.js --name websocket
Просмотр состояния службы
pm2 status server.js --name websocket

