Image

지식 기반 → WebSocket 서버 설정 및 사용

WebSocket 서버는 실시간 데이터를 수신하도록 설계되었습니다. 이는 브라우저에서 링크를 열고 html, xml, json 등과 같은 데이터를 얻는 기존 웹 서버와 다릅니다. 기존 서버는 연결을 설정 -> 데이터를 전송 -> 연결을 종료합니다. WebSocket 서버는 다르게 작동합니다. 서버와 연결을 설정하면 이 연결은 항상 활성 상태를 유지합니다. 데이터가 변경되는 즉시 서버는 클라이언트에게 데이터를 전송합니다. 양방향 통신을 사용하기 때문에 연결은 항상 활성 상태이며 끊어지지 않습니다.

1. 적용

실시간 데이터를 수신하기 위해 사용됩니다. 예를 들어, 웹사이트 방문자나 온라인 상점 주문 상태를 별도의 웹 페이지 또는 별도의 장치에서 실시간으로 표시하고 싶을 때 활용할 수 있습니다.

2. 작동 방식

WebSocket 서버는 사용자가 후속 연결을 위해 할당하는 전용 포트에서 실행되는 별도의 서비스입니다. WS (WebSocket 서버) 또는 WSS (WebSocket 서버 + SSL) 프로토콜을 사용하면 html + JS를 사용하여 브라우저를 통해 실시간으로 데이터를 출력하도록 구성할 수 있습니다.

참고! 이 서비스는 새 포트 개방을 요구하므로, 이러한 서비스 설정 및 실행은 가상 서버에서만 가능합니다. 이것이 바로 WebSocket 서버가 가상 호스팅 서비스에서 사용할 수 없는 이유입니다.

모든 가상 호스팅 클라이언트가 이러한 서비스를 실행하기를 원한다고 상상해 보십시오. 각 클라이언트마다 별도의 포트를 열어야 하며, 이는 서비스 자체의 보안을 위협할 것입니다. 호스팅 제공업체와 관계없이 VPS/VDS 서버에서만 시작 및 구성이 가능합니다.

3. 작업 예시

PHP 스크립트 https://domain.tld/online-orders.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초에 한 번

// === 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를 사용하여 연결

다음 내용을 포함하는 wss-client.html 파일을 PC에 생성하십시오:

웹 서버처럼 브라우저에서 도메인을 통해 WebSocket에 직접 연결할 수 없습니다. 스크립트를 생성해야 합니다. ws.domain.tld 대신 자신의 도메인을 지정하십시오.

<!DOCTYPE html>
<html lang="ko">
<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 서버 클라이언트는 암호화를 사용하여 실시간 데이터를 수신합니다. 저희 예시에서는 PHP 스크립트를 시스템(API를 사용하여 데이터를 가져오는)과 WebSocket 서버(데이터를 전송하는) 사이의 중개자로 사용합니다.

PHP 스크립트 자체는 제거할 수 있으며, server.js에서 직접 API 요청을 사용하여 시스템에 대한 API 권한 부여를 수행할 수 있습니다. 이는 훨씬 더 효율적일 것입니다. 그러나 그렇게 하면 .env 파일에 API 키를 안전하게 저장하는 방법과 구성 파일 데이터 작업을 위한 npm install dotenv 패키지 설치를 다루어야 하므로 문서가 상당히 길어질 것입니다.





No Comments Yet