반응형

npm으로 node.js 모듈을 install하다 이런 오류가 발생할 경우

To see a list of supported npm commands, run: npm help
npm notice Changelog: https://github.com/npm/cli/releases/tag/v10.8.2 Unknown command: "notice"
To see a list of supported npm commands, run: npm help
npm ERR! errno -13
Unknown command: "ERR!"

아래 명령어 실행 후 다시 install을 시도하면 잘 될 수도 있다~
npm cache clean --force




반응형
Posted by Hippalus
,

반응형
1. 브라우저 보안 정책상 자동 재생이 불가하나 그래도 해야겠다면 mute로 실행되어야 한다.
2. hls.js로 스트림 서버 접속해서 영상을 받아오면 된다.

<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>
<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Live Streams</title>
</head>
<body>
  <video id="live-stream" width="640" height="360" controls autoplay></video>
  <script>
    var video = document.getElementById('live-stream');
   
    if (Hls.isSupported()) {
        var hls = new Hls({
            lowLatencyMode: true,
            liveSyncDurationCount: 1,
            liveMaxLatencyDurationCount: 3,
            liveDurationInfinity: true,
            highBufferWatchdogPeriod: 1,
            nudgeMaxRetry: 5,
            nudgeOffset: 0.1,
            maxFragLookUpTolerance: 0.1,
            startFragPrefetch: true
        });


      hls.loadSource(videoSrc);
      hls.attachMedia(video);
      hls.on(Hls.Events.MANIFEST_PARSED, function() {
        hls.startLoad(-1);  // 가장 최근 세그먼트부터 로드
        video.play();
      });

      hls.on(Hls.Events.ERROR, function(event, data) {
        console.log('HLS error:', data);
        if (data.fatal) {
          hls.destroy();
          setTimeout(() => {
            hls.loadSource(videoSrc);
            hls.attachMedia(video);
          }, 1000);
        }
      });
    }
    else if (video.canPlayType('application/vnd.apple.mpegurl')) {
        video.src = videoSrc;

        video.addEventListener('loadedmetadata', function() {
            if (video.duration > 15) {
                video.currentTime = video.duration - 1;  // 가장 최근 시점으로 이동
            }

            video.play();
        });
    }
  </script>
</body>
</html>

OBS Studio의 설정, node.js 서버설정, 클라이언트에서도 최근 시점으로 이동시키는 설정 등 
3위일체가 되었다면 3초는 아니더라도 5초~8초 사이의 딜레이정도만 존재하는 훌륭한 스트리밍 서비스가 가능하다.
다만 아직 못해본건 서버사양과 대역폭 등에 대한 최대 지원 정도는 테스트 해봐야 함.
경우에 따라선 nginx로 분산처리도 고려 대상임.

* 쿠키 *
만약 youtube의 실시간 채널 라이브 방송을 진행하고 이를 웹브라우저에서 보고 싶다면 아래처럼 iframe을 이용하면 확인할 수 있다.
youtube는 3초 정도의 딜레이가 존재한다.
구글이니까

<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>
<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Live Streams</title>
</head>
<body>
  <iframe id="live-stream" width="640" height="360" src="https://www.youtube.com/embed/******qSuVkrE?autoplay=1" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe>
</body>
</html>
반응형
Posted by Hippalus
,

반응형

다른거 다 필요 없다.

방송 송출 서버 주소 설정하고 방송하면 된다.
설정을 해야 하니 제어 > 설정 눌러보면 팝업윈도우가 새로 뜨는데
좌측 메뉴에서 방송 누르고 서비스는 사용자 지정, 서버는 프로토콜을 rtmp로 설정 후 내 node.js 서버가 존재하는 ip, 도메인을 입력 후 그 하위 디렉토리까지 입력해준다.
일전에 nginx 설정할 때 location /live 로 입력했던것 기억하는가?
그 live가 이 live다.
스트림 키는 유저별로 생성해주는 키로 마찬가지로 일전에 /node/stream/media/live/스트림키 어쩌구저쩌구 에 ts파일과 m3u8파일이 생긴다고 말했다.
그때 사용되는 폴더가 이 스트림키로 생성된다.
rtmp://mydomain.com/live 이런식으로 서버에 설정해주면 된다. (클라이언트 접속시엔 포트 번호가 들어가야 하지만 여기선 제거)

여기까지만 해도 일단 방송 출력은 될 것이다.
하지만 가장 중요한건 딜레이 줄이기 위한 설정이 남아있다.
이것 몰라서 8시간 헤맸다.

제어 > 설정 눌러보면 팝업윈도우가 새로 뜨는데
여기서 좌측 메뉴들 중 출력 선택 > 고급 선택 > 비디오 인코더 x264, 키프레임 간격 2s, CPU 사용량 사전 설정(ultrafast)
이정도만 손봐주면 된다.
이거 안 해주면 15초 25초씩 딜레이 걸린다.
참고로 유튜브라이브는 3초고 rtmp는 5초~8초가 최대 한계다.

