Recommanded Free YOUTUBE Lecture: <% selectedImage[1] %>
Docbook 원문

Real Time Signal (2)

Real Time Signal (2)

윤 상배

dreamyun@yahoo.co.kr

교정 과정
교정 0.82003년 8월 24일 23시
최초 문서작성


1절. 소개

지난번 Real Time Signal에 이은 두번째 기사이다.

지난 기사가 RTS의 개념설명에 중점을 두었다면 이번 기사는 RTS의 실질적인 응용에 중점을 두고 있다.


2절. 네트워크 프로그램과 RTS

지난번 기사는 UDP를 이용한 네트워크 프로그래밍 예제 였는데, RTS의 개념을 설명 하는데에는 유용했지만 많은 수의 네트워크 프로그램이 주로 TCP를 이용해서 작성된다는 것을 감안하면 그리 실용성 있는 예는 아니었다.

이번에는 RTS를 이용한 그럴듯한 TCP기반의 네트워크 프로그램을 작성 하도록 하겠다. 작성하고자 하는 프로그램은 간단한 채팅 프로그램이다. 뭐 채팅 프로그램이라고 해서 다양한 기능과 다중 채널을 지원하는 그럴듯한 프로그램이 아닌 단일 채널에서 단순히 메시지만 교환하는 기능을 가지도록 작성할 것이다.

이전 기사에서 다루기가 까다롭다는 점이 RTS의 단점이라고 했는데, 이 기사를 읽어 나가면서 왜 다루기 까다로운지 RTS를 제대로 다루기 위해서 어떤 문제를 어떻게 해결해야 하는지에 대한 아이디어를 얻을 수 있을 것이다.

프로그램의 이름은 jchat(-.-)로 하겠다.


2.1절. 프로그램 작동 방식

이 프로그램은 최초 실행시 socket(2)함수를 이용해서 듣기 소켓을 생성한다. 그리고 듣기 소켓에 이벤트가 발생할 경우 RTS를 발생시키도록 설정한다. 이 설정에는 fcntl(2)함수와 시그널관련 함수들이 사용된다.

그러다가 소켓으로 이벤트가 통지 되면 이벤트의 종류를 판단한뒤 데이터를 읽어야 할지 소켓을 종료 시켜야 할지 등을 결정한다. 이벤트가 도착되었을 경우 이벤트를 발생시킨 소켓이 듣기 소켓일 수도 있지만 클라이언트와 연결된 연결소켓일 수도 있으므로 이벤트가 발생했다면 우선 이 이벤트가 듣기 소켓에서 발생한것인지 아니면 연결 소켓에서 발생한 것인지 판단해주어야 한다.

만약 듣기 소켓에서 발생한 이벤트라면 accept(2)를 호출해서 클라이언트외의 연결소켓을 만들고 만들어진 연결 소켓은 RTS를 발생 시키도록 설정해 주어야 한다. 그렇지 않고 연결 소켓에서 발생한 이벤트라면 데이터를 읽어들여서 처리하도록 한다.


2.2절. RTS 대기열의 크기에 따른 문제

당연하지만 RTS 대기열의 크기는 무한하지 않다. 그렇다면 대기열을 몽땅 채우게 되면 어떤일이 일어 나게 될지도 생각해 보아야한다. 물론 이러한 프로그램을 작성할경우 최소한 대기열을 소모하는 일이 없도록 기교를 써서 프로그램을 작성할 것이다.

RTS 시그널 대기열은 프로세스단위로 유지되므로 처리해야 하는 클라이언트의 데이터량을 계산해서 RTS+쓰레드, RTS+fork의 식으로 서버를 작성하면 될것이다. 이경우 거의 필수 적으로 쓰레풀/프로세스풀 프로그래밍 기법을 사용해야 한다.


2.2.1절. RTS 대기열 Overflow

이런 저런 노력에도 불구하고 RTS 대기열 Overflow(이하 RTS Overflow)가 발생할 수 있다. 일단 RTS Overflow상태를 염두에 두고 프로그래밍을 할경우 Overflow상태를 확인할 수 있어야하고 확인했을 경우 필요한 조취를 취해주어야 하는데, 이는 프로그램을 매우 복잡하게 만든다. 프로그램 코드의 복잡도가 증가한다는 점은 RTS의 유일한 단점이다.

RTS Overflow가 발생했다면 반드시 이문제를 해결해 주어야만 하다. RTS Overflow가 발생하면 프로세스로 SIGIO시그널이 전달된다. 이를 위해서 코드를 만들때 SIGIO에 대한 시그널 핸들러를 등록해 주고 SIGIO를 받았다면 RTS Overflow가 발생했다고 판단하고 복구 작업을 해야 한다. 우선은 모든 소켓에 대한 RTS대응을 하지 않도록 설정한후 시그널 대기열로 부터 모든 시그널을 제거해야한다. 그리고 나서 잃어버린 소켓 이벤트를 복구해야 하는데 꾀나 귀찮은 작업이 될 것이라는걸 예상할 수 있다.

