네트워크 프로그래밍의 기초 Ⅱ

내용

   * 클라이언트 서버의 배경
   * 간단한 스트림서버
   * 간단한 스트림클라이언트
   * 데이터그램 소켓
   * 블로킹
   * select()--동기화된 중복입출력, 대단하군!
   * 참고사항

-----------------------------------------------------------

요즘은 클라이언트-서버가 판치는 세상이죠~~ 네트워크에 관한 모든 것은 서버 프로세스를 요청하는 클라이언트 프로세스로서 다루어진다. 텔넷을 이용하여 23번포트에 접속하는 (클라이언트)것은 서버프로그램(telnetd)을 작동시키게 되는 것이며 이 서버 프로그램은 들어오는 각종 신호를 받아들여서 당신의 텔넷 접속을 위하여 로그인 프롬프트를 주게 되는 것이다. 등등.

주목할점은 클라이언트와 서버간에는 SOCK_STREAM이든, SOCK_DGRAM이든지간에 같은 것으로만 된다면 의사소통이 된다는 것이다. 좋은 예들은 telnet-telnetd,ftp-ftpd, 또는 bootp-bootpd 등이다. ftp를 쓴다면 반드시 상대편에 ftpd가 돌고 있다는 것이다.

보통 호스트에는 하나의 서버 프로그램이 돌고 있게 된다. 그리고 그 서버는 fork()를 이용하여 다중의 클라이언트를 받게 되는 것이다. 기본적인 루틴의 구조는 다음과 같다. 서버는 접속을 대기하다가 accept()를 호출하게 되며 그 때 fork()를 이용하여 자식 프로세스를 만들어내어 그 접속을 처리하게 된다. 이것이 바로 다음에 소개될 예제 서버 프로그램의 구조이다.

-----------------------------------------------------------

간단한 스트림 서버

이 서버가 하는 일은 오직 스트림 접속을 하게 되는 모든 클라이언트에게 "Hello, World!\n"을 출력해 주는 것이다. 이 서버를 테스트하기 위해서는 하나의 윈도우에서 이 서버를 실행시켜 놓고 다른 윈도우에서 텔넷 접속을 시도해 보는 것이다.

    $ telnet remotehostname 3490

hostname 은 서버 프로그램이 작동된 호스트의 이름이다.
서버 프로그램 코드

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
#define MYPORT 3490    /* the port users will be connecting to */
#define BACKLOG 10     /* how many pending connections queue will hold */
main()
{
   int sockfd, new_fd;  /* listen on sock_fd, new connection on new_fd */
   struct sockaddr_in my_addr;    /* my address information */
   struct sockaddr_in their_addr; /* connector's address information */
   int sin_size;
   if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
            perror("socket");
            exit(1);
   }
   my_addr.sin_family = AF_INET;         /* host byte order */
   my_addr.sin_port = htons(MYPORT);     /* short, network byte order */
   my_addr.sin_addr.s_addr = INADDR_ANY; /* auto-fill with my IP */
   bzero(&(my_addr.sin_zero), 8);       /* zero the rest of the struct */
   if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr))        == -1) {
            perror("bind");
            exit(1);
   }
   if (listen(sockfd, BACKLOG) == -1) {
            perror("listen");
            exit(1);
   }

   while(1) {  /* main accept() loop */
        sin_size = sizeof(struct sockaddr_in);
        if ((new_fd = accept(sockfd, (struct sockaddr *)&their_addr, \
             &sin_size)) == -1) {
                perror("accept");
                continue;
        }
        printf("server: got connection from %s\n", \
        inet_ntoa(their_addr.sin_addr));

        if (!fork()) { /* this is the child process */
        if (send(new_fd, "Hello, world!\n", 14, 0) == -1)
                perror("send");
                close(new_fd);
                exit(0);
        }
        close(new_fd);  /* parent doesn't need this */
        while(waitpid(-1,NULL,WNOHANG) > 0); /* cleanup childprocesses */
        }
    }

이 코드는 문법상의 단순함을 위하여 하나의 커다란(내 생각에) main()에 모든 것이 들어가 있다. 만약에 이것을 잘게 잘라서 작은 여러개의 함수로 구성을 하는것이 좋다고 생각된다면 그래도 된다. 다음의 클라이언트 코드를 이용한다면 이 서버로부터 문자열을 받아 낼수도 있다.

-----------------------------------------------------------