만약 웹캠 없이 스마트폰으로 방송을 해야 한다면 iVCam이란 프로그램을 설치하기 바란다.
중공산 프로그램 같긴한데.. 좀 찝찝하다만 동작은 잘 된다.
내 폰은 iPhone인데 PC와 폰 모두 동일한 Wifi로 연결해야만 영상 전송이 가능하다.
따라서 그냥 케이블로 연결했다.
이것도 처음엔 잘 안됐는데 어찌저찌하다보니 되더라.. 그리고 가끔씩 끊어지더라
왜 끊어지는진 모르겠다. 연결 끊김 연결 반복되기도 하더란.. 그래도 뭐 잘은 되니

다 연결해서 화면 뜨고 방송 시작 누르면 방송 된다.

live폴더 아래에 스트림키 폴더에 보면 이렇게 영상 파일들이 실시간으로 생성되는걸 확인할 수 있다.

매번 ls로 확인하기 귀찮으면
watch -n 1 "ls -lR /node/stream/media/live"
이렇게 해두면 알아서 파일이 실시간 확인 가능하다.

다음엔 웹브라우저로 송출받은 영상을 확인해 보겠다.

반응형
Posted by Hippalus
,

반응형

1. node-media-server (nms) 모듈 설치 : npm install node-media-server
2. nms 기본 포트는 1935 사용
3. 앞서 nginx에서 설정했던 7999 포트로 클라이언트 웹브라우저에서 요청시 실시간 영상 제공
4. media파일 경로는 /node/stream/media
    스트림키를 부여 받고 방송을 할 경우 저 media 폴더 하위에 스트림키 폴더가 생성되고 그 안에 ts, m3u8 파일 생성됨
5. 방송 송출자와 시청자의 시간차를 줄이기 위해 hls_time을 2로 설정함. 숫자가 낮을수록 차가 줄어들지만 부하발생

const NodeMediaServer = require('node-media-server');

const config = {
  rtmp: {
    port: 1935,
    chunk_size: 60000,
    gop_cache: false,
    ping: 30,
    ping_timeout: 60
  },
  http: {
    port: 7999,
    allow_origin: '*',
    cors: {
        enable: true,
        origin: '*'
    },
    mediaroot: '/node/stream/media',
    webroot: '/node/stream/www',
  },
  trans: {
    ffmpeg: '/usr/local/bin/ffmpeg', // 여기에 올바른 ffmpeg 경로를 설정합니다.
    tasks: [
      {
        app: 'live',
        hls: true,
        hlsFlags: '[hls_time=2:hls_list_size=3:hls_flags=delete_segments]',
        hlsKeep: true, // 스트림 종료 후 hls 파일 삭제 방지
        dash: true,
        dashFlags: '[f=dash:window_size=3:extra_window_size=5]',
        dashKeep: true // 스트림 종료 후 dash 파일 삭제 방지
      }
    ]
  },

  logType: 3 // Debug 레벨 설정
};

const nms = new NodeMediaServer(config);
nms.run();
반응형
Posted by Hippalus
,

반응형

[주요 설정]
1. 8000포트를 사용하여 SSL로 live 클라이언트 웹 브라우저에서 접속
2. 서버의 스트리밍 파일(m3u8, ts등)이 생성되는 곳은 /node/stream/media/live 폴더
3. 빌어X을 CORS는 제낌
4. node.js로 만든 스트리밍 서버에선 클라이언트 브라우저의 요청시 7999포트를 이용해서 hls로 리턴해줄 예정

server {
    listen 8000 ssl;
    server_name dev.*****.co.kr;

    ssl_certificate     /usr/local/nginx/conf/cert/2024/dev_*****_co_kr_NginX_cert.pem;
    ssl_certificate_key /usr/local/nginx/conf/cert/2024/dev_*****_co_kr_NginX_key.pem;
    access_log /usr/local/nginx/logs/dev.*****.co.kr_access_log;
    error_log /usr/local/nginx/logs/dev.*****.co.kr_error_log;
    ssl_session_cache shared:SSL:1m;
    ssl_session_timeout 5m;
    ssl_protocols TLSv1.1 TLSv1.2;
    ssl_ciphers ******************************************************************************;
    ssl_prefer_server_ciphers on;

    add_header 'Access-Control-Allow-Origin' '*' always;
    add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE' always;
    add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always;
    add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;

    location /live {
        alias /node/stream/media/live;
        proxy_pass http://127.0.0.1:7999;
        proxy_buffering off;
        proxy_request_buffering off;

        proxy_cache off;

        proxy_read_timeout 10s;
        proxy_send_timeout 10s;
        send_timeout 10s;

        proxy_http_version 1.1;
        proxy_connect_timeout 75s;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
        add_header Cache-Control no-cache;

        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Allow-Origin' '*';
            add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
            add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
            add_header 'Access-Control-Max-Age' 1728000;
            add_header 'Content-Type' 'text/plain; charset=utf-8';
            add_header 'Content-Length' 0;
            return 204;
        }

        types {
            application/vnd.apple.mpegurl m3u8;
            video/mp2t ts;
        }
    }


    location /hls {
        # Disable cache
        add_header Cache-Control no-cache;

        # CORS setup
        add_header 'Access-Control-Allow-Origin' '*' always;
        add_header 'Access-Control-Expose-Headers' 'Content-Length';

        # allow CORS preflight requests
        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Allow-Origin' '*';
            add_header 'Access-Control-Max-Age' 1728000;
            add_header 'Content-Type' 'text/plain charset=UTF-8';
            add_header 'Content-Length' 0;
            return 204;
        }
        
        # Serve HLS fragments
        types {
            application/vnd.apple.mpegurl m3u8;
            video/mp2t ts;
        }
        
        root /tmp;
        add_header Cache-Control no-cache;
    }
}