그렇다면 RTS Overflow가 발생하지 않도록 하는게 가장 좋은 방법일 것이다. RTS Overflow를 피하기 위해서 시그널 대기열에 있는 소켓파일 지시자당 하나의 이벤트만 허용하도록 하는 커널수정이 이루어졌다. 이 방법을 이용할 경우 시그널 대기열의 크기가 최대 오픈가능한 파일의 수를 초과하지 않는한은 RTS Overflow가 발생하지 않는다. 그러므로 사실상 RTS Overflow를 일어나지 않게 되었다.

다음과 같이 fcntl(2)을 이용해서 하나의 소켓당 하나의 이벤트가 대기열에 들어가도록 조작할 수 있다.

fcntl(sockfd, F_SETAUXFL, O_ONESIGFD);
				
그러나 이 기능들은 최근의 기능들로 운영체제가 지원하는지 확인할 필요가 있다.

여기에서는 대기열 Overflow에 대한 설명은 이정도로 줄이도록 하겠다. 실제 구현은 각자 연구해 보기 바란다. phhttpd라는 RTS를 응용한 웹서버가 있는데 비교적 간단하니 이 쏘쓰를 분석하면 RTS에 대해서 좀더 깊이 있는 이해가 가능할 것이다.


2.3절. 예제

다음은 RTS를 응용한 채팅 예제 프로그램이다. 뭐 그렇다고 해서 복잡한 기능들을 가지고 있진 않다. 그냥 굴러만 가는 프로그램으로 메시지 전달만을 가지고 있는 간단한 프로그램이다.

하나의 클라이언트가 메시지를 전송했을 경우 현재 연결된 모든 클라이언트에게 메시지를 전달해야 하므로 연결된 클라이언트의 정보를 유지할 필요가 있을 것이다. 링크드 리스트로 구현을 하든지 배열을 이용하든지 편한 방법을 이용하면 된다. 필자는 속편하게 STL의 map을 통해서 구현했다. (그런 이유로 g++과 같은 컴파일러를 이용해서 컴파일 해야만 한다)

코드는 단순하므로 주석만으로도 충분히 이해가능 할것이다.

예제 : rts_chat.cc

#include <signal.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <arpa/inet.h>

#include <map>
#include <string>

#ifndef __USE_GNU 
#define __USE_GNU
#endif 
#include <fcntl.h>
#define SOCK_MAX_NUM    1024

using namespace std;

// 듣기 소켓 저장용
static int listen_sockfd;

// 연결된 클라이언트 정보를 유지하기 위한 
// 자료구조와 자료구조 순환을 위한 iterator  
map<int, struct sockaddr_in> msoinfo;
map<int, struct sockaddr_in>::iterator mi;


/*
 * RTS overflow 처리 핸들러
 * 여기에서는 구현되지 않았다. 각자 구현해보기 바란다.
 */
void do_sigio(int signo)
{
    printf("SIGIO : RTS signal queue overflow\n");
    // RTS overflow 처리를 위한 루틴이 들어간다.
}

/* 
 * 시그널 핸들러를 등록한다.
 * 여기에서는 SIGIO에 대한 핸들러만 등록했다. 
 * SIGIO가 발생했을 경우 RTS overflow 처리를 위한 핸들러가 
 * 실행된다.
 */
int init_signal_handler()
{
    struct sigaction sigact;

    sigemptyset(&sigact.sa_mask);
    sigact.sa_flags = SA_SIGINFO;
    sigact.sa_restorer = NULL;
    sigact.sa_handler = do_sigio;
    if (sigaction(SIGIO, &sigact, NULL) < 0)
    {
        printf("sigio handler error\n");
        exit(0);
    }
}

/*
 * 인자로 주어진 파일지정자 fd에 대해서
 * RTS대응하도록 만든다.
 */
int setup_sigio(int fd)
{
    if (fcntl(fd, F_SETFL, O_RDWR|O_NONBLOCK|O_ASYNC) < 0)
    {
        printf("Nonblocking error\n");
        return -1;
    }
    if (fcntl(fd, F_SETSIG, SIGRTMIN) < 0)
    {
        printf("Couldn't set signal %d on %d\n", SIGRTMIN, fd);
        return -1;
    }    
    if (fcntl(fd, F_SETOWN, getpid()) < 0)
    {
        printf("Could'nt set owner %d on %d\n", getpid(), fd);    
        return -1;
    }
    return 0;
}

/*
 * setup_sigio에 대한 포장함수
 */
void setup_sigio_listeners(int fd)
{
    if (setup_sigio(fd) != 0)
    {
        printf("setup sigio error : %d\n", fd);
        exit(0);
    }
}

/*
 * 듣기 소켓(endpoint socket)를 생성하고 
 * 만들어진 듣기 소켓에 대해서 RTS대응 하도록 한다.
 * 일반적인 socket -> bind -> listen 과정을 거친다.
 */
