Image

ナレッジベース → WebSocketサーバーの設定と使用

WebSocketサーバーは、リアルタイムでデータを受信するために設計されています。これは、ブラウザでリンクを開いてhtmlxmljsonなどのデータを受信する従来のWebサーバーとは異なります。従来のサーバーは、接続を確立し -> データを転送し -> 接続を切断します。 WebSocketサーバーは異なります。一度接続を確立すると、その接続は常にアクティブな状態を保ちます。データが変更されるとすぐにサーバーはクライアントにデータを転送します。これは双方向通信であるため、接続は常にアクティブで切断されません。

1. 用途

リアルタイムでデータを受信するために使用されます。例えば、ウェブサイトの訪問者数やオンラインストアの注文状況を、別のウェブページや別のデバイスでオンライン表示したい場合などに利用されます。

2. 仕組み

WebSocketサーバーは、後続の接続のために指定された専用ポートで動作する独立したサービスです。WS(WebSocketサーバー)またはWSS(WebSocketサーバー + SSL)プロトコルを使用すると、html + JSを用いてブラウザ経由でリアルタイムにデータを更新表示するように設定できます。

注意!このサービスは新しいポートの開放を必要とするため、このようなサービスのセットアップと実行は仮想サーバーでのみ可能です。これが、WebSocketサーバーが共有ホスティングサービスで利用できない理由です。

もし共有ホスティングのすべての顧客がこのようなサービスを起動したいと考えたら、それぞれに個別のポートを開放する必要があり、それはサービス自体のセキュリティを損なうことになります。ホスティングプロバイダーに関わらず、起動と設定はVPS/VDSサーバーでのみ可能です。

3. タスク例

https://domain.tld/online-orders.phpというPHPスクリプトがあり、これがJSON形式でデータを出力しているとします。現状では常に更新が必要なため、このデータをリアルタイムで表示したいと考えます。

注意!PCやブラウザの場合、リクエスト頻度を増やすことでデータを更新できますが、マイクロコントローラーからJSONデータを頻繁にリクエストして更新すると、単純に1〜2秒間フリーズする原因となります。また、例えば20秒ごとの定期的なPOST/GETリクエストは、やはりリアルタイムとは言えません。

結論として、WSSが必要です。リソースを最小限に構成したDebian 12上に新しいVPSを起動しましょう。WSSには高いパフォーマンスは必要ありません。すべての設定はrootユーザーとして行います。

3.1 サービスのインストール

apt update
apt install -y curl git nodejs npm

3.2 WebSocketプロジェクトの作成

mkdir ~/websocket-server
cd ~/websocket-server
npm init -y
npm install ws node-fetch express

3.3 websocket-serverプロジェクトのルートに設定ファイルを作成

nano server.js
import express from "express";
import { WebSocketServer } from "ws";
import fetch from "node-fetch";
import https from "https";
import fs from "fs";

// === 設定 ===
const PORT = 8080; // このWebSocketサーバーのポートを決定します
const FETCH_URL = "https://domain.tld/online-orders.php";
const FETCH_INTERVAL = 1000; // 1秒に1回

// === HTTPS証明書 ===
// (WSSが必要な場合、SSL証明書が必要です。Let's Encryptまたは自己署名証明書を使用できます)
const options = {
  key: fs.readFileSync("/etc/ssl/private/your-key.pem"),
  cert: fs.readFileSync("/etc/ssl/certs/your-cert.pem")
};

// === HTTP(S)サーバー + WebSocket ===
const app = express();
const server = https.createServer(options, app);
const wss = new WebSocketServer({ server });

let latestData = null;

// === 定期的なJSONリクエスト ===
async function updateData() {
  try {
    const res = await fetch(FETCH_URL);
    const json = await res.json();
    latestData = json;

    // すべての接続されたクライアントに送信
    wss.clients.forEach(client => {
      if (client.readyState === 1) { // WebSocket.OPEN
        client.send(JSON.stringify(json));
      }
    });
  } catch (err) {
    console.error("データ取得エラー:", err);
  }
}

setInterval(updateData, FETCH_INTERVAL);
updateData(); // 初回ロード

// === WebSocket接続 ===
wss.on("connection", ws => {
  console.log("クライアントが接続されました");

  if (latestData) ws.send(JSON.stringify(latestData));

  ws.on("close", () => console.log("クライアントが切断されました"));
});

server.listen(PORT, () => {
  console.log(`WSSサーバーがポート ${PORT}で起動しました`);
});

3.4 Let's Encryptを介したWSSのSSL設定

SSLにはドメインが必要です。例えば、ws.domain.tldのようなサブドメインを使用します。

apt install certbot python3-certbot-nginx
certbot certonly --standalone -d ws.domain.tld

通知用のメールアドレスを提供し、簡単な手順に従ってください。完了すると、証明書とキーは以下の場所にあります。

/etc/letsencrypt/live/ws.domain.tld/fullchain.pem
/etc/letsencrypt/live/ws.domain.tld/privkey.pem

server.jsファイルのパスを置き換えてください。

const options = {
  key: fs.readFileSync("/etc/letsencrypt/live/ws.domain.tld/privkey.pem"),
  cert: fs.readFileSync("/etc/letsencrypt/live/ws.domain.tld/fullchain.pem")
};

3.5 実行とデバッグ

node server.js

SyntaxError: Cannot use import statement outside a moduleというエラーが発生しました。

この場合、package.jsonファイルに"type": "module",という行を追加して変更する必要があります。

package.jsonの正しい設定例:

