반응형

너무 정신 없이 이것 저것 검색해가며 설치하다 보니 잡탕이지만 나름 정리해보자면
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
,

반응형

소켓 프로그램을 만드는건 매우 쉽다.
하지만 잘 만드는건 어렵다.

소켓 프로그램은 일반 응용어플리케이션과 달리 외부 영향과 다양한 변수가 존재한다.
웹소켓은 그나마 TCP/IP와 달리 수월한 편에 속하지만 여전히 잘 만드는건 어렵다.

이번에 작업할 내용은 비교적 간단한 채팅방 개념의 다중 채팅이다.
즉 여러 클라이언트가 소켓 서버에 접속해서 각각 원하는 방에 입장하고 해당 방에서 발생한 대화는 해당 방에만 전달되는 전통적인 그 채팅이다.

기본적인 부분은 차치하고 설계시 고려대상으로 둔 부분은 몇 개의 클라이언트를 지원할 것인가였다.

나름 최대 4000개 이상의 클라이언트를 지원할 생각이다.

그럼 하나의 서버가 이를 다 커버할 수 있을까?
대답은 No

이때 사용할 기술이 PM2, Redis, Nginx다.
PM2는 멀티 프로세스로 서버 소켓 프로그램을 실행할 것이고
Nginx는 멀티 프로세스로 실행되는 서버 소켓 프로그램의 분배 접속에 
Redis는 멀티 프로세스간의 대화 내용을 공유하게 만들어주는 기술로 활용할 것이다.

이제 하나씩 진행해보자.

반응형
Posted by Hippalus
,

반응형

앞서 리눅스 centos에 postgreSQL을 설치해 보았다.
https://hippalus.tistory.com/649

이제 설치된 리눅스 postgreSQL에 어플리케이션으로 접속하여 DB를 조회해 볼 예정인데 일단 손쉬운 node.js로 구현해보려 한다.