int get_listener_fd()
{
    int clilen;
    int state;

    struct sockaddr_in serveraddr;
    clilen = sizeof(serveraddr) ;
    listen_sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_sockfd < 0)
    {
        printf("Socket create error\n");
        return -1;    
    }

    bzero(&serveraddr, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serveraddr.sin_port = htons(1234);

    state = bind (listen_sockfd, (struct sockaddr *)&serveraddr, 
                                sizeof(serveraddr));
    if (state < 0)
    {
        printf("Bind error\n");
        return -1;
    }
    state = listen(listen_sockfd, 5);
    if (state < 0)
    {
        printf("Listen error\n");
        return -1;
    }
    printf("Listen Socket Create %d\n", listen_sockfd);
    return setup_sigio(listen_sockfd);
}

/*
 * accept(2)를 이용해서 클라이언트와의 연결을 만든다.  
 * 만들어진 소켓 지정자에 대해서는 
 * setup_sigio_listeners()사용자 정의 함수를 이용해서 
 * RTS에 대응하도록 변경한다. 
 */
int get_connect_fd()
{
    int sockfd;
    socklen_t clilen; 
    struct sockaddr_in clientaddr;

    clilen = sizeof(clientaddr) ;
    sockfd = accept(listen_sockfd, (struct sockaddr *)&clientaddr, 
                    &clilen);
    if(sockfd < 0)
    {
        printf("Acceft error\n");
        return -1;
    }
    msoinfo[sockfd] = clientaddr;
    setup_sigio_listeners(sockfd);
    printf("Accept %d\n", sockfd);
}

/*
 * 클라이언트가 '/quit' 를 입력해서 연결을 끊거나 
 * 오류로 인해서 비정상 종료할경우 
 * 현재 연결되어 있는 다른 클라이언트에게 종료 메시지를 
 * 전송한다.  
 */
int chat_close(int fd)
{
    char buf[256] = {0x00, };
    if (read(fd, buf, 255) <= 0)
    {
        mi = msoinfo.begin(); 
        while(mi != msoinfo.end())
        {
            sprintf(buf,"Disconnect : %s\n", inet_ntoa(mi->second.sin_addr));
            if (mi->first != fd)
                write(mi->first, buf, 256);
            *mi++;
        }
        msoinfo.erase(msoinfo.find(fd));
        close(fd);
    }
}

/*
 * 채팅상황을 출력한다. 
 * 현재 연결된 클라이언트의 IP와 Port번호를 출력한다.
 */
void chat_info(int fd)
{
    char buf[256] = {0x00, };
    int i = 1;
    mi = msoinfo.begin();

    while(mi != msoinfo.end())
    {
        sprintf(buf, "%d : %s (%d)\n",i, inet_ntoa(mi->second.sin_addr), 
                            mi->second.sin_port);
        write(fd, buf, 256);
        i++;
        *mi++;
    }    
}

/*
 * 채팅 메시지를 전송한다.
 * 자신을 제외한 모든 클라이언트에게 메시지를 
 * 전송한다.
 * '/quit', '/info'는 채팅명령으로 
 * 각각 '종료', '채팅정보'를 출력한다.
 */
int send_chat_msg(int fd)
{
    char buf[256] = {0x00, };
    if (read(fd, buf, 255) <= 0)
    {
        chat_close(fd);
    }
    else
    {
        if (strncasecmp(buf,"/quit", 5) == 0)
        {
            chat_close(fd);
        }
        else if(strncasecmp(buf,"/info", 5) == 0)
        {
            chat_info(fd);
        }
        else
        {
            mi = msoinfo.begin(); 
            while(mi != msoinfo.end())
            {
                if (mi->first != fd)
                    write(mi->first, buf, 256);
                *mi++;
            }
        }
    }
}

int main()
{
    struct siginfo si;
    sigset_t set;
    int clilen;
    int ret;

    // SIGRTMIN가 BLOCK되도록 설정한다.
    sigemptyset(&set);
    sigaddset(&set, SIGRTMIN);
    sigprocmask(SIG_BLOCK, &set, NULL);

    init_signal_handler();
    // 듣기 소켓을 생성한다.
    if(get_listener_fd() < 0) 
    {
        printf("Socket Create error\n");
        exit(0);
    }

    while(1)
    {
        printf("sigwait\n");
        // 소켓으로 부터 이벤트를 기다린다. 
        ret = sigwaitinfo(&set, &si);
        if (ret == SIGRTMIN)
        {
            printf("ok signal %d : %d\n", si.si_fd, si.si_code);
            // 만약 듣기 소켓에 발생한 이벤트라면 
            // accept(2)를 호출해서 연결 소켓을 생성한다. 
            if (si.si_fd == listen_sockfd)
            {
                get_connect_fd();
            }
            // 그렇지 않으면 연결 소켓에서의 이벤트이므로
            // 데이터를 읽어서 처리한다. 
            else
            {
                send_chat_msg(si.si_fd);
            }
        }
    }
}
			
위의 코드를 컴파일한 후 실행시키고 테스트를 해보도록 하자. 별도의 클라이언트를 준비할 필요 없이 telnet만을 이용해서 테스트 가능하다.