{
  "name": "websocket-server",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
...

再度実行します。

node server.js

WSSサーバーがポート 8080で起動しました
クライアントが接続されました

3.6 自動起動の設定:

npm install -g pm2
pm2 start server.js --name websocket
pm2 save
pm2 startup

3.7 サービス動作の確認

pm2 list

3.8 HTMLとJSを使用した接続

PC上に以下の内容を含むwss-client.htmlファイルを作成してください。

Webサーバーのようにブラウザからドメイン経由でWebSocketに直接接続することはできません。スクリプトを作成する必要があります。ws.domain.tldの代わりにあなたのドメインを指定してください。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>WSSテスト</title>
  <style>
    body {
      font-family: monospace;
      background: #111;
      color: #0f0;
      padding: 20px;
    }
    input, button {
      background: #222;
      color: #0f0;
      border: 1px solid #0f0;
      padding: 6px 10px;
      font-family: monospace;
    }
    #log {
      margin-top: 20px;
      white-space: pre-wrap;
      background: #000;
      border: 1px solid #0f0;
      padding: 10px;
      height: 400px;
      overflow-y: auto;
    }
  </style>
</head>
<body>
  <h2>🔌 WebSocketテストクライアント</h2>

  <label>サーバーURL:</label>
  <input id="wsUrl" type="text" size="50" value="wss://ws.domain.tld:8080">
  <button onclick="connect()">接続</button>
  <button onclick="disconnect()">切断</button>

  <div id="status">ステータス: <b>未接続</b></div>
  <div id="log"></div>

  <script>
    let ws;

    function log(msg) {
      const logDiv = document.getElementById('log');
      logDiv.innerText += msg + "\n";
      logDiv.scrollTop = logDiv.scrollHeight;
    }

    function connect() {
      const url = document.getElementById('wsUrl').value;
      ws = new WebSocket(url);

      log("🔄 " + url + "に接続中 ...");
      document.getElementById('status').innerHTML = "ステータス: <b>接続中...</b>";

      ws.onopen = () => {
        log("✅ 接続が確立されました");
        document.getElementById('status').innerHTML = "ステータス: <b style='color:lime'>接続済み</b>";
      };

      ws.onmessage = event => {
        try {
          const json = JSON.parse(event.data);
          log("📦 受信:\n" + JSON.stringify(json, null, 2));
        } catch (e) {
          log("📦 テキスト:\n" + event.data);
        }
      };

      ws.onclose = () => {
        log("❌ 接続が閉じられました");
        document.getElementById('status').innerHTML = "ステータス: <b style='color:red'>切断済み</b>";
      };

      ws.onerror = err => {
        log("⚠️ エラー: " + err.message);
      };
    }

    function disconnect() {
      if (ws) {
        ws.close();
        ws = null;
      }
    }
  </script>
</body>
</html>

3.9 ログの表示

接続は機能するはずです。スクリプト内のデータが変更されるとすぐに、WebSocketは接続されているすべてのクライアントにデータを更新します。

以下のコマンドでイベントログを表示できます。

pm2 logs websocket
0|websocket  | 5件の訪問が更新されました

4. 認証

上記の例では、誰でもWebSocketサーバーに接続できるため、トークンベースの認証を追加しましょう。

更新された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";

// === 設定 ===
const PORT = 8080;
const FETCH_URL = "https://domain.tld/online-orders.php";
const FETCH_INTERVAL = 1000; // 1秒ごと
const VALID_TOKENS = ["ABC123", "ESP32TOKEN", "MYSECRET"]; // 許可されたトークンのリスト

// === SSL ===
const options = {
  key: fs.readFileSync("/etc/letsencrypt/live/ws.domain.tld/privkey.pem"),
  cert: fs.readFileSync("/etc/letsencrypt/live/ws.domain.tld/fullchain.pem")
};

// === サーバーの作成 ===
const app = express();
const server = https.createServer(options, app);
const wss = new WebSocketServer({ server });

let latestData = null;

// === 定期的なJSON取得のための関数 ===
async function updateData() {
  try {
    const res = await fetch(FETCH_URL);
    const json = await res.json();
    latestData = json;

    // すべてのアクティブなクライアントに送信
    wss.clients.forEach(client => {
      if (client.readyState === 1) { // WebSocket.OPEN
        client.send(JSON.stringify(json));
      }
    });
  } catch (err) {
    console.error("データ取得エラー:", err);
  }
}

setInterval(updateData, FETCH_INTERVAL);
updateData();

// === 接続の処理 ===
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 (latestData) ws.send(JSON.stringify(latestData));

  ws.on("close", () => {
    console.log(`🔌 クライアント (${token}) が切断されました`);
  });
});

server.listen(PORT, () => {
  console.log(`✅ WSSサーバーがポート ${PORT}で起動しました`);
});

4.1 トークン認証での接続

HTML + JSを使用する例では、単に以下の行を変更してください。

<input id="wsUrl" value="wss://ws.domain.tld:8080/?token=ABC123">

5. 結論

設定は完了しました。これで、すべてのWebSocketサーバークライアントは暗号化を使用してリアルタイムでデータを受信します。この例では、APIを使用してシステムからデータを取得し、それをWebSocketに転送するための中間役としてPHPスクリプトを使用しています。

PHPスクリプト自体は削除し、server.js内で直接APIリクエストを使用し、システムへのAPI認証を行うことも可能です。これはさらに効率的ですが、その場合、APIキーの.envファイルでの安全な保存や、設定ファイルからのデータ操作のためのnpm install dotenvパッケージのインストールに触れる必要があり、記事が非常に長くなってしまいます。





No Comments Yet