Prev | Contents | Next

6 클라인트-서버 배경지식

클라이언트-서버에 대해 이야기할 차례입니다. 통신망에 있는 거의 모든 것들은 서버 프로세스에게 이야기하는 클라이언트 프로세스를 상대하거나 그 반대입니다. telnet을 예로 들어봅시다. 여러분이 텔넷(클라이언트)로 원격지 호스트의 23번 포트에 접속할 때 그 호스트의 프로그램(telnetd라고 불리는 서버)이 생명을 얻는다. 그것이 들어오는 텔넷 요청을 처리하고 여러분에게 로그인 프롬프트를 띄워주는 등의 일을 처리합니다.

클라이언트 - 서버 상호작용

위의 도표에 클라이언트와 서버의 정보 교환이 정리되어 있습니다.

클라이언트-서버 쌍은 SOCK_STREAM이나 SOCK_DGRAM 또는 다른 어떤 것이라도 말할 수 있음을 기억하세요.(둘이 같은 방식으로 말하기만 한다면) 클라이언트-서버 쌍의 좋은 예시는 telnet/telnetd, ftp/ftpd 또는 Firefox/Apache 입니다. 여러분이 ftp를 쓸 때마다 여러분의 요청을 받아들이는 원격지 프로그램인 ftpd가 있습니다.

흔히 한 대의 장치에는 오직 하나의 서버만이 있을 것이며 그 서버는 fork() 를 통해서 여러 클라이언트를 처리할 것입니다.(역자 주 : 한 대의 장치에서 여러 개의 서버를 실행하는 많은 방법이 있지만 이 문서의 초판은 90년대에 작성되었습니다. ) 기본적인 과정은 아래와 같습니다. 서버가 연결을 기다리고, accept()한 후, 요청을 처리할 자식 프로세스를 fork()합니다. 이것이 다음 절에서 우리의 예제 서버가 하는 일입니다.

6.1 단순한 스트림 서버

이 서버가 하는 일은 스트림 연결에 “Hello, world!”을 전송하는 것 뿐입니다. 이 서버를 시험하기 위해서 할 일은 하나의 창에서 이것을 실행한 후 다른 창에서 텔넷에 아래 명령어로 접속하는 일 뿐입니다.

$ telnet remotehostname 3490

remotehostname은 여러분이 실행하는 장치의 아이피입니다.

서버 코드23:

/*
** server.c -- a stream socket server demo
*/

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>

#define PORT "3490"  // 사용자들이 접속할 포트

#define BACKLOG 10   // 몇 개의 대기중인 연결이 유지될 것인가

void sigchld_handler(int s)
{
    // waitpid()이 errno를 덮어쓸 수 있으므로 저장했다가 되살린다.
    int saved_errno = errno;

    while(waitpid(-1, NULL, WNOHANG) > 0);

    errno = saved_errno;
}


// IPv4 또는 IPv6 sockaddr을 받아온다.
void *get_in_addr(struct sockaddr *sa)
{
    if (sa->sa_family == AF_INET) {
        return &(((struct sockaddr_in*)sa)->sin_addr);
    }

    return &(((struct sockaddr_in6*)sa)->sin6_addr);
}