다음은 node.js로 스트리밍 서버를 만들어보겠다.
서버는 환경설정보다 쉽다.

반응형
Posted by Hippalus
,

반응형

너무 정신 없이 이것 저것 검색해가며 설치하다 보니 잡탕이지만 나름 정리해보자면
nginx를 이용해 스트리밍 방송을 하려면 nginx에 rtmp모듈을 설치하여 컴파일하여야 하고
nms를 이용해 실시간 방송을 하려면 ffmpeg도 설치해야 한다.
현재 실행 권한은 su 권한이다.

먼저 nginx에 rtmp모듈을 설치 및 컴파일을 진행한다.
아래 과정은 Nginx를 커스터마이징하여 필요한 기능을 추가하고, 소스 코드를 컴파일하여 시스템에 설치하는 과정으로 각각의 단계는 다음과 같은 목적을 가지고 있다.

Nginx 소스 코드 다운로드 및 해제: Nginx 소스 코드를 다운로드하고 압축을 해제하여 빌드 준비
RTMP 모듈 다운로드: Nginx에 RTMP 스트리밍 기능을 추가하기 위해 RTMP 모듈 소스 코드를 다운로드
필수 라이브러리 설치: Nginx 빌드에 필요한 PCRE, zlib, OpenSSL 라이브러리와 개발 파일을 설치
configure 실행: 시스템 환경에 맞게 Nginx 빌드 설정을 준비하고, RTMP 모듈을 포함
make 및 make install: 소스 코드를 컴파일하고, 컴파일된 바이너리를 시스템에 설치

이 과정을 통해 원하는 기능을 가진 커스텀 Nginx를 빌드하여 사용할 수 있다.

1. 먼저 설치된 nginx의 버전을 확인해 본다.
설치된 폴더는 /usr/local/nginx/sbin 이므로
cd /usr/local/nginx/sbin
./nginx -v를 실행하면 
설치된 버전 확인이 가능하다.
네 경우 nginx version: nginx/1.12.2 로 나왔다.

2. wget 명령어나 curl 명령어로 파일을 다운 받는다.
난 curl로 다운받았다.
wget http://nginx.org/download/nginx-1.12.2.tar.gz
curl -O http://nginx.org/download/nginx-1.12.2.tar.gz

tar -xvf nginx-1.12.2.tar.gz

git clone https://github.com/arut/nginx-rtmp-module.git
cd nginx-1.12.2

Nginx 빌드시 필요한 의존성으로 사용되는 패키지 설치
libpcre3 및 libpcre3-dev: 정규 표현식을 사용하는 소프트웨어 개발을 위한 PCRE 라이브러리.
zlib1g-dev: 데이터 압축 및 해제를 위한 zlib 라이브러리 개발 파일.
libssl-dev: 보안 통신을 지원하는 OpenSSL 라이브러리 개발 파일.

yum install pcre pcre-devel
yum install zlib zlib-devel
yum install openssl openssl-devel

./configure --add-module=/usr/local/nginx/nginx-rtmp-module
make
make install

이제 ffmpeg 설치
1. EPEL 리포지토리 추가
yum install epel-release -y
참고로 실행 전 yum repolist | grep epel 으로 실행시 이미 설치되어있다면 미진행해도 됨
이런식으로 나옴
epel: ftp.kaist.ac.kr epel/x86_64 Extra Packages for Enterprise Linux 7 - x86_64  

2. FFmpeg는 기본적으로 CentOS/RHEL의 공식 저장소에는 포함되어 있지 않으므로 FFmpeg 설치를 위해 RPM Fusion 리포지토리 추가
yum localinstall --nogpgcheck https://download1.rpmfusion.org/free/el/rpmfusion-free-release-7.noarch.rpm
yum localinstall --nogpgcheck https://download1.rpmfusion.org/nonfree/el/rpmfusion-nonfree-release-7.noarch.rpm