간단한 스트림 클라이언트

이녀석은 서버보다 더 쉬운 코드이다. 이 프로그램이 하는 일은 명령행에서
지정된 주소에 3490번 포트에 접속하여 서버가 보내는 문자열을 받는 것 뿐이다.

    #include <stdio.h>
    #include <stdlib.h>
    #include <errno.h>
    #include <string.h>
    #include <netdb.h>
    #include <sys/types.h>
    #include <netinet/in.h>
    #include <sys/socket.h>
    #define PORT 3490    /* the port client will be connecting to */
    #define MAXDATASIZE 100 /* max number of bytes we can get at once */
    int main(int argc, char *argv[])
    {
     int sockfd, numbytes;
     char buf[MAXDATASIZE];
     struct hostent *he;
     struct sockaddr_in their_addr; /* connector's address information */
       if (argc != 2) {
           fprintf(stderr,"usage: client hostname\n");
           exit(1);
       }
     if ((he=gethostby name(argv[1])) == NULL) {  /* get the host info */
            herror("gethostbyname");
            exit(1);
     }
     if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
            perror("socket");
            exit(1);
     }
     their_addr.sin_family = AF_INET;      /* host byte order */
     their_addr.sin_port = htons(PORT);    /* short,network byte order */
     their_addr.sin_addr = *((struct in_addr *)he->h_addr);
     bzero(&(their_addr.sin_zero), 8);  /* zero the rest of the struct */
     if (connect(sockfd, (struct sockaddr *)&their_addr, \
         sizeof(struct sockaddr)) == -1) {
         perror("connect");
         exit(1);
     }
     if ((numbytes=recv(sockfd, buf, MAXDATASIZE, 0)) == -1) {
            perror("recv");
            exit(1);
        }
        buf[numbytes] = '\0';
        printf("Received: %s",buf);
        close(sockfd);
        return 0;
    }

이 클라이언트를 작동하기에 앞서서 서버를 작동시켜놓지 않았다면 connect()함수는 "Connection refused"를 돌려주게 될것이다. 쓸 만하군!

-----------------------------------------------------------

데이터그램 소켓

이에 관해서는 그다지 얘기할 것이 많지 않다. 따라서 그냥 두개의 프로그램을 보여 주겠다. listener는 호스트에 앉아서 4950포트에 들어오는 데이터 패킷을 기다린다. talker는 지정된 호스트의 그 포트로 뭐든지 간에 사용자가 입력한 데이터를 보낸다.

listener.c
    #include <stdio.h>
    #include <stdlib.h>
    #include <errno.h>
    #include <string.h>
    #include <sys/types.h>
    #include <netinet/in.h>
    #include <sys/socket.h>
    #include <sys/wait.h>
    #define MYPORT 4950    /* the port users will be connecting to */
    #define MAXBUFLEN 100
    main()
    {
     int sockfd;
     struct sockaddr_in my_addr;    /* my address information */
     struct sockaddr_in their_addr; /* connector's address information */
     int addr_len, numbytes;
     char buf[MAXBUFLEN];
     if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
           perror("socket");
            exit(1);
        }
        my_addr.sin_family = AF_INET;         /* host byte order */
        my_addr.sin_port = htons(MYPORT); /* short, network byte order */
        my_addr.sin_addr.s_addr = INADDR_ANY; /* auto-fill with my IP */
        bzero(&(my_addr.sin_zero), 8);    
        /* zero the rest of the struct */
        if (bind(sockfd, (struct sockaddr *)&my_addr,             sizeof(structsockaddr)) == -1) {
            perror("bind");
            exit(1);
        }
        addr_len = sizeof(struct sockaddr);
        if ((numbytes=recvfrom(sockfd, buf, MAXBUFLEN, 0, \
            (struct sockaddr *)&their_addr, &addr_len)) == -1) {
            perror("recvfrom");
            exit(1);
        }
        printf("got packet from %s\n",inet_ntoa(their_addr.sin_addr));
        printf("packet is %d bytes long\n",numbytes);
        buf[numbytes] = '\0';
        printf("packet contains \"%s\"\n",buf);
        close(sockfd);
    }

결국 socket()를 호출할 때 SOCK_DGRAM을 사용하게  된것을 주의하고, listen()이나 accept()를 사용하지 않은것도 주의해 봐야 한다. 이 코드가 바로 비연결 데이터그램 소켓의 자랑스러운 사용예인 것이다.

