Image

Bilgi Veritabanı → WebSocket Sunucusu Özellikleri ve Yapılandırması

Ö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




No Comments Yet