3. FFmpeg 설치
yum install ffmpeg ffmpeg-devel -y

4. FFmpeg 설치 확인
ffmpeg -version



만약 설치가 잘 안 된다면 아래 방법으로 시도
wget 또는 curl 설치하며 진행하면 되는데
yum install curl -y
curl -L -o ffmpeg-release-amd64-static.tar.xz https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz

압축 해제
tar xvf ffmpeg-release-amd64-static.tar.xz

압축 해제된 디렉토리로 이동
cd ffmpeg-*-amd64-static

FFmpeg 및 FFprobe 복사
해제된 디렉토리 내의 ffmpeg 및 ffprobe 파일을 /usr/local/bin으로 복사

cp ffmpeg /usr/local/bin/
cp ffprobe /usr/local/bin/

실행 권한 부여
chmod +x /usr/local/bin/ffmpeg
chmod +x /usr/local/bin/ffprobe

FFmpeg 버전 확인
ffmpeg -version
bash: /usr/bin/ffmpeg: 그런 파일이나 디렉터리가 없습니다 뜨면
ls -l /usr/local/bin/ffmpeg 로 확인

여기까지 진행됐다면 다음엔 nginx 설정을 진행한다.

반응형
Posted by Hippalus
,

반응형

pm2에 대해선 다른 블로그들을 참조하길 바란다.
백그라운드 상태에서 내가 만든 프로그램을 돌리고 관리하는 서비스다.


채팅서버는 port번호 8985부터 8987 총 3개를 동작시킬 예정이다.
편의를 위해 sh을 만들겠다.

앞서 만든 서버 프로그램이 있는 위치에서 vi start.sh로 파일을 만든다.
pm2로 실행할 때 port 번호를 전달하는 방식이다.

#!/bin/bash

# Start Node.js servers with PM2 on ports 8985 to 8987
for port in {8985..8987}
do
  pm2 start server.js --name app-$port -- $port
done

# Show PM2 process list
pm2 ls

:wp로 저장하고 나온다.


이번엔 종료다.
마찬가지로 vi stop.sh 파일을 만든다.

#!/bin/bash

# Stop Node.js servers with PM2 on ports 8985 to 8987
for port in {8985..8987}
do
  pm2 stop app-$port
  pm2 delete app-$port
done

# Show PM2 process list
pm2 ls

:wp로 저장하고 나온다.

이제 만든 파일들의 권한을 실행 가능하게 바꿔준다.
chmod +x start.sh
chmod +x stop.sh

start.sh를 실행한다.
./start.sh

중지하려면
./stop.sh

하나씩 중지되다가 아래처럼 아무것도 안남게 된다.

반응형
Posted by Hippalus
,

반응형

서버 소스 server.js

너무 간단한 버전이라 딱히 설명할게 없다.
클라이언트가 처음 접속할 때 user 아이디와 참여하고자 하는 방번호(그룹으로 묶여 있어서 이 방번호 안에서 발생한 메세지는 해당 방 참여자에게만 전송 용도)를 보내면 이를 map으로 저장해두고 
chat 메세지가 수신되면 보내는게 고작이다.
개별 실행하려면 node server.js 8985 처럼 포트번호를 적고 실행하면 동작한다.

const express = require('express');
const http = require('http');
const socketIO = require('socket.io');
const { createClient } = require('redis');

const app = express();
const server = http.createServer(app);

const io = socketIO(server, {
    cors: {
        origin: '*',
    }
});

const { v4: uuidv4 } = require('uuid');

// UUID 생성
const SERVERID = uuidv4();

console.log(SERVERID); // 고유한 UUID 출력

// Redis adapter setup
const redisAdapter = require('socket.io-redis');
const redisClient = createClient({
    url: 'redis://localhost:6379'
});
//redisClient.connect().catch(console.error);

redisClient.on('error', (error) => {
    console.error('Redis connection error:', error);
    // 여기에서 적절한 에러 처리를 수행할 수 있습니다.
});

redisClient.connect()
    .then(() => {
        console.log('Connected to Redis server');
    })
    .catch((error) => {
        console.error('Failed to connect to Redis server:', error);
        // 여기에서 적절한 에러 처리를 수행할 수 있습니다.
    });

// Store server's UUID in Redis
redisClient.set('server_id', SERVERID);

io.adapter(redisAdapter(redisClient));

const PING_INTERVAL = 5000; // 5 seconds
const CLIENT_TIMEOUT = PING_INTERVAL * 2; // double seconds

// Heartbeat channel
const HEARTBEAT_CHANNEL = SERVERID + '_' + 'heartbeat';

// Function to publish heartbeat
async function publishHeartbeat() {
    //console.log('Boradcast heartbeat message');
    await redisClient.publish(HEARTBEAT_CHANNEL, 'ping');
}

