BLOG main image
분류 전체보기 (117)
Hidden (1)
five senses (0)
safe system (77)
code (35)
database (1)
Link (0)
Visitors up to today!
Today hit, Yesterday hit
daisy rss
tistory 티스토리 가입하기!
2008. 7. 18. 05:00


http://www.popome.com/?p=27

리눅스에서 TCP/IP 데몬 개발을 단계적으로 설명하고자 한다.

총 8단계에 걸쳐서 연재를 할 예정이고 각 단계별로 간단한 소스와 설명을 통해서 데몬 프로그램 기법을 정리할 예정이다.

모든 단계별 소스는 아주 간단하게 동작하도록 작성하였으나 보고 이해하기만 하면 절대로 그 의미를 100% 자신의 것으로 만들 수 없다. 앞으로 나오는 8단계는 영어 알파벳이라 생각하고 완젼히 외워야 한다. 복사해서 실행하지 말고 직접 타이핑 쳐서 컴파일 하기 바란다.

1단계 : fork()
2단계 : signal()
3단계 : thread
4단계 : 간단한 tcp 서버 데몬 #1
5단계 : 간단한 tcp 서버 데몬 #2
6단계 : 복수 클라이언트 접속용 tcp 서버 데몬 #1
7단계 : 복수 클라이언트 접속용 tcp 서버 데몬 #2
8단계 : 복수 클라이언트 접속용 tcp 서버 데몬 #3

물론 8단계가 완벽한 데몬형태가 되지는 않지만 8단계의 모든 소스를 조합한다면 충분히 완벽한 데몬을 제작하는데 어렵지 않을것이다.

1단계 : fork()

대부분의 네트웍 데몬을 공부하게 되면 가장 먼저 이해해야 하는 함수가 fork()다. 이 함수는 child processor를 생성하는 함수로 간단한 셈플을 아래 작성했다.

아래의 프로그램을 실행하면 재미있는 결과가 나올것이다.

물론 결과에서 프로세서 아이디에 해당하는 숫자(31332,31332) 는 실행하는 컴퓨터에 따라서 다르게 표시될것이다.

프로세서를 이해하지 못하는 사람이라면 if문으로 싸여진 출력문 중에서 조건이 맞는 하나만 출력되리라 생각되지만 실제로 실행결과는 두가지 모두 출력하게 된다.

하지만 fork() 위로는 출력문은 하나만 출력된다.
이는 fork() 가 호출되기 직전까지는 하나의 프로세서에서 fork()를 만나면서 두개의 프로세서로 나뉘어진다는 의미다.
손오공이 분신술로 2마리가 되는것과 같다.(^^)

fork()는 불려지는 순간 호출한 프로세서를 복제하게 되고 그 이후는 각각 따로 동작하게 된다는 점이 중요하다.

그래서 cnt값을 출격하는것이 Parent와 Child가 다르게 찍히게 되는것이다. Sleep이라는 함수를 이용해서 약간의 시간차 출력문으로 cnt의 변화값을 출력하게 되는데 초기 10의 값은 두 프로세서가 동일하게 가지고 있지만 부모프로세서는 중간에 0으로 변경을 하게 되어 마지막에 프로그램이 종료하는 순간 출력문은 10, 0 두가지 모두 출력하게 되는 것이다. 프로세서 번호를 따져보면 Parent와 Child가 초기에 같은 변수가 각각 따로 변함을 알게 된다.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> 

void main( int argc, char *argv[] )
{
        int      cnt = 10;
        pid_t   pid; 

        printf( “Call fork()” ); 

        pid = fork(); 

        printf( “fork() : %d“, pid ); 

        if ( pid )      // parent
        {
                cnt = 0;
                printf( “This is parent : %d“, getpid() );
                sleep( 5 );
                printf( “server end” );
        }
        else
        {
                printf( “This is child : %d, %d“, getpid(), getppid() );
                sleep( 3 );
                printf( “child end” );
        } 

        printf( “End process : %d %d“, getpid(), cnt );
}

=======================================
Call fork()
fork() : 31332
fork() : 0
This is child : 31332, 31331
This is parent : 31331
child end
End process : 31332 10
server end
End process : 31331 0
======================================


2단계 : signal()


통신 프로그램 생각할때 먼저 떠올리는건 서버와 클라이언트 즉 리눅스와 윈도우 아니면 서버가 서버의 통신을 생각한다.

통신은 이렇게 두개의 프로그램 또는 다른 서버간에 통신뿐 아니라 1단계에서 말한 프로세서간 내부 통신도 매우 중요한 부분이다.


통신 프로그램을 짤때 단순히 클라이언트가 서버에 접속해서 데이타를 요청하고 받는 단순 구조인 경우라면 크게 신경을 안써도 되겠지만 만일 접속된 클라이언트간에 데이타를 주고 받는 상황이라면 내부 프로세서가 통신은 매우 중요한 부분이다.

게임 프로그램이나 메신저와 같이 클라이언트간에 데이타를 주고 받거나 또는 시스템 상에서 데몬의 종료나 환경 변수의 변경등을 알려주는 기능도 내부 통신이라고 할 수 있다.


