Önceki makalede, WebSocket Sunucu hizmetini kurma ve yapılandırma sürecini açıklamıştık.
Bu hizmet kapsamlı yetenekler sunar ve bu kılavuzda bunlardan bazılarına göz atacağız. Önceki makalede, yalnızca temel bir yapılandırmayı gösterdik - bir komut dosyasını çağırdığımız ve bir WebSocket aracılığıyla sürekli olarak veri gönderdiğimiz yer. Bu yaklaşım, bir üretim sunucusu kurmaya gelince her zaman verimli değildir.
1. Yalnızca değiştiyse veri gönderme
Bu kodda, veriler yalnızca yeni JSON dizesi önceki dizeden farklıysa istemcilere gönderilir.
const newJSON = JSON.stringify(live);
if (newJSON !== previousLiveJSON) {
previousLiveJSON = newJSON;
latestLiveData = live;
broadcast();
console.log(`Canlı veriler güncellendi (${live.length} ziyaret)`);
}
Bu yaklaşım, özellikle veriler bir mikrodenetleyiciye gönderildiğinde istemci kaynaklarını önemli ölçüde kaydeder - bu gibi durumlarda, yükü büyük ölçüde azaltır.
2. İki isteği bir araya getirme
Kodunuzu, her iki isteğin de aynı anda yürütülmesini ve gerekli verileri sağlamasını sağlayacak şekilde yapılandırabilirsiniz. Bu örnekte, iki farklı veri türü alıyoruz: Canlı ve Özet.
/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; // sizin +3 saat diliminiz
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 });
// === Global değişkenler ===
let latestLiveData = [];
let latestSummary = {};
let previousLiveJSON = "";
let previousSummaryJSON = "";
// ===== Canlı istatistikleri getirme (son ziyaretler) =====
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;
}
// ===== Özet istatistikleri getirme (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
};
}
// ===== Ana güncelleme işlevi =====
async function fetchMatomoData() {
try {
const [live, summary] = await Promise.all([
fetchMatomoLive(),
fetchSummaryData()
]);
// --- Değişiklikleri kontrol et ---
const newLiveJSON = JSON.stringify(live);
const newSummaryJSON = JSON.stringify(summary);
const liveChanged = newLiveJSON !== previousLiveJSON;
const summaryChanged = newSummaryJSON !== previousSummaryJSON;
// --- Önbellekleri güncelle ---
if (liveChanged) {
previousLiveJSON = newLiveJSON;
latestLiveData = live;
console.log(`Canlı güncellendi (${live.length} ziyaret)`);
} else {
console.log("⏸ Canlı değişmedi");
}
if (summaryChanged) {
previousSummaryJSON = newSummaryJSON;
latestSummary = summary;
console.log("Özet güncellendi");
} else {
console.log("⏸ Özet değişmedi");
}
// --- Yalnızca veriler değiştiyse istemcilere güncellemeler gönder ---
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("Güncellenmiş veriler istemcilere gönderildi");
} else {
console.log("Değişiklik yok - gönderme atlandı");
}
} catch (err) {
console.error("Veri getirme hatası:", err.message);
}
}
// ===== Döngüyü başlat =====
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 ile bağlantı reddedildi: ${token}`);
ws.close(4001, "Geçersiz token");
return;
}
console.log(`İstemci bağlandı (${token})`);
if (latestLiveData.length > 0 || Object.keys(latestSummary).length > 0) {
ws.send(JSON.stringify({ visits_live: latestLiveData, summary: latestSummary }));
}
ws.on("close", () => {
console.log(`İstemci ${token} bağlantısı kesildi`);
});
});
// ===== HTTP Hata Ayıklama =====
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 sunucusu ${PORT} bağlantı noktasında çalışıyor`);
});
Bu örnekte, verileri doğrudan iki farklı API çağrısından istiyoruz ve bunları tek bir yanıtta göndererek PHP komut dosyalarını atlıyoruz.
3. Güvenli kimlik doğrulama kurulumu
Yukarıdaki kodda, kimlik doğrulama verilerini depolamak için değişkenler kullanılır. Bunu ele almak için aşağıdaki paketi yüklemeniz gerekir:
npm install dotenv
Yapılandırma dosyası şöyle görünür:
nano /root/websocket-server/.env
# === API Yapılandırması ===
MATOMO_URL=https://domain.tld/path/
MATOMO_TOKEN=YOUR_API_KEY
MATOMO_SITE_ID=1
# === Sunucu Yapılandırması ===
PORT=8080
FETCH_INTERVAL=1000
# === WebSocket Erişimi ===
VALID_TOKENS=WSS_CLIENT_TOKEN_1,WSS_CLIENT_TOKEN_2
# === SSL Yolları ===
SSL_KEY=/etc/letsencrypt/live/wss.domain.tld/privkey.pem
SSL_CERT=/etc/letsencrypt/live/wss.domain.tld/fullchain.pem
Gördüğünüz gibi, kimlik doğrulama hem API istekleri hem de WebSocket sunucusuna bağlanırken istemciler için gerçekleştirilir.
WSS'ye bağlanmak için, bir token içeren adresi kullanın:
/?token=WSS_CLIENT_TOKEN_1
4. API veri istek sıklığı
Yapılandırma aşağıdaki satırda yapılır:
const FETCH_INTERVAL = parseInt(process.env.FETCH_INTERVAL) || 1000;
Parametre milisaniye cinsinden belirtilir, burada 1000 = 1 saniyedir.
10 saniye için şöyle görünecektir:
const FETCH_INTERVAL = parseInt(process.env.FETCH_INTERVAL) || 10000;
Kodumuzda, bu parametre .env içinde tanımlanmamışsa varsayılan olarak ayarlanır.
Zaten orada belirtildiği için, doğrudan .env dosyasında düzenlenmelidir.
12 saat için, .env içindeki parametreyi değiştirin:
FETCH_INTERVAL=43200000
5. Sonuç
Bu yapılandırma, PHP komut dosyalarını atlayarak gerçek zamanlı istatistikler almanıza ve ayrıca WSS istemcilerine göndermeden önce veri değişikliklerini hesaba katmanıza olanak tanır.
Yeni ayarları uygulamak için, hizmeti yeniden başlatmayı unutmayın:
cd /root/websocket-server
pm2 restart server.js --name websocket
Hizmet durumunu kontrol edin:
pm2 status server.js --name websocket