// Start heartbeat interval
setInterval(publishHeartbeat, PING_INTERVAL);

// Function to check and remove zombie clients
async function checkAndRemoveZombieClients() {
    console.log('Checking for zombie clients');
    const serverId = await redisClient.get('server_id'); // Redis에서 서버의 고유값 가져오기
    const clients = await redisClient.hGetAll('clients');

    for (const key in clients) {
        const client = JSON.parse(clients[key]);
        if (client.serverId === serverId) { // 해당 서버의 고유값과 일치하는 클라이언트만 처리
            const now = Date.now();
            const lastPong = client.lastPong || 0;
            if (now - lastPong > CLIENT_TIMEOUT * 3) {
                console.log(`Removing zombie client: ${key}`);
                // 좀비 클라이언트의 소켓을 disconnect
                const socket = io.sockets.sockets.get(key);
                if (socket) {
                    socket.disconnect(true);
                }
                // Redis에서 클라이언트 정보 삭제
                await redisClient.hDel('clients', key);
            }
        }
    }
}



// Start zombie client check interval
setInterval(checkAndRemoveZombieClients, PING_INTERVAL * 2); // Check every CLIENT_TIMEOUT milliseconds


// Function to check and update client timeout
async function updateClientTimeout(socketId) {
    console.log("updateClientTimeout call! : ", socketId)
    const now = Date.now();
    const clientJson = await redisClient.hGet('clients', socketId);
   
    if (!clientJson) return; // Client not found, possibly already disconnected

    const client = JSON.parse(clientJson);
    client.lastPong = now; // Update last pong time

    // Check if client was in timeout state before this pong
    if (now - client.lastPong > CLIENT_TIMEOUT) {
        console.log(`Client ${socketId} was in timeout state, but responded now.`);
        // Optionally, you can emit an event to the client to notify that it was nearly disconnected
        io.to(socketId).emit('timeout-warning', 'You were about to be disconnected due to inactivity.');
    }

    // Update client info in Redis
    await redisClient.hSet('clients', socketId, JSON.stringify(client));
}

io.on('connection', async (socket) => {
    console.log('A new client has connected.');

    const user = socket.handshake.query.user;
    const itemseq = socket.handshake.query.itemseq;

    console.log('Welcome!!!', user, itemseq);

    // Store client information in Redis
    //await redisClient.hSet('clients', socket.id, JSON.stringify({ user, itemseq, socketId: socket.id, lastPong: Date.now() }));
    try {
        //await redisClient.hSet('clients', socket.id, JSON.stringify({ user, itemseq, socketId: socket.id, lastPong: Date.now() }));
        await redisClient.hSet('clients', socket.id, JSON.stringify({ user, itemseq, socketId: socket.id, lastPong: Date.now(), serverId: SERVERID }));
    } catch (error) {
        console.error('Failed to store client information in Redis:', error);
        // 여기에서 적절한 에러 처리를 수행할 수 있습니다.
    }

    // Subscribe client to heartbeat channel
    const subscriber = redisClient.duplicate();
    await subscriber.connect();
    await subscriber.subscribe(HEARTBEAT_CHANNEL, async (message) => {
        if (message === 'ping') {
            socket.emit('heartbeat', 'ping');
        }
    });

    socket.on('heartbeat', async (msg) => {
        if (msg === 'pong') {
            // Update client's timeout status individually
            await updateClientTimeout(socket.id);
        }
    });

    socket.on('data message', async (msg) => {
        console.log('Received message:', msg);

        if (msg.type === 'bid') {
            console.log("Bid received!!! : ", msg.itemseq);

            // Get all clients from Redis
            const clients = await redisClient.hGetAll('clients');
           
            const clientsToNotify = [];

            for (const key in clients) {
                const client = JSON.parse(clients[key]);

                if (client.itemseq === msg.itemseq) {
                    clientsToNotify.push(client);
                }
            }

            for (const client of clientsToNotify) {
                console.log("Sending to client!", client.socketId);
                io.to(client.socketId).emit('data message', msg);
            }
        } else {
            // Handle other types of messages
        }
    });

    socket.on('disconnect', async () => {
        console.log('Client disconnected.');
        // Unsubscribe from heartbeat channel and close the subscriber
        if (socket.subscriber) {
            await socket.subscriber.unsubscribe(HEARTBEAT_CHANNEL);
            await socket.subscriber.quit();
            delete socket.subscriber;  // Clean up the reference
        }

        await redisClient.hDel('clients', socket.id);
    });
});



// Start the server on the specified port or default to 8985
const port = process.argv[2] || 8985;
server.listen(port, async () => {
    // All redis clients delete.
    await redisClient.del('clients');

    console.log(`Server is running on port ${port}.`);
});



클라이언트 소스 

