Dans l'article précédent, nous avons décrit le processus d'installation et de configuration de base du service WebSocket Server.
Ce service offre de nombreuses possibilités, et dans ce guide, nous allons en explorer certaines. Dans l'article précédent, nous n'avons montré qu'une configuration de base, où nous accédons à un script et transmettons continuellement des données via WebSocket. Cette approche n'est pas toujours efficace lorsqu'il s'agit de configurer un serveur de production.
1. Transmission des données uniquement si elles ont changé
Dans ce code, l'envoi des données aux clients ne se fait que si la chaîne JSON des nouvelles données est différente de la précédente.
const newJSON = JSON.stringify(live);
if (newJSON !== previousLiveJSON) {
previousLiveJSON = newJSON;
latestLiveData = live;
broadcast();
console.log(`Обновлены Live данные (${live.length} визитов)`);
}
Cette approche économise considérablement les ressources du client, surtout lorsque les données sont transmises à un microcontrôleur, ce qui réduit considérablement la charge.
2. Regrouper deux requêtes en une seule
Vous pouvez structurer le code de manière à ce que les deux requêtes soient exécutées simultanément et transmettent les données nécessaires. Dans ce code, nous obtenons deux types de données différents : Live et 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 });
// === Variables globales ===
let latestLiveData = [];
let latestSummary = {};
let previousLiveJSON = "";
let previousSummaryJSON = "";
// ===== Récupération des statistiques Live (dernières visites) =====
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;
}
// ===== Récupération des statistiques générales (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
};
}
// ===== Fonction principale de mise à jour =====
async function fetchMatomoData() {
try {
const [live, summary] = await Promise.all([
fetchMatomoLive(),
fetchSummaryData()
]);
// --- Vérification des modifications ---
const newLiveJSON = JSON.stringify(live);
const newSummaryJSON = JSON.stringify(summary);
const liveChanged = newLiveJSON !== previousLiveJSON;
const summaryChanged = newSummaryJSON !== previousSummaryJSON;
// --- Mise à jour des caches ---
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 без изменений");
}
// --- Envoi aux clients uniquement s'il y a des modifications ---
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);
}
}
// ===== Lancement de la boucle =====
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}`);
});
Dans cet exemple, nous demandons directement les données via deux requêtes API différentes et les transmettons dans une seule réponse, en évitant les scripts PHP.
3. Configuration sécurisée de l'autorisation
Dans le code ci-dessus, des variables sont utilisées pour stocker les données d'autorisation. Pour cela, il est nécessaire d'installer le paquet :
npm install dotenv
Le fichier de configuration ressemble à ceci :
nano /root/websocket-server/.env
# === Configuration API ===
MATOMO_URL=https://domain.tld/path/
MATOMO_TOKEN=YOUR_API_KEY
MATOMO_SITE_ID=1
# === Configuration du serveur ===
PORT=8080
FETCH_INTERVAL=1000
# === Accès WebSocket ===
VALID_TOKENS=WSS_CLIENT_TOKEN_1,WSS_CLIENT_TOKEN_1
# === Chemins vers SSL ===
SSL_KEY=/etc/letsencrypt/live/wss.domain.tld/privkey.pem
SSL_CERT=/etc/letsencrypt/live/wss.domain.tld/fullchain.pem
Comme vous l'avez déjà remarqué, l'autorisation se fait à la fois pour l'accès à l'API et pour les clients lors de la connexion au serveur WebSocket.
Pour vous connecter à WSS, utilisez l'adresse avec le token
/?token=WSS_CLIENT_TOKEN_1
4. Fréquence des requêtes de données via l'API
La configuration se fait dans la ligne :
const FETCH_INTERVAL = parseInt(process.env.FETCH_INTERVAL) || 1000;
Le paramètre est indiqué en millisecondes, où 1000 = 1 seconde.
Pour 10 secondes, cela ressemblera à ceci :
const FETCH_INTERVAL = parseInt(process.env.FETCH_INTERVAL) || 1000;
D'après notre code, le paramètre figure par défaut et s'il n'est pas défini dans .env. Comme il est défini dans .env, il faut le modifier là-bas.
Pour 12 heures, modifiez le paramètre dans .env :
FETCH_INTERVAL=43200000
5. Conclusion
Cette configuration permet d'obtenir des statistiques en temps réel en évitant les scripts PHP et en tenant compte des modifications des données reçues pour la transmission ultérieure aux clients WSS.
Pour appliquer les paramètres, n'oubliez pas de redémarrer le service :
cd /root/websocket-server
pm2 restart server.js --name websocket
Affichage de l'état du service
pm2 status server.js --name websocket