talker.c
    #include <stdio.h>
    #include <stdlib.h>
    #include <errno.h>
    #include <string.h>
    #include <sys/types.h>
    #include <netinet/in.h>
    #include <netdb.h>
    #include <sys/socket.h>
    #include <sys/wait.h>
    #define MYPORT 4950    /* the port users will be connecting to */
    int main(int argc, char *argv[])
    {
     int sockfd;
     struct sockaddr_in their_addr; /* connector's address information */
     struct hostent *he;
     int numbytes;
     if (argc != 3) {
            fprintf(stderr,"usage: talker hostname message\n");
            exit(1);}

     if ((he=gethostbyname(argv[1])) == NULL) {  /* get the host info */
            herror("gethostbyname");
            exit(1);
        }
        if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
            perror("socket");
            exit(1);
        }
      their_addr.sin_family = AF_INET;    /* host byte order */
      their_addr.sin_port = htons(MYPORT);/* short, network byte order */
      their_addr.sin_addr = *((struct in_addr *)he->h_addr);
      bzero(&(their_addr.sin_zero), 8); /* zero the rest of the struct */
      if ((numbytes=sendto(sockfd, argv[2], strlen(argv[2]), 0, \
          (struct sockaddr *)&their_addr, sizeof(struct sockaddr))) ==            -1) {
            perror("sendto");
            exit(1);
        }
      printf("sent %d bytes to %s\n",numbytes, inet_ntoa(               their_addr.sin_addr ) );
        close(sockfd);
        return 0;
    }

이것이 다다. listener를 한 호스트에서 실행 시키고 다른 곳에서 talker를 실행시킨다. 핵가족시대에 어울리는 가족용 오락이 될수도... 앞에서도 얘기했었지만 한가지 작은 내용을 더 말해야 할것 같다. 만약 talker에서 connect()를 호출해서 연결을 했다면 그 다음부터는 sendto(), recvfrom()이 아니라 그냥 send().recv()를 사용해도 된다는 것이다. 전달되어야 하는 호스트의 주소는 connect()에 지정된 주소가 사용되게 된다.

-----------------------------------------------------------

블로킹

블로킹. 아마 들어봤겠지. 그런데 도대체 그게 뭘까? 사실 "잠들다"의 기술용어에 불과한 것이다. 아마도 listener를 실행시키면서 눈치를 챘겠지만 그 프로그램은 그저 앉아서 데이터 패킷이 올때까지 기다리는 것이다. 잠자면서.. recvfrom()을 호출했는데 데이터가 들어온 것이 없다면? 바로 뭔가 데이터가 들어올 때까지 블로킹이 되는 것이다(그냥 거기서 자고 있는 것이다.). 많은 함수들이 블로킹이 된다. accept()는 블록이 된다. recv*()종류들이 모두 블록이 된다.

그들이 이렇게 할 수 있는 이유는 그렇게 할  수 있도록 허락을 받았기 때문이다. 처음에 socket()으로 소켓이 만들어질때 커널이 블록 가능하도록 세팅을 했기 때문이다. 만일 블록할수 없도록 세팅하려면 fcntl()을 사용한다.

    #include <unistd.h>
    #include <fcntl.h>
    .
    .
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    fcntl(sockfd, F_SETFL, O_NONBLOCK);
    .
    .

소켓을 블록할수 없도록 세팅함으로써 정보를 추출하는 데에 효과적으로 socket을 이용할 수 있다. 만일 데이터가 접수되지 않은 소켓에서 데이터를 읽으려고 시도한다면 -1을 결과로 돌려주고 errno를 EWOULDBLOCK 으로 세팅하게 된다. 일반적으로는 이런 식으로 정보를 뽑아 내는 것은 별로 좋은 방식은 아니다. 만일 들어오는 데이터를 감시하기 위하여 이런 방식으로 바쁘게 데이터를 찾는 루틴을 만든다면 이는 CPU 시간을 소모하게 되는 것이다. 구식이다. 보다 멋진 방법은 다음절에 나오는 select()를 사용하여 데이터를 기다리는 식이다.

-----------------------------------------------------------

select() ; 동기화된 중복 입출력. 대단하군!

이건 뭔가 좀 이상한 함수이다. 그러나 상당히 유용하므로 잘 읽어보기 바란다.
다음 상황을 가정해 보자. 지금 서버를 돌리고 있으며 이미 연결된 소켓에서 데이터가 들어오는 것을 기다리고 있다고 하자.