역시 별거 없다.
https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js 갖다쓰고
connect 버튼 누르면 서버에 접속하고
이후 접속되면 타입으로 전송하고 서버에서 오면 받아서 뿌리는게 끝이다.
 


<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Socket.IO Chat</title>
</head>
<body>
  <ul id="messages"></ul>
  <form id="form" action="">
    <input id="user" type="text" placeholder="user" value="arosones">
    <input id="itemseq" type="text" placeholder="itemseq" value="a01">
    <button id="connectButton">connect</button>
    <input id="bidamount" type="text" autocomplete="off" placeholder="bidamount">
    <button id="sendButton" disabled>send</button>
  </form>

  <script>
    $(document).ready(function() {
       
      var socket;
 
      $('#connectButton').click(function(e) {
        e.preventDefault();

        var user = $('#user').val();
        var itemseq = $('#itemseq').val();
       
        if (user && itemseq) {
            socket = io('https://dev.kobay.co.kr:8984', {
                query: {
                  user: user,
                  itemseq: itemseq
                },

                transports: ['websocket'],
                reconnectionAttempts:2,
                reconnectionDelay: 100
              });

          socket.on('error', (error) => {
            console.log('WebSocket connection error:', error);
                // 여기에 추가적인 오류 처리 로직을 넣을 수 있습니다.
          });
         
          socket.on('connect', () => {
            console.log('WebSocket connected');
           
            $('#connectButton').prop('disabled', true);
            $('#sendButton').prop('disabled', false);
          });

          socket.on('disconnect', () => {
            console.log('WebSocket disconnected');

            $('#connectButton').prop('disabled', false);
            $('#sendButton').prop('disabled', true);
          });

          socket.on('data message', function(msg) {
            console.log("msg : ", msg);

            var item = $('<li>').text(msg.bidamount);

            $('#messages').append(item);
          });


          socket.on('heartbeat', (msg) => {
            console.log("heartbeat rcv : ", msg);
              if (msg === 'ping') {
                    // 'ping' 메시지를 받으면 즉시 'pong'으로 응답
                    socket.emit('heartbeat', 'pong');
              }
          });          
        }
      });

      $('#sendButton').click(function(e) {
        e.preventDefault();

        var bidamount = $('#bidamount').val();
        var itemseq = $('#itemseq').val();

        if (bidamount && socket.connected) {
          var message = {
            type: 'bid',
            bidamount: bidamount,
            itemseq: itemseq,
          };

          socket.emit('data message', message);

          $('#bidamount').val('');
        }
      });
    });
  </script>
</body>
</html>




다만 중요한 부분은 transports: ['websocket'] 이부분이다.
WebSocket 전송 방식만 사용하는건데 이거 빠지면 사정없이 서버에서 끊어지고 난리도 아니다.
이 부분을 몰라서 이틀을 허비했다. ㅡㅡ

추가로
포스트맨으로 테스트 하려면

 

 



반응형
Posted by Hippalus
,

반응형

CentOS 7에서 Redis를 설치하는 방법은 다음과 같다.

1. EPEL 저장소 설치
Redis 패키지는 EPEL 저장소(Extra Packages for Enterprise Linux)를 통해 제공되므로 EPEL 저장소를 시스템에 추가한다.
yum install epel-release
(yum install epel-release yum-utils 로 설치시 유틸리티 도구들도 사용 가능하다)

2. 저장소를 추가하였다면 Redis 설치한다.
yum install redis
설치된 폴더는 /usr/bin이며 실제 시스템 서비스로 관리하기 위해 관리 스크립트가 존재하는 /etc/init.d에서 가지고 놀아야 한다.
서비스 시작 : systemctl start redis
서비스 종료 : systemctl stop redis
서비스 재시작 : systemctl restart redis
서비스 상태 확인 : systemctl status redis
부팅 시 서비스 자동 시작 설정 : systemctl enable redis
일단 서비스를 시작만하자
systemctl start redis

만약 서비스를 종료하거나 시작하는데 먹통이 되고 바로 커맨드 라인으로 복귀되지 않으면 환경설정을 확인해봐야 한다.
systemctl status redis 로 상태를 확인해 보면
redis-shutdown[3419]: ERR Errors trying to SHUTDOWN. Check logs.
이런 로그를 확인할 수 있을것이다.
vi나 cat명령어로 로그를 확인해보면
cat /var/log/redis/redis.log
Permission denied가 보인다.
데이터베이스 파일의 권한 문제, 메모리 부족 문제, 설정 파일 문제 등이 있을 수 있을 수 있는데 
내 경우 Redis가 백그라운드에서 데이터베이스를 저장하려고 시도했지만, RDB 파일을 저장할 위치에 대한 권한 문제가 발생하여 저장에 실패한 상황이었다.