int main(void)
{
    int sockfd, new_fd;  // sock_fd에서 대기하고 들어오는 연결은 new_fd에 저장
    struct addrinfo hints, *servinfo, *p;
    struct sockaddr_storage their_addr; // 접속자의 주소 정보
    socklen_t sin_size;
    struct sigaction sa;
    int yes=1;
    char s[INET6_ADDRSTRLEN];
    int rv;

    memset(&hints, 0, sizeof hints);
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_flags = AI_PASSIVE; // use my IP

    if ((rv = getaddrinfo(NULL, PORT, &hints, &servinfo)) != 0) {
        fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv));
        return 1;
    }

    // 모든 결과를 조회하고 쓸 수 있는 첫 번째 것을 사용
    for(p = servinfo; p != NULL; p = p->ai_next) {
        if ((sockfd = socket(p->ai_family, p->ai_socktype,
                p->ai_protocol)) == -1) {
            perror("server: socket");
            continue;
        }

        if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes,
                sizeof(int)) == -1) {
            perror("setsockopt");
            exit(1);
        }

        if (bind(sockfd, p->ai_addr, p->ai_addrlen) == -1) {
            close(sockfd);
            perror("server: bind");
            continue;
        }

        break;
    }

    freeaddrinfo(servinfo); // 이 구조체는 더 이상 필요없음

    if (p == NULL)  {
        fprintf(stderr, "server: failed to bind\n");
        exit(1);
    }

    if (listen(sockfd, BACKLOG) == -1) {
        perror("listen");
        exit(1);
    }

    sa.sa_handler = sigchld_handler; // 죽은 프로세스를 다 거둬들이자
    sigemptyset(&sa.sa_mask);
    sa.sa_flags =   ;
    if (sigaction(SIGCHLD, &sa, NULL) == -1) {
        perror("sigaction");
        exit(1);
    }

    printf("server: waiting for connections...\n");

    while(1) {  // 주 accept() 루프
        sin_size = sizeof their_addr;
        new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size);
        if (new_fd == -1) {
            perror("accept");
            continue;
        }

        inet_ntop(their_addr.ss_family,
            get_in_addr((struct sockaddr *)&their_addr),
            s, sizeof s);
        printf("server: got connection from %s\n", s);

        if (!fork()) { // 자식 프로세스입니다.
            close(sockfd); // 자식은 리스너가 필요없습니다.
            if (send(new_fd, "Hello, world!", 13, 0) == -1)
                perror("send");
            close(new_fd);
            exit(0);
        }
        close(new_fd);  // 부모는 이것이 필요없습니다.
    }

    return 0;
}

궁금한 독자들을 위해 덧붙이자면 구문의 명료함을 위해서 하나의 큰 main()함수 안에 모든 코드를 다 적었습니다. 원한다면 더 작은 함수들로 나누어도 좋습니다.

(아마도 이 sigaction()을 처음 볼 수도 있는데 괜찮다. 이 코드는 fork()된 자식 프로세스가 종료되면서 생기는 좀비 프로세스를 거둬들이는 데 사용됩니다. 좀비 프로세스를 많이 만들고 거둬들이지 않으면 시스템 관리자가 흥분할 것입니다.)

다음 절에 나오는 클라이언트를 사용해서 이 서버로부터 데이터를 얻을 수 있습니다.

6.2 단순한 스트림 클라이언트

이 녀석은 서버보다도 더 쉽습니다. 이 클라이언트가 하는 일은 여러분이 명령줄에 지정한 호스트의 3490번 포트로 접속하는 것입니다. 이것은 서버가 보낸 문자열을 받습니다.

클라이언트 소스24:

/*
** client.c -- 스트림 소켓 클라이언트의 예시
*/

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>

#include <arpa/inet.h>

#define PORT "3490" // 클라이언트가 접속할 포트

#define MAXDATASIZE 100 // 한 번에 받을 수 있는 최대 바이트 갯수

// IPv4 또는 IPv6 sockaddr를 받아온다
void *get_in_addr(struct sockaddr *sa)
{
    if (sa->sa_family == AF_INET) {
        return &(((struct sockaddr_in*)sa)->sin_addr);
    }

    return &(((struct sockaddr_in6*)sa)->sin6_addr);
}

