Image

ナレッジベース → WebSocketサーバーの機能と構成

前の記事では、WebSocketサーバーサービスのインストールと設定の手順について説明しました。

このサービスは広範な機能を提供しており、このガイドではそのいくつかを見ていきます。前の記事では、スクリプトを呼び出し、WebSocketを通じて継続的にデータを送信する基本的な構成のみを示しました。これは、本番サーバーのセットアップに関しては、必ずしも効率的なアプローチではありません。

1. データが変更された場合にのみ送信する

このコードでは、新しいJSON文字列が前の文字列と異なる場合にのみ、データがクライアントに送信されます。

const newJSON = JSON.stringify(live);

if (newJSON !== previousLiveJSON) {
  previousLiveJSON = newJSON;
  latestLiveData = live;
  broadcast();
  console.log(`ライブデータが更新されました(${live.length}回の訪問)`);
}

このアプローチは、特にマイクロコントローラーにデータを送信する場合に、クライアントリソースを大幅に節約します。そのような場合、負荷が大幅に軽減されます。

2. 2つのリクエストを1つにまとめる

両方のリクエストが同時に実行され、必要なデータを提供するようにコードを構成できます。 この例では、LiveSummaryという2つの異なるタイプのデータを受信します。

/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 = "";

// ===== ライブ統計の取得(最近の訪問) =====
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.length}回の訪問)`);
    } else {
      console.log("⏸ ライブデータは変更されていません");
    }

    if (summaryChanged) {
      previousSummaryJSON = newSummaryJSON;
      latestSummary = summary;
      console.log("サマリーが更新されました");
    } else {
      console.log("⏸ サマリーは変更されていません");
    }

    // --- データが変更された場合にのみ、クライアントに更新を送信 ---
    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, "無効なトークン");
    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デバッグ =====
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} で実行されています`);
});

この例では、2つの異なる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_2

# === 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) || 10000;

私たちのコードでは、このパラメーターは.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




No Comments Yet