문제없지, 그냥 accept()하고 recv()몇개면 될텐데.. 서둘지 말지어다, 친구. 만일 accept()에서 블로킹이 된다면? 동시에 어떻게 recv()를 쓸 것인가? 블로킹 못하게 세팅한다고? CPU시간을 낭비하지 말라니까. 그러면 어떻게? 더이상 떠들지 말고 다음을 보여주겠다.

       #include <sys/time.h>
       #include <sys/types.h>
       #include <unistd.h>
       int select(int numfds, fd_set *readfds, fd_set *writefds,
                  fd_set *exceptfds, struct timeval *timeout);

이 함수는 화일 기술자의 "집합", 특별히 readfds,writefds,exceptfds등을 관리한다. 만일 일반적인 입력이나 소켓 기술자로부터 읽어 들일수 있는가를 확인하려면 단지 화일 기술자 0과 sockfd를 readfds에 더해주기만 하면 된다.

numfds는 가장 높은 파일 기술자에다가 1을 더해서 지정해야 하며 이번 예제에서는 정규 입력의 0보다 확실히 크게 하기 위해서 sockfd+1 을 지정해야 한다. select()의 결과값이 나올때 readfs는 선택한 파일 기술자 중에 어떤 것이 읽기 가능한가를 반영할 수 있도록 수정되며 FD_ISSET() 매크로를 이용하여 체크할 수 있다.

너무 멀리 나가기 전에 이 "집합"들을 어떻게 관리하는 가에 대해서 얘기를 해야 할것 같다. 각각의 "집합"은 fd_set형이며 다음의 매크로들로 이를 제어할 수 있다.

   * FD_ZERO(fd_set *set) - 파일기술자 집합을 소거한다.
   * FD_SET(int fd, fd_set *set) - fd 를 set에 더해준다.
   * FD_CLR(int fd, fd_set *set) - fd 를 set에서 빼준다.
   * FD_ISSET(int fd, fd_set *set) - fd가 set안에 있는지 확인한다.

끝으로 이 수상한 struct timeval은 또 무엇인가? 아마도 누군가가 어떤 데이터를 보내는 것을 무한정 기다리기를 원치는 않을 것이다. 특정시간마다 아무일도 안 벌어지더라도 "현재 진행중..."이라는 메시지를 터미널에 출력시키기라도 원할 것이다. 이 구조체는 그 시간간격을 정의하기 위해서 사용되는 것이다. 이 시간이 초과되고 그 때까지 select()가 아무런 변화를 감지하지 못한 경우라면 결과를 돌려주고 다음 작업을 진행 할수 있도록 해준다.
struct timeval의 구조는 다음과 같다.

    struct timeval {
        int tv_sec;     /* seconds */
        int tv_usec;    /* microseconds */
    };

기다릴 시간의 초를 지정하려면 그냥 tv_sec에 지정하면 된다. tv_use c에는 마이크로 초를 지정한다. 밀리초가 아니고 마이크로초이다. 마이크로초는 백만분의 일초이다. 그런데 왜 usec인가? u는 그리스 문자의 Mu를 닮았고 이는 마이크로를 의미하는데 사용된다. 함수가 끝날때 timeout에 남은 시간이 기록될수도 있으며 이 내용은 유닉스마다 다르기는 하다.

와우~ 마이크로 초 단위의 타이머를 가지게 되었군! 만일 timeval에 필드들을 0으로 채우면 select()는 즉시 결과를 돌려주며 현재 set들의 내용을 즉시 알려주게 된다. timeout을 NULL로 세팅하면 결코 끝나지 않고 계속 파일 기술자가 준비되는 것을 기다리게 되며 끝으로 특정한 set에 변화에 관심이 없다면 그 항목을 NULL로 지정하면 된다. 다음은 정규 입력에 무언가 나타날때까지 2.5초를 기다리는 코드이다.

       #include <sys/time.h>
       #include <sys/types.h>
       #include <unistd.h>
       #define STDIN 0  /* file descriptor for standard input */
       main()
       {
           struct timeval tv;
           fd_set readfds;
           tv.tv_sec = 2;
           tv.tv_usec = 500000;
           FD_ZERO(&readfds);
           FD_SET(STDIN, &readfds);
           /* don't care about writefds and exceptfds: */
           select(STDIN+1, &readfds, NULL, NULL, &tv);
           if (FD_ISSET(STDIN, &readfds))
               printf("A key was pressed!\n");
           else
               printf("Timed out.\n");
       }