이 경우 환경설정을 손봐야 한다.
/etc/redis.conf 또는 /etc/redis/redis.conf 파일을 열어본다.(내 경우 /etc/redis.conf 였다.)
vi /etc/redis.conf 후 /f 명령어로 dir을 n n n 누르다 보면 발견된다.
다행히 경로는 맞다. dir /var/lib/redis

그럼 권한 문제다.
chown redis:redis /var/lib/redis

Redis 서비스를 재시작 한다.
systemctl restart redis

이제 서비스를 stop, restart 시 정상 동작하게 되고 로그를 봐도 잘 진행됨이 확인 된다.
cat /var/log/redis/redis.log


3. Redis가 원격에서 액세스가 되도록 설정 파일을 바꿔준다.
vi /etc/redis.conf
/127.0.0.1 검색해서 (혹시 또 있다면 n으로 계속 검색)
bind 127.0.0.1을 0.0.0.0 으로 수정
변경 내용을 :wq로 저장 후 서비스 재시작 시켜준다.
systemctl restart redis

하는김에 서버가 6379 포트 수신 중인지 확인
(Redis는 기본적으로 6379 포트에서 실행)
netstat -tulpn | grep LISTEN

서버 확인도 해준다.
Redis 명령창을 실행하면 된다.
redis-cli 입력 후 엔터
ping라고 엔터치면 pong라고 응답이 온다.
기본 명령어( get, set, del )테스트
"hello"라는 key에 "world"라는 value를 저장
set hello world
앞서 저장해둔 "hello" key에 해당하는 value를 확인
get hello 엔터
world 확인


여기까지 정상이면 일단 준비는 끝

조금 응용해보자
Redis 서버의 클라이언트 연결에 관한 정보를 조회하는 데 사용되는 명령어
redis-cli INFO clients 를 입력하면 현재 연결된 클라이언트 정보를 확인할 수 있다.

저장시킨 키에 해당하는 모든 정보를 조회하는 명령어
HGETALL 

node.js에서 hset으로 clients를 추가했고 이를 삭제하려면
DEL clients 
1이 나오면 실제 삭제됨, 0이면 삭제된 값이 없음
(integer) 1
(integer) 0

번외로 뭔가 꼬여서 삭제 / 삭제하는 법을 사족으로 달고자 한다.
이런 오픈소스들은 익숙해지기 전까진 삭제해도 뭔가 껄끄럽다.
프로세스별 사용중인 포트 확인
netstat -tulpn | grep LISTEN

Redis 서비스가 실행 중이면 중지
systemctl stop redis

Redis 패키지를 제거
yum remove redis

Redis 관련 데이터와 설정 파일도 삭제하려면 
rm -rf /etc/redis /var/lib/redis /var/log/redis

반응형
Posted by Hippalus
,

반응형

1. yum 외부 저장소 추가
cd /etc/yum.repos.d
ls를 실행해보면 yum 저장소에는 nginx가 없다.

따라서 vi에디터로 /etc/yum.repos.d에 nginx.repo 파일을 생성 후 아래 스크립트를 복사 붙여넣기 한다. 

[nginx]
name=nginx repo
baseurl=http://nginx.org/packages/centos/7/$basearch/
gpgcheck=1
enabled=1

ESC :wq로 저장 후 나와준다.

*참고
baseurl에 centos/7로 되어 있는데 만약 centos가 8이면 8을 입력하면 된다.
(OS버전 확인 방법은 cat /etc/centos-release)
그리고 gpgcheck 옵션은 리포지토리에서 받은 패키지들의 GPG( GNU Privacy Guard) 서명을 확인할지 여부를 나타내는데 0으로 설정하면 서명을 확인하지 않고 패키지를 설치하게 되는데 무결성 보장, 신뢰성, 보안 취약성 등의 문제가 있을 수도 있으니 그냥 1로 하자.

enabled 활성화된 리포지토리에서 패키지를 설치할 수 있게 해주는 옵션이다.
 

2. yum install
yum install -y nginx
설치 과정에서 Public key for nginx-1.26.0-1.el7.ngx.x86_64.rpm is not installed 라며 제대로 설치가 안 될 경우
RPM 패키지가 서명되었지만 시스템에 해당 서명 키가 설치되어 있지 않아 발생하는 것으로 해결 방법은 해당 패키지에 대한 GPG 키를 가져와서 시스템에 추가한 후 다시 yum install -y nginx를 실행하면 된다.
rpm --import https://nginx.org/keys/nginx_signing.key

 



3.nginx.conf 수정
vi 명령어로 vi /etc/nginx/nginx.conf 파일을 수정한다.
* 내 경우 소스로 업체에서 설치를 해뒀는지 vi /usr/local/nginx/conf/nginx.conf로 수정해야 했다.


열어보면 이상한 설정들이 존재하는데


눈여겨 볼 곳은 이곳 부터다.
worker_processes auto;