int main(int argc, char *argv[])
{
    int sockfd, numbytes;
    char buf[MAXDATASIZE];
    struct addrinfo hints, *servinfo, *p;
    int rv;
    char s[INET6_ADDRSTRLEN];

    if (argc != 2) {
        fprintf(stderr,"usage: client hostname\n");
        exit(1);
    }

    memset(&hints, 0, sizeof hints);
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;

    if ((rv = getaddrinfo(argv[1], PORT, &hints, &servinfo)) != 0) {
        fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv));
        return 1;
    }

    // 모든 결과를 순회하면서 쓸 수 있는 가장 첫 번째 것을 사용한다
    for(p = servinfo; p != NULL; p = p->ai_next) {
        if ((sockfd = socket(p->ai_family, p->ai_socktype,
                p->ai_protocol)) == -1) {
            perror("client: socket");
            continue;
        }

        if (connect(sockfd, p->ai_addr, p->ai_addrlen) == -1) {
            close(sockfd);
            perror("client: connect");
            continue;
        }

        break;
    }

    if (p == NULL) {
        fprintf(stderr, "client: failed to connect\n");
        return 2;
    }

    inet_ntop(p->ai_family, get_in_addr((struct sockaddr *)p->ai_addr),
            s, sizeof s);
    printf("client: connecting to %s\n", s);

    freeaddrinfo(servinfo); // 이 구조체는 더 이상 필요 없다

    if ((numbytes = recv(sockfd, buf, MAXDATASIZE-1, 0)) == -1) {
        perror("recv");
        exit(1);
    }

    buf[numbytes] = '\0';

    printf("client: received '%s'\n",buf);

    close(sockfd);

    return 0;
}

클라이언트를 실행하기 전에 서버를 실행하지 않으면 connect()는 “Connection refused”를 반환한다는 점을 기억하세요. 아주 유용합니다.

6.3 데이터그램 소켓

위에서 sendto()recvfrom()에 대해 논의할 때 UDP 데이터그램 소켓의 기본에 대해서 이미 알아보았습니다. 그러므로 바로 2개의 예제 프로그램을 제시하겠습니다. talker.clistener.c입니다.

listener는 장치에서 포트 4950으로 들어오는 패킷을 대기합니다. talker는 지정한 장치의 해당 포트로 사용자가 명령줄에 입력한 내용을 담은 패킷을 보냅니다.

데이터그램 소켓은 연결이 없고 소켓을 이더넷에 발송한 후 성공 여부는 신경쓰지 않기 때문에 클라이언트와 서버에 IPv6을 사용하도록 명시할 것입니다. 이렇게 하면 서버가 IPv6에서 듣고 클라이언트가 IPv4에서 발송해서 데이터를 받을 수 없는 상황을 피할 수 있을 것입니다. (우리의 TCP 스트림 소켓 세상에서도 이런 불일치가 발생할 수 있지만 connect()에서 하나의 주소 체계에 대해 에러를 발생시키고 다른 주소체계를 쓰도록 해줍니다.)

여기에 listener.c의 소스코드가 있습니다.25:

/*
** listener.c -- 데이터그램 소켓 "서버"의 예시
*/

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

#define MYPORT "4950"    // 연결할 포트

#define MAXBUFLEN 100

// get sockaddr, IPv4 or IPv6:
void *get_in_addr(struct sockaddr *sa)
{
    if (sa->sa_family == AF_INET) {
        return &(((struct sockaddr_in*)sa)->sin_addr);
    }

    return &(((struct sockaddr_in6*)sa)->sin6_addr);
}

int main(void)
{
    int sockfd;
    struct addrinfo hints, *servinfo, *p;
    int rv;
    int numbytes;
    struct sockaddr_storage their_addr;
    char buf[MAXBUFLEN];
    socklen_t addr_len;
    char s[INET6_ADDRSTRLEN];

    memset(&hints, 0, sizeof hints);
    hints.ai_family = AF_INET6; // IPv4를 쓰려면 AF_INET으로 설정합니다
    hints.ai_socktype = SOCK_DGRAM;
    hints.ai_flags = AI_PASSIVE; // 내 주소를 씁니다

    if ((rv = getaddrinfo(NULL, MYPORT, &hints, &servinfo)) != 0) {
        fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv));
        return 1;
    }

    // 모든 결과를 순회하면서 가능한 첫 번째 것에 바인드합니다
    for(p = servinfo; p != NULL; p = p->ai_next) {
        if ((sockfd = socket(p->ai_family, p->ai_socktype,
                p->ai_protocol)) == -1) {
            perror("listener: socket");
            continue;
        }

        if (bind(sockfd, p->ai_addr, p->ai_addrlen) == -1) {
            close(sockfd);
            perror("listener: bind");
            continue;
        }

        break;
    }

    if (p == NULL) {
        fprintf(stderr, "listener: failed to bind socket\n");
        return 2;
    }

    freeaddrinfo(servinfo);

    printf("listener: waiting to recvfrom...\n");

    addr_len = sizeof their_addr;
    if ((numbytes = recvfrom(sockfd, buf, MAXBUFLEN-1 , 0,
        (struct sockaddr *)&their_addr, &addr_len)) == -1) {
        perror("recvfrom");
        exit(1);
    }

    printf("listener: got packet from %s\n",
        inet_ntop(their_addr.ss_family,
            get_in_addr((struct sockaddr *)&their_addr),
            s, sizeof s));
    printf("listener: packet is %d bytes long\n", numbytes);
    buf[numbytes] = '\0';
    printf("listener: packet contains \"%s\"\n", buf);

    close(sockfd);

    return 0;
}