아파치 웹서버의 파라메터나 도메인 정보가 바뀌었을때 데몬을 재 시작해도 상관없지만 보통의 경우는 killall -HUP httpd 이런식으로 해당 데몬에게 신호를 주는 방식도 있다.


내부 프로세서가 통신은 IPC라고 하는 칭하는데 3가지의 형태가 있다.

1. tcp, udp, pipe와 같이 데이타를 전송하는 방식

2. semaphore, mutex 와 같이 데이타 동기를 맞추는 방식

3. signal, wait 과 같이 신호를 전송하는 방식

더 많은 분류와 함수도 있겠지만 그 목적별로 나눈다면 위의 가지수가 될것이다.


이번에 설명하는 signal은 데모을 만들때 중요한 기능이므로 우선 설명하기로 한다.


예제 프로그램은 kill 호출이 되기전까지는 1초씩 기다리면서 무한루프를 도는 구조이다. SIGINT는 Ctrl+C를 막는 기능이고 SIGHUP은 일반적으로 프로그램에게 외부 환경 파일이 변경됨을 알려주는 기능으로 많이 사용한다. SIGTERM은 kill 신호를 받았을때 바로 죽지 않고 종료를 안전하게 처리하는 기능을 수행한다.


정확한 코딩방식으로는 signal로 받은 함수를 원상 복귀시키는 로직도 포함이 되어야 하지만 일반적으로 프로그램이 종료하면 자동 복귀가 되므로 생략하는 경우가 많다.


SIGCHILD는 fork로 생성된 프로세서가 종료될때 부모프로세서에게 종료됨을 알리고 부모가 wait도는 waitpid 형태의 함수를 호출해서 종료 상황을 인식할 수 있도록 프로그램을 짤 수 있다.


하지만 SIG_IGN 를 지정하면 해당 신호를 무시하여 간단히 처리하는것도 방법이다. 이렇게 처리하면 fork()로 생성된 child 가 종료시 defunct 형태로 ps 로 확인되는 zombi 프로세서의 생성을 자동적으로 처리해주는 기능을 수행하게 된다.


alarm 함수는 SIGALM을 호출하게 되어 타이머로서 이용할 수 있다.

block 모드로 동작하는 read, write, select 와 같은 함수에서 해당 기능을 이용하면 데이타 전송시 무한 대기로 빠지는 것을 방지할 수 있다.


단 kill -9 와 같이 강제 종료의 경우는 신호가 잡히지 않고 강제 종료가 되게 되므로 일반적인 kill 신호로 데몬을 종료시키면서 안전하게 종료하는 방법으로 개발해야 한다.


예제 프로그램을 실행하고나서 Ctrl+C 키를 누르거나 다른 텔넷창에서 kill -HUP process-id 과같이 테스트를 해볼 수 있다.

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <signal.h>  


int sig = 0;  


void sigRtn( int s )

{

    sig = s;

    printf( "Receive signal : %d", sig );

}  


void main()

{

    signal( SIGINT,  sigRtn );

    signal( SIGHUP,  sigRtn );

    signal( SIGTERM, sigRtn );

    signal( SIGALRM, sigRtn );

    signal( SIGCHILD, SIG_IGN );  


    alarm( 10 );  


    while( sig != SIGTERM )

    {

        sleep( 1 );

    }  


    printf( "Receive kill" );

}


3 장 : thread


많은 데몬 개발 또는 통신 프로그램에서 fork()를 많이 이용한다.

하지만 필자는 fork보다는 thread 를 더 선호하는 편이다.


fork()는 단순 클라이언트의 경우에 상당히 유리하고 간단하게 개발할 수 있는 반면에 클라이언트간에 데이타 공유나 전송에는 오히려 더 어려운 코딩을 해야 하기 때문이다.


fork()가 진정한 child프로세서를 만드는 기법이라면 thread는 윈도우의 개발 방식과 상당히 유사하기 때문에 윈도우 클라이언트와 연동하는 모델의 서버를 개발한다면 thread쪽을 더 권장한다.


thread 가 fork()와 가장 다른점은 함수단위의 스래드 처리가 가능하기 때문에 global 변수나 기타 다른 함수의 데이타 공유가 가능하다는 점이다. 하지만 이런한 편의성은 두 스래드간에 동시 자료 접근으로 인한 결함도 야기된다. 예를 들어 두 스래드가 동시에 트리 또는 큐같은 메모리를 사용하게 되면 데이타의 연결고리가 끝어질 수 있는 단접이 있다. 이런 경우에 이후에 설명할 예정인 세마포 또는 뮤택스와 같은 동기 함수를 이용하는 코딩 기법이 필요하게 된다.


이번 예제에서 가장 중요한 포인트는 cnt라고 하는 global변수가 fork()방식으로 했을때 parent와 child가 각각 다른 변수로 인식되는 반면 thread에서는 같은 변수로 사용이 되는 접을 알수 있는 소스다.


아래의 소스를 실행해보면 cnt가 중간에 바뀌고 나서 thread에서 출력시에도 바뀐 값을 똑같이 출력하는것을 확인 할 수 있다.