만일 한줄씩 버퍼링하는 터미널이라면 엔터키를 치지 않는 이상은 그냥 타임아웃에 걸릴 것이다. 이제 아마도 이 훌륭한 방법을 데이터그램 소켓에서 데이터를 기다리는 데에 사용할수 있으리라고 생각할 것이다. 맞다. 그럴 수도 있다. 어떤 유닉스에서는 이 방법이 되지만 안되는 것도 있다. 하고자 하는 내용에 대해서는 아마도 맨페이지를 참조해야 할 것이다.

select()에 관한 마지막 얘기는 listen()이 된 소켓이 있다면 이 방법을 이용하여 소켓 기술자를 readfds에 첨가하는 방식으로 새로운 연결이 있었는가를 확인할 수도 있다는 것이다. 이것이 select()에 대한 짧은 검토였다.

-----------------------------------------------------------

참고사항

여기까지 와서는 아마 좀더 새로운 다른 것은 없는가 할것이다. 또 어디서 다른 무언가를 더 찾을수 있는가를 알고자 할 것이다. 초보자라면 다음의 맨페이지를 참고하는 것도  좋다.

   * socket()
   * bind()
   * connect()
   * listen()
   * accept()
   * send()
   * recv()
   * sendto()
   * recvfrom()
   * close()
   * shutdown()
   * getpeername()
   * getsockname()
   * gethostbyname()
   * gethostbyaddr()
   * getprotobyname()
   * fcntl()
   * select()
   * perror()

다음 책들도 도움이 될 것이다.

     Internetworking with TCP/IP, volumes I-III
     by Douglas E. Comer and David L. Stevens.
     Published by Prentice Hall.
     Second edition ISBNs: 0-13-468505-9, 0-13-472242-6,0-13-474222-2.

     There is a third edition of this set which covers IPv6 and IP over ATM.
     Using C on the UNIX System
     by David A. Curry.
     Published by O'Reilly & Associates, Inc.
     ISBN 0-937175-23-4.

     TCP/IP Network Administration
     by Craig Hunt.
     Published by O'Reilly & Associates, Inc.
     ISBN 0-937175-82-X.

     TCP/IP Illustrated, volumes 1-3
     by W. Richard Stevens and Gary R. Wright.
     Published by Addison Wesley.
     ISBNs: 0-201-63346-9, 0-201-63354-X, 0-201-63495-3.

     Unix Network Programming
     by W. Richard Stevens.
     Published by Prentice Hall.
     ISBN 0-13-949876-1.

웹상에는 다음과 같은 것들이 있을 것이다.

     BSD Sockets: A Quick And Dirty Primer
     (http://www.cs.umn.edu/~bentlema/unix/--has other great Unix       system     programming info, too!)

     Client-Server Computing
     (
http://pandonia.canberra.edu.au/ClientServer/socket.html)

     Intro to TCP/IP (gopher)    
     (
gopher://gopher-chem.ucdavis.edu/11/Index/Internet_aw
      /Intro_the_Internet/intro.to.ip/)

     Internet Protocol Frequently Asked Questions (France)
     (http://web.cnam.fr/Network/TCP-IP/)
     The Unix Socket FAQ (
http://www.ibrado.com/sock-faq/)

끔찍하지만..RFC도 봐야 하겠다.

     RFC-768 -- The User Datagram Protocol (UDP)
     (ftp://nic.ddn.mil/rfc/rfc768.txt)
     RFC-791 -- The Internet Protocol (IP)
     (ftp://nic.ddn.mil/rfc/rfc791.txt)
     RFC-793 -- The Transmission Control Protocol (TCP)
     (ftp://nic.ddn.mil/rfc/rfc793.txt)
     RFC-854 -- The Telnet Protocol (ftp://nic.ddn.mil/rfc/rfc854.txt)
     RFC-951 -- The Bootstrap Protocol (BOOTP)
     (ftp://nic.ddn.mil/rfc/rfc951.txt)
     RFC-1350 -- The Trivial File Transfer Protocol (TFTP)
     (
ftp://nic.ddn.mil/rfc/rfc1350.txt)

Posted by 젤라피

트랙백 주소 :: http://jellapi.net/jpidev/trackback/5

댓글을 달아 주세요