getaddrinfo()에서 우리가 마침내 SOCK_DGRAM을 사용한다는 것에 주목하세요. 또한, listen()accept()이 필요하지 않다는 점도 기억하세요. 이것이 연결없는 데이터그램 소켓을 사용할 때의 장점 중 하나입니다.

다음으로 talker.c의 소스코드26입니다.

/*
** talker.c -- 데이터그램 "클라이언트"의 예시
*/

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

#define SERVERPORT "4950"    // 사용자들이 접속할 포트

int main(int argc, char *argv[])
{
    int sockfd;
    struct addrinfo hints, *servinfo, *p;
    int rv;
    int numbytes;

    if (argc != 3) {
        fprintf(stderr,"usage: talker hostname message\n");
        exit(1);
    }

    memset(&hints, 0, sizeof hints);
    hints.ai_family = AF_INET6; // IPv4을 쓰려면 AF_INET으로 설정합니다
    hints.ai_socktype = SOCK_DGRAM;

    if ((rv = getaddrinfo(argv[1], SERVERPORT, &hints, &servinfo)) != 0) {
        fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv));
        return 1;
    }

    // 모든 결과를 순회하면서 소켓을 만듭니다
    for(p = servinfo; p != NULL; p = p->ai_next) {
        if ((sockfd = socket(p->ai_family, p->ai_socktype,
                p->ai_protocol)) == -1) {
            perror("talker: socket");
            continue;
        }

        break;
    }

    if (p == NULL) {
        fprintf(stderr, "talker: failed to create socket\n");
        return 2;
    }

    if ((numbytes = sendto(sockfd, argv[2], strlen(argv[2]), 0,
             p->ai_addr, p->ai_addrlen)) == -1) {
        perror("talker: sendto");
        exit(1);
    }

    freeaddrinfo(servinfo);

    printf("talker: sent %d bytes to %s\n", numbytes, argv[1]);
    close(sockfd);

    return 0;
}

이것이 전부입니다! 하나의 장치에서 listener를 실행하고 talker를 다른 장치에서 실행하세요.(역자 주 : 하나의 장치에서도 순서만 맞게 실행하면 문제는 없습니다. 여러 터미널을 동시에 열 수 있는 다양한 방법이 있습니다.) 그것들이 통신하는 것을 지켜보세요. 온 가족을 위한 즐거운 쑈가 될 것이라 확신합니다.

이번에는 서버를 실행할 필요도 없습니다! talker를 혼자 실행시키면 패킷을 행복하게 발송하고, 아무도 반대쪽에서 recvfrom()을 호출하지 않는다면 그저 패킷은 사라질 뿐입니다. UDP 데이터그램 소켓으로 보낸 데이터는 도착을 보장하지 않는다는 점을 기억하세요!

전에 몇 번 말한 사소한 것 한 가지를 빼면 전부입니다: 연결된 데이터그램 소켓이 그것입니다. 그것에 대해서 여기에서 말해야하는데, 이 문서의 데이터그램에 대한 부분이 바로 여기이기 때문입니다. 위의 talkerlistener의 주소를 지정하고 connect()를 호출한다고 합시다. 그 순간부터 talkerconnect()로 지정한 주소로만 데이터를 보내고 받을 수 있습니다. 이런 이유로 sendto()recvfrom()을 쓸 필요가 없습니다. 단순히 send()recv()를 쓰면 됩니다.


Prev | Contents | Next