다중 타스크 프로그램에서 fork와 thread어느쪽이 정답이하고는 할수 없다.

그건 while 이냐 for 냐의 구분과 비슷할 것이다.

하지만 이 두 방식의 차이를 정확히 이해한다면 데몬 개발에서는 상당히 편리한 코딩을 할 수 있게 된다.


#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <pthread.h>


int cnt = 0;


void *fun( void *arg )

{

printf( “This is child : %d“, getpid() );

sleep( 3 );

printf( “child end : %d“, cnt );


pthread_exit( NULL );

return 0;

}


void main( int argc, char *argv[] )

{

pthread_t t;

pthread_create( &t, NULL, fun, NULL );


cnt = 10;


printf( “This is parent : %d

“, getpid() );

sleep( 5 );

printf( “server end : %d

“, cnt );

}


=============================

This is child : 12325

This is parent : 12323

child end : 10

server end : 10



지난 3장까지는 기초적인 함수와 개녕이였고 이번 장부터는 실질 서버의 모습을 소개합니다.


첨부된 소스는 최소한의 코딩으로 만든 서버로 클라이언트가 접속한뒤 문자열을 송신하면 클라이어트에 받은 문자갯수를 돌려주고 종료하는 프로그램입니다.


테스트를 위해서는 telnet 을 클라이언트로 이용하시면 됩니다.

서버 기동시 포트번호를 파라메터로 입력하시면 되는데 주의할점은 1024 이하의 번호를 쓰기 위해서는 root로 로그인되어야 하고 이후번호의 경우는 무관합니다. linux의 기본 보안 정책이고..


tcp 서버를 만들기 위해서 가장 기본적으로 사용하는 함수는

socket, bind, listen, accept, recv, send, close


클라이언트라면

socket, connect, send, recv, close


서버의 경우는 bind, listen, accept 가 추가됩니다.


socket은 통신을 하기 위한 핸들을 생성하는 함수입니다.

파일 오픈과 동일한 기능이라고 생각하면 됩니다.

실재로 socket에서 리턴된 핸들에다 read, write와 같은 파일 명령으로 데이타를 송수신하여도 동일한 결과로 동작하게 됩니다.

close를 이용하는 걸 봐도 같다는걸 알수 있습니다.


bind는 뭔가를 묶는다는 뜻이므로 해당 어드레스와 포트를 시스템에게 예약하는 명령어입니다.


listen은 클라이언트가 젒속하도록 대기상태로 만드는 것입니다.

실제로 listen 이후부터는 클라이언트가 접속이 가능합니다.


일반적으로 listen( s, 5 )라고 5라는 숫자가 있는 리눅스의 기본 queue의 갯수가 5입니다. 변경할 필요도 없으면 크게 한다고 해서 실제로 커지지도 않습니다. 큐의 개념은 accept 로 빼내기전에 버퍼링되는 커넥션이라고 생각하면 됩니다.


accept라는것을 접속대기중인 클라이언트를 실제로 프로그램과 접속을 시켜주는 명령어입니다. accept의 리턴값이 소켓이고 새로운 소켓번호를 통해서 접속된 클라이언트와 송수신을 하게 되는 것입니다.


이후는 recv, read, send, write의 함수를 이용해서 통신을 하면됩니다.


샘플소스는 무조건 클라이언트를 기다라다가 접속이후 문자열을 기다리다가 문자열이 들어오면 문자열을 출력한뒤 클라이언트로 해당 문자열의 길이를 반환하고 종료하는 프로그랭입니다.


실제로 서버라기 보다는 수신 대기프로그램 및 수신 테스트 프로그램이라고 생각하면 됩니다.


#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <unistd.h>  


#include <netdb.h>

#include <sys/socket.h>

#include <arpa/inet.h>  


int main( int argc, char *argv[] )

{

    if ( argc != 2 )

    {

        printf( "Usage : %s port", argv[0] );

        return -1;

    }  


    struct sockaddr_in  addr;  


    memset( &addr, 0, sizeof(addr) );  


    addr.sin_addr.s_addr = htonl( INADDR_ANY );

    addr.sin_port = htons( atoi( argv[1] ) );  


    int s = socket( AF_INET, SOCK_STREAM, 0 );  


    if ( !bind( s, (struct sockaddr *)&addr, sizeof(struct sockaddr_in) ) )

    {

        if ( !listen( s, 5 ) )

        {

            int S = accept( s, NULL, NULL );  


            if ( S != -1 )

            {

                char buff[1024];

                int  size;  


                memset( buff, 0, sizeof(buff) );

                size = read( S, buff, sizeof(buff) );  


                printf( "Read : %s", buff );  


                sprintf( buff, "Read %d bytes", size );

                write( S, buff, strlen(buff) );  


                close( S );

                printf( "Disconnect" );

            }

            else

            {

                printf( "accept error" );

            }

        }

        else

        {

            printf( "listen error" );

        }

    }

    else

    {

        printf( "bind error" );

    }  


    close( s );  


    return 0;

}