events {
    worker_connections 1024;
}
프로세서의 코어수 만큼 연결 처리를 1024개를 자동으로 감지해서 설정해주는 부분인데
논리적으로야 코어가 16코어면 16 X 1024개니 어마어마하다.
하지만 실제 그만큼 처리 가능할리는 만무하니 이정도로만 알고 지나가겠다.

로드밸런싱을 위해 옵션을 주면 접속한 클라이언트들을 적절히 배분해준다.
ip_hash가 일반적으로 사용되지만 내 경우 모두 동일한 IP이므로 정상 동작하지 않을것이다.
따라서 아래 두개 중 least_conn을 적용했다.

다음은 upstream설정이다.
몇개의 서버 프로세스를 실행시킬지에 대한 설정인데 upstream다음에 적당한 이름을 지어주면 된다.
나는 클라이언트가 접속할 포트로 8984를 이용하고, 접속 후 생성된 서버 프로그램용으로 총 3개의 소켓 서버를 사용할 생각이므로 8985 부터 8987까지 추가해뒀다.
물리적 서버도 분리하면 더 많은 클라이언트 처리가 가능하겠지만
node.js 싱글스레드 방식이므로 하나의 local서버에서 포트만 달리 구성해도 나름 효과적이다.

upstream의 이름을 난 그냥 node_app이라 지었고

  upstream node_app {
    least_conn
    server 127.0.0.1:8985;  # 첫 번째 Node.js 인스턴스
    server 127.0.0.1:8986;  # 두 번째 Node.js 인스턴스
    server 127.0.0.1:8987;  # 세 번째 Node.js 인스턴스
    # 필요에 따라 더 많은 인스턴스를 추가
  }
그리고 로드밸런싱을 어떤 방식으로 할지 적어주면 되는데 
아래 4가지 중 하나를 선택하면 된다.
난 현재 연결 수가 가장 적은 서버로 연결되는 방식인 least_conn 방식을 선택했다.

least_conn: 현재 연결 수가 가장 적은 서버로 요청
round_robin (기본값): 순차적으로 각 서버에 요청
ip_hash : 해시값이 3이고 서버가 5개 있다면, 3 % 5 = 3 이므로 4번째 서버(0부터 시작하므로)가 선택
least_time : 요청을 처리하는 데 걸리는 시간에 기반하여 서버를 선택하며 least_time header 또는 least_time last_byte 옵션으로 사용

같은 서버에 port 번호만 다르게 해서 실행할거니 server 127.0.0.1 그리고 포트 번호를 적어줬다.


다음은 추가한 upstream을 호출하는 부분이다.
http를 사용한다면 그냥 80쪽에 추가하면 되지만
server {
     listen 80;
     listen 8984;
}

https(SSL)을 사용하여 통신하겠다면 listen 443 ssl아래에 아래처럼 소켓 클라이언트가 접속할 포트를 추가해주면 되겠다.
내가 지정한 포트는 8984다.

  server {
        listen 443 ssl;
        listen 8984 ssl;
        ...
        이하 ssl 구성 (pem, cache, protocols TLS...등등) 생략
        ...
  }

이제 가장 중요한 /socket.io 설정 부분이다.
이부분 때문에 좀 헤맸다.
proxy_pass : 앞서 upstream으로 분산처리를 하였는데 이 upstream이름을 적어주면 된다.
limit_conn conn_limit_per_ip : 클라이언트가 몇개 이상 접속하게 되면 자동으로 차단하여 서버를 보호할 것이다. 난 테스트 해보니 8000개까지 처리 가능하였다.
다른 설정들은 그냥 복사하면 그만이다.

    location /socket.io/ {
      limit_conn conn_limit_per_ip 8000;

      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header Host $host;

      proxy_pass http://backsvr;
      proxy_set_header X-Server-Name $upstream_addr;

      # enable WebSockets
      proxy_http_version 1.1;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection "upgrade";
    }


추가된 결과



4. Nginx 재시작 하여 설정 변경 사항 적용
Nginx가 설치된 위치는 내 경우 /usr/sbin아래이므로
/usr/sbin/nginx -s reload

여기까지 Nginx 준비는 끝이다.

실행 : systemctl start nginx
중지 : systemctl stop nginx
재시작 : systemctl restart nginx
상태 : systemctl status nginx

추가로 만약 nginx 실행 문제가 발생할 경우 확인하는 방법을 설명하자면
만약 path가 없어서 nginx -t명령어 실행이 불가능할 경우
whereis nginx 으로 nginx가 어디에 설치되어 있는지 확인하고 (보통 /usr/sbin 폴더에 있다.)

수정된 conf파일 검사 : /usr/local/nginx/sbin/nginx -t

수정 후 nginx 중지 : /usr/local/nginx/sbin/nginx -s stop
수정 후 nginx 실행 : /usr/local/nginx/sbin/nginx

이 순서대로 진행하면 된다.


반응형
Posted by Hippalus
,