일전에 로컬 DB에 user를 만들었을 당시엔 편의상 pgAdmin에서 생성( https://hippalus.tistory.com/648 )했는데 이번엔 리눅스 서버에 설치된 postgreSQL에 계정을 psql을 이용해 생성해보고 어플리케이션에서 접근할 테이블과 관련한 권한 설정도 진행해보려 한다.
지금은 tester 라는 계정을 만들며 작업을 진행하겠다.
이미 앞전 포스팅에서 tester와 testdb를 만든 상태라면 2번과 3번은 패싱~

1. 리눅스 터미널에 su 계정으로 접속되어 있단 가정하에 먼저 su계정에서 postgres계정으로 전환하자.
su - postgres
psql을 입력 후 엔터를 친다.

2. 커맨드 상태에서 CREATE USER tester WITH PASSWORD '적당한 암호';

엔터를 치면 CREATE ROLE 이라고 나와야 한다.

3. testdb라는 테스트용 DB를 만들어본다.(이미 만든 상태라면 pass)
CREATE DATABASE testdb;

4. 일단 여기까지 기본 준비는 끝이고 이제 본격적으로 어플리케이션에서 tester의 설정을 만져보려한다.
tester 계정은 단순히 어플리케이션단에서 SELECT, INSERT, UPDATE, DELETE 정도의 권한만 부여받은채로 실행될 계정이다.
이용자가 사용할 계정인데 SUPER USER 권한이 부여된채로 노출됐다가 무슨 일이 생길지 모르기 때문이다.

내가 사용할 DB는 testdb이므로 testdb로 실행해야 하므로 testdb로 사용 DB를 바꿔준다.
\c testdb 를 입력하고 엔터
그럼 이렇게 뜰 것이다.


5. 이제 해야 할 일은 이 testdb에서 table이 생성될 때 마다 자동으로 programuser 계정으로 SELECT, INSERT, UPDATE, DELETE 권한이 부여되게 만들어야 한다.

'public' 스키마의 모든 테이블에 대한 기본 권한 부여
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO tester ;

만약 이전에 생성한 테이블들이 있다면 아래처럼 한 번 더 실행해준다.
'public' 스키마의 모든 테이블에 대해 'appuser'에게 권한 부여
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO tester;

참고로 취소는 다음과 같다.
'public' 스키마의 모든 테이블에 대한 기본 권한 취소
ALTER DEFAULT PRIVILEGES IN SCHEMA public REVOKE SELECT, INSERT, UPDATE, DELETE ON TABLES FROM tester;

'public' 스키마의 모든 테이블에 대한 권한 취소
REVOKE SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public FROM tester;

6. 테이블이 만들어질 때마다 tester계정에 해당 테이블을 SELECT, INSERT, UPDATE, DELETE권한이 자동으로 부여되게 만들었으니 board_tbl을 만들자.
CREATE TABLE IF NOT EXISTS public.board_tbl
(
    b_idx integer NOT NULL,
    b_title text COLLATE pg_catalog."default" NOT NULL,
    b_content text COLLATE pg_catalog."default" NOT NULL,
    CONSTRAINT board_tbl_pkey PRIMARY KEY (b_idx)
);

7. 이제 기본 DB의 user작업은 끝났다.
node.js로 board_tbl을 select해보겠다.
insert 문을 이용해 데이터를 넣어보자.
insert into board_tbl (b_idx, b_title, b_content) values (1, 'title', 'content');
insert into board_tbl (b_idx, b_title, b_content) values (2, 'title2', 'content2');
insert into board_tbl (b_idx, b_title, b_content) values (3, 'title3', 'content3');

8. 드디어 node.js 프로그래밍
(node.js설치는 검색해서 알아서~)

node.js에서 postgreSQL을 접근하려면 pg 모듈을 설치해야 한다.
npm i pg를 터미널에서 입력하면 잘 설치 된다.

그다음 실제 소스 구현이다.
app.js라는 파일을 만들고 아래와 같이 타이핑하자.

const { Client } = require('pg');

// PostgreSQL 연결 정보 설정
const client = new Client({
  user: 'tester', //아까 생성한 어플리케이션용 계정
  host: '000.000.000.000', //postgreSQL이 설치된 서버의 ip
  database: 'testdb', //생성한 DB명
  password: '적당한암호', //어플리케이션용 계정의 비밀번호
  port: 5432, // PostgreSQL 기본 포트 번호
});

// PostgreSQL 데이터베이스에 연결
client.connect();

// board_tbl 테이블에서 데이터 조회
client.query('SELECT * FROM board_tbl', (err, res) => {
  if (err) {
    console.error('error :', err);
    return;
  }
  console.log('result :');
  for (let row of res.rows) {
    console.log(row);
  }
  // 데이터베이스 연결 종료
  client.end();
});

저장 후 실행
node app.js 엔터
잘~ 나온다.

만약 정보가 틀리거나 권한이 없거나 그러면 이런 오류를 발견하게 될 것이다.
반응형
Posted by Hippalus
,

반응형


https://www.postgresql.org/download/


일단 리눅스를 선택한다.
그럼 바로 아래에 OS선택이 뜨는데 난 centos7이니 Red Hat을 선택했다.

그럼 이 페이지로 이동하는데
https://www.postgresql.org/download/linux/redhat/
조금만 스크롤 하면 이런 화면이 나온다.

어떤 버전을 다운받을지 플랫폼은 뭔지 아키텍쳐는 뭔지 선택하면 설치 스크립트가 나오는데
난 최신 버전 16과 CentOS7 선택했고 아키텍쳐 확인은 리눅스(centos 7)에서 아래 명령어로 확인한다.
uname -m
난 x86_64가 나왔다.
그럼 아래와 같은 스크립트가 나온다. 복사한다.

sudo yum install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm
sudo yum install -y postgresql16-server
sudo /usr/pgsql-16/bin/postgresql-16-setup initdb
sudo systemctl enable postgresql-16
sudo systemctl start postgresql-16

리눅스에서 su권한을 획득한 상태에서 첫번째 스크립트 yum을 실행한다.
yum install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm

일단 성공.

두번째 스크립트 실행
yum install -y postgresql16-server

Error가 뜬다. 썩을
yum list | grep postgresql16 명령어로 postgresql16을 설치할 수 있는패키지 항목을 조회해 보았으나 아무것도 없었다.

yum list | grep postgresql15명령어로 확인해보니 뜬다.

계획을 바꿔서 15를 설치해보겠다.
yum install -y postgresql15-server

성공

postgresql15-server 설치가 완료됬다면 /usr/pgsql-15 경로에서 확인이 가능하다.

데이터베이스 setup을 진행한다.
/usr/pgsql-15/bin/postgresql-15-setup initdb
cd /usr/pgsql-15/bin/명령어로 이동해서 실행을 해도 된다.

PostgreSQL 기동
systemctl start postgresql-15
(재기동은 restart를 쓰면된다.)

일단 기초 설치는 이걸로 끝이다.
이제 su계정에서 postgres 계정으로 전환하고 나머지 설정을 손보려한다.

su - postgres
pwd를 실행해보면  현재 위치가 /var/lib/pgsql로 나타난다.

원격접속 설정을 위해 /var/lib/pgsql/15/data/pg_hba.conf 를 vi에디터로 편집한다.
vi /var/lib/pgsql/15/data/pg_hba.conf
아래로 스크롤 해보면 local connection 설정부분이 나온다.

# IPv4 local connections:
host    all             all             127.0.0.1/32            scram-sha-256  요 아래에 0.0.0.0/0... 을 추가한다.
host    all             all             0.0.0.0/0               scram-sha-256


다음은 postgresql.conf파일이다.
마찬가지로 vi에디터로 편집해주자.
vi /var/lib/pgsql/15/data/postgresql.conf

listen_addresses를 보면 localhost가 있는데 이를 *로 바꿔준다.
저장하고 나와서 exit명령어를 입력하여 su계정으로 전환하자.
환경 설정이 바뀌었으니 su계정으로 postgreSQL을 재시작해주자.
exit를 입력하면 다시 su 상태가 된다.

systemctl restart postgresql-15


참고로 처음 제공된 스크립트들 중  systemctl enable postgresql-15는 OS가 기동될때 자동으로 postgresql 서버가 실행되도록 하는 설정이다.
실행하면 이런 처리 결과를 확인할 수 있게 된다.



어찌 저찌 외부 접속까지는 다 됐는데 그럼 남은건 외부에서 어떤 계정을 사용해서 접근하게 만들지를 설정할 단계다.
다시 postgres계정으로 전환하자
su - postgres

그리고 psql을 실행 후 postgres=# 상태가 확인된다면
\du  명령어를 입력해 사용자명, 권한, 소유한 데이터베이스 등의 정보를 표시해 본다.

나님이 슈퍼유저란 뜻이다.
당연히 계정 생성이 가능하다.
외부 접속을 하려면 새로운 db user 를 생성하거나 postgres(관리자)의 password 를 설정해야 한다.
난 둘 다 하겠다.

먼저 관리자 password 설정
alter user postgres with password '원하는비밀번호입력';
ALTER ROLE 라고 뜨면 성공이다.

다음은 외부 접속용 아이디 생성
create user tester with password ' 원하는비밀번호입력 ';
CREATE ROLE 라고 뜨면 성공이다.

DB도 만들어주자
createdb testdb;
간단하다.


이제 내 PC(맥)에서 접속을 해보자.
(솔직히 이 부분에서 몇 시간을 소비해버렸다. 난 분명히 postgresql.conf 파일의 설정을 외부 접속 가능하도록 위에서 진행했는데... 한참 삽질 하다보니 바뀌어 있지 않았다. -_-)

pgAdmin4를 실행해서 Servers에서 마우스 오른쪽을 눌러보니 Register가 보인다. 그 옆엔 Server가 있으니 이를 선택하자


그럼 Register 새창이 뜨고 여기에 Name에 내가 원하는 식별 이름을 적어주자.


그 옆 탭은 Connection 당연히 postgreSQL이 설치된 ip를 host name/address에 적어주고
port는 기본 port인 5432
db는 내가 생성한 db인 testdb 
username도 내가 일전에 생성한 userid
password도 아까 user생성할 때 함께 적었던 그 비번을 넣으면 된다.
그리고 Save

그럼 생성한 user를 이용해 외부 DB에 최종적으로 연결된 모습을 확인할 수 있다.


다음엔 생성된 계정 tester를 이용해 testdb에 접근하여 데이터를 조회해오는 샘플 프로젝트를 진행해 볼 예정이다.
https://hippalus.tistory.com/650

반응형
Posted by Hippalus
,

반응형

생각해보자.
DB에 프로그램이 접속해서 CRUD 행위가 가능하려면 이용자가 있어야 하는 법
그런데 난 postgreSQL을 처음 설치할 때 이용자는 만들었는데 프로그램이 접속해서 사용하는 이용자는 만든 기억이 없다.

그러니 일단 만들자.
Databases아래에 Login/Group Roles가 있다. 여기서 또 마우스 우측 버튼을 누르고 Create > Login/Group Roles를 선택


그러면 늘 그러하듯 새창이 뜬다.
이름을 지정해주자 난 programuser라고 하련다.


우측탭으로 이동하면 Password를 입력할 수 있다.
그 아래 expires는 그냥 놔두면 never expire라 하니 건드리지 말아야겠다.
Connection limit는 -1인걸로 미루어 이게 현명한 처사 같다.
접속 제한이 없단 말 이겠지. 웹같은 환경에선 다수가 동시 접속이 일어날테니 -1로 놔두는게 맞지 싶다.


Save하면 끝.
그런데 이렇게 하고 프로그램 구현을 하면 제대로 안 된다.
이유는 각 테이블의 권한이 없기 때문이다.
따라서 이번 포스팅은 그냥 pgAdmin으로 계정 생성하는 목적 정도로만 공유하고 다음 포스팅에서 제대로 어플리케이션에서 사용 될 계정을 만들고 설정도 다뤄보려 한다.

반응형
Posted by Hippalus
,

반응형

일단 내 상황은 이렇다.
사용해본 DB는 MSSQL을 15년 이상 주력사였고 최근 Oracle(과거 SI 잠깐 했을 당시에 2년 정도 토드사용해본게 전부였지만 최근 다시 사용) 그 외 잡다한 MySql, MSACCESS(ㅋㅋㅋ), DB같지도 않은 파라독스(S모 카드사에서 처음 접하고 개깜놀) 대충 이렇다.

이 상황에서 처음 PostgreSQL을 설치해보았다.
그리고 첫 실행을 하고자 설치된 폴더로 이동해보니 딱 저렇게 보인다.
느낌적으로 pgAdmin 4라는 툴이 MSSQL의 SQL Server Management Studio스러워 보인다.
SQL Shell은 아마 커맨드 라인으로 실행해주는 paAdmin 4일듯 싶다.

 

pgAdmin 4를 냅다 실행해본다.

 

음 역시나가 역시다.
탭 영역을 보니 대시보드가 있고 그 옆에 프로퍼티, 시퀄 등등등이 있다. 
눈깔을 좌측 트리로 이동해보자 Servers가 있다.


MSSQL처럼 UI가 마음에 드는구나.
트리를 펼쳐서 PostgreSQL 16을 눌러보자.
팝업윈도우가 뜬다.


아까 설치할 때 기록한 비번을 넣어준다.


이러니 Databases와 Roles, Tablespaces 등등 기본 시스템 DB쪽으로 추정된다.


자 그럼 이제 내 디비를 만들어봐야겠제?
PostgreSQL 16에서 마우스 오른쪽을 눌러본다. Create가 있다 그 옆에 Database가 있구나.
정말 직관적이다.(MSSQL 이용자라면 너무 쉽다.)
있어야 할 곳에 기능이 있다. 그게 MS의 최대 장점 아닌가?


Database를 선택하니 역시나 창이 하나 뜬다.
뻔하다. DB명 만들어주고 저장하면 그만이다.
그 옆 Definition, Security 등을 눌러봤지만 뭘 하는건진 아직 모르겠다. 
일단 DB나 만들자. Save 전에 SQL탭이 눈에 들어온다.
뻔하다. 내가 만드는 액션의 SQL문이겠지. 그리고 저 SQL문은 SQL Shell 프로그램에서 동작시키거나 SQL management처럼 쿼리분석기에서 실행시킬 수 있을것이다.
그냥 저런것도 보여주는구나 하고 넘기자. SAVE

 

Save를 하고 나니 내 첫 PostgreSQL DB가 생성된게 보인다.


그럼 테이블을 만들어 보자.
테이블이야 뭐 뻔하다 Schemas에 있겠지
역시나 Tables가 있다.
만들려면? 뻔하지 마우스 오른쪽 눌러보자.
Create가 나온다. 그리고 Table


그러면 Database 처럼 Table 생성창이 뜬다.
이름은 게시판이 제일 만만하니 board_tbl이라 정해준다.
그다음은? 당연히 컬럼이다. General 옆에 Columns가 보인다. 그거 누른다.


난 매우 간단하게 기준키 필드는 integer, 제목은 varchar, 내용은 text나 varchar로 만들고자 한다.
그런데 varchar가 안보인다.
이건 좀 당황스럽다.


chatGPT센세에게 물어보고 싶지만 내가 쓰는 버전은 3.5버전이므로 최신 정보는 알 수 없을 것 같다.
claude.ai 티쳐에게 물어보자.

16버전에선 varchar대신 text를 사용하는게 좋단다.
초기 버전엔 varchar를 제공했지만 요즘은 그냥 닥치고 text쓰란다.
그럼 varchar와 text가 아무런 차이점이 없다란 말인데... 흠..일단 믿어보자.

그런데 text[] 이건 뭐지? 배열인데...
또 claude.ai 티쳐에게 퀘스쳔

{'태그1', '태그2'...}이런식을 저장하는 자료형이란다.
대충 PostgreSQL이 일반적인 RDBMS가 아니라는말은 익히 들어왔었던지라 지리정보나 그따위 정보에 특화된 필드가 아닐까 추측해본다.
조금 구글링 해보니 JSON형도 있던데... 음... 그렇군 
옥히 도키

b_idx는 integer로 PK로 설정하고 다른 제목과 내용 역시 Not NULL로 설정한다. 기본값은 없다.

Save


저장하고 나니 역시나 좌측 트리 영역에 테이블과 필드가 잘 나타난다.


이제 데이터를 insert해보자
ANSI문법은 언제나 통하니 간단하게 insert문을 실행해보자.
드래그 후 F5(execute script)를 실행하니 error가 뜬다.


이런.. b_title인데 필드명 오타를 냈나보다.
보자.. 테이블 스키마 수정은... 뭐 뻔하지 마찬가지로 테이블의 board_tbl에서 마우스 우측 눌러 Properties를 누르면 되겠지


역시나다. b_titile 오타 i를 하나 제거하자
Save


저장하기 전 SQL문을 구경해보자
좀 특이하다. 이게 ANSI문법인가.
MSSQL과는 좀 다르다.
ALTER TABLE IF EXISTS.... RENAME 뭐 그런가보다.


다시 SQL문을 실행하자.
그 전에 나는 그냥 select 문도 추가하고 F5를 실행하였는데 오류가 난다.
아마 오라클처럼 ;를 넣어줘야 할 것 같다.


정상적으로 insert, select가 실행된다.
다시 한 번 실행해보면 b_idx값이 똑같이 1로 넣으니 당연히 duplicate 오류가 뜬다.


2로 바꾸고 실행하면 잘 넣어지고 잘 보여진다.

일단 첫인상.
매우 직관적이다.
MSSQL을 사용해봤다면 토드를 사용할 때와 다르게 친숙한 느낌을 받을 수 있다.
여기에 뭐랄까 script같은 느낌도 들고...
조만간 회사 오라클 DB를 PostgreSQL로 바꾸게 될 지 모르겠다.
대공사겠지만.

반응형
Posted by Hippalus
,