본문 바로가기

서버운영 (TA, ADMIN)/네트워크

[네트워크] 소켓 프로그래밍 활용

소켓 프로그래밍 기초에서는 TCP/IP 프로토콜이 무엇인지 살펴보고 이를 이용해 네트워크 프로그래밍을 하는 데 필요한 기본 개념을 다뤘습니다. TCP와 UDP의 차이점과 IP주소와 포트 번호의 역할도 알아보았습니다. 또한 TCP/IP 프로토콜 기반 프로그램 작성에 필수적인 소켓 인터페이스 관련 함수와 구조체를 살펴보고 간단한 프로그램도 작성해봤습니다.


네트워크를 통해 데이터를 주고받으며 동작하는 네트워크 프로그램은 일반적으로 서버와 클라이언트로 역할을 구분할 수 있습니다. 서버는 클라이언트의 요청에 따라 다양한 서비스를 제공하는 프로그램으로, 보통 데몬(daemon) 프로세스라고 합니다. 예를 들어, 브라우저를 통해 웹사이트에 접속할 경우 서버에는 웹 서비스를 제공하는 HTTP 데몬이 동작하고 있어 사용자들이 웹페이지를 볼 수 있는 것입니다.


데몬 프로세스는 크게 두 가지 형태로 나눌 수 있습니다. 첫번째는 반복 서버로, 데몬 프로세스가 직접 클라이언트의 요청을 처리하는 형태입니다. 두번째는 데몬 프로세스가 직접 서비스를 제공하지 않고, 서비스와 관련 있는 다른 프로세스를 fork 함수로 생성한 후 이 프로세스를 클라이언트와 연결해 서비스를 제공하는 동시 동작 서버입니다. 네트워크 프로그램을 처음 도입한 당시에는 반복 서버 형태로 서비스를 제공했습니다. 그런데 다양한 네트워크 서비스가 등장하면서 데몬 프로세스의 개수가 너무 많아지게 되었습니다. 이런 문제를 해결할 목적으로 대표 데몬프로세스가 동작하면서 클라이언트의 요청에 적합한 서비스 프로그램을 동작시키는 동시 동작 서버를 도입했습니다. 대표적인 예가 inetd 데몬인데, 클라이언트의 요청에 따라 자식 프로세스를 생성하고 해당 요청을 서비스할 프로세스를 실행합니다. 자주 사용하는 텔넷이나 FTP가 이와 같은 형태로 서비스하고 있습니다.


이 장에서는 TCP/IP 프로토콜을 이용해 프로그램을 작성할 때 도움이 될 수 있도록 소켓 인터페이스를 이용한 다양한 예제를 다룹니다. 또한 TCP 기반 네트워크 프로그램과 UDP 기반 네트워크 프로그램으로 나누어 살펴보고, 반복 서버와 동시 동작 서버의 예제를 살펴볼 것입니다.



01. TCP 기반 프로그래밍

네트워크를 기반으로 하는 통신 프로그램은 일반적으로 서버와 클라이언트로 역할을 구분할 수 있습니다. 그중 서버 컴퓨터에서 동작하며 서비스를 제공하는 프로그램은 데몬프로세스라고 합니다. 데몬 프로세스는 크게 두 가지 형태로 나눌 수 있습니다.


  • 반복 서버: 데몬 프로세스가 직접 모든 클라이언트의 요청을 차례로 처리
  • 동시 동작 서버: 데몬 프로세스가 직접 서비스를 제공하지 않고, 서비스와 관련 있는 다른 프로세스를 fork 함수로 생성해 클라이언트와 연결


이 절에서는 두 가지 형태의 예제를 TCP 기반으로 살펴봅니다. 특히 같은 시스템 안에서 동작하는 유닉스 도메인 소켓이 아닌 인터넷을 통해 다른 시스템과 연결하는 인터넷 소켓(AF_INET)을 사용한 예제를 살펴봅니다.



반복 서버


반복 서버는 서버 프로그램이 클라이언트의 요청을 직접 처리합니다. 따라서 반복 서버 프로세스는 한 번에 한 클라이언트의 요청만 처리할 수 있고, 여러 클라이언트가 서비스를 요청할 경우 순차적으로 처리하므로 클라이언트가 자기 순서를 기다려야 합니다. 이를 방지하기 위해 반복 서버 프로세스를 여러 개 동작시킬 수 있습니다.


반복 서버는 인터넷 소켓과 TCP(SOCK_STREAM)를 이용해 통신합니다. 서버는 계속 동작하면서 클라이언트가 요청하는 서비스를 받아 처리해줍니다.


아래 예제는 반복 서버 프로세스와 이에 접속하는 클라이언트 프로그램입니다. 올바른 결과를 얻으려면 서버와 클라이언트가 통신해야 합니다. 따라서 서버 프로세스를 먼저 실행해 클라이언트의 요청을 기다리게 해놓고, 클라이언트 프로세스를 실행해야 합니다.

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

// 포트번호를 정의한다.
#define PORTNUM 9000

int main(void) {
    char buf[256];
    struct sockaddr_in sin, cli;
    int sd, ns, clientlen = sizeof(cli);

    // 서버 주소 구조체에 소켓의 종류를 AF_INMET으로
    // 지정하고 포트번호와 서버의 IP 주소를 설정한다.
    memset((char *)&sin, '\0', sizeof(sin));
    sin.sin_family  = AF_INET;
    sin.sin_port    = htons(PORTNUM);
    sin.sin_addr.s_addr = inet_addr("127.0.0.1");

    // 소켓 생성시 SOCK_STREAM으로 지정해 TCP를 사용한다.
    if((sd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        perror("socket");
        exit(1);
    }

    // 생성한 소켓을 bind 함수를 사용해 위에서 설정한 IP주소/포트 번호와 연결한다.
    if(bind(sd, (struct sockaddr *)&sin, sizeof(sin))) {
        perror("bind");
        exit(1);
    }

    // listen 함수를 호출해 클라이언트의 요청을 받을 준비를 마쳤음을
    // 운영체제에 알린다. 한번에 5개의 클라이언트가 접속할 수 있도록 설정한다.
    if(listen(sd, 5)) {
        perror("listen");
        exit(1);
    }

    // while 문으로 무한 반복하게 한다(이 서버는 무한 반복하면서 서비스를 제공)
    while(1) {
        // accept 함수를 사용해 클라이언트의 요청이 올때까지 기다린다.
        // 클라이언트의 접속 요청이 오면 새로운 소켓 기술자 ns가 생성되고
        // 이를 통해 클라이언트와 통신한다.
        if((ns = accept(sd, (struct sockaddr *)&cli, &clientlen)) == -1) {
            perror("accept");
            exit(1);
        }

        // accept 함수를 통해 알아낸 클라이언트의 IP 주소를 문자열로
        // 변환해 버퍼에 저장한다.
        sprintf(buf, "%s", inet_ntoa(cli.sin_addr));
        // 어느 클라이언트가 서버로 접속했는지 출력한다.
        printf("*** Send a Message to Client(%s)\n", buf);

        // 서버에서 클라이언트로 보낼 간단한 환영 메시지를 작성한다.
        strcpy(buf, "Welcome to Network Server!!!");
        // send 함수를 사용해 클라이언트로 메시지를 보낸다.
        if(send(ns, buf, strlen(buf) + 1, 0) == -1) {
            perror("send");
            exit(1);
        }
        // 클라이언트가 보낸 메시지를 받아서 출력한다.
        if(recv(ns, buf, strlen(buf), 0) == -1) {
            perror("recv");
            exit(1);
        }
        // 작업이 끝나면 클라이언트와 접속할 때 사용한 소켓 기술자를 닫고,
        // 다시 accept 함수를 수행해 클라이언트의 접속을 기다린다.
        printf("** From Client : %s\n", buf);
        close(ns);
    }
    close(sd);

    return 0;
}


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

// 클라이언트도 서버와 같은 포트 번호를 사용해 소켓을 생성한다.
#define PORTNUM 9000

int main(void) {
    int sd;
    char buf[256];
    struct sockaddr_in sin;

    memset((char *)&sin, '\0', sizeof(sin));
    sin.sin_family = AF_INET;
    sin.sin_port = htons(PORTNUM);
    // 접속할 서버의 주소를 지정한다.
    sin.sin_addr.s_addr = inet_addr("127.0.0.1");

    // 소켓 서버를 생성하고 서버와 접속한다.
    if((sd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        perror("socket");
        exit(1);
    }

    if(connect(sd, (struct sockaddr *)&sin, sizeof(sin))) {
        perror("connect");
        exit(1);
    }

    // 서버에 접속한 후 서버에서 보낸 메시지를 받는다.
    if(recv(sd, buf, sizeof(buf), 0) == -1) {
        perror("recv");
        exit(1);
    }

    // 서버로 보낼 메시지를 복사한 후 서버로 메시지를 전송한다.
    printf("** From Server : %s\n", buf);

    strcpy(buf, "I wans a HTTP Service.");
    if(send(sd, buf, sizeof(buf) + 1, 0) == -1) {
        perror("send");
        exit(1);
    }

    close(sd);

    return 0;
}

[root@dev-web ch12]# ./ex12_1s.out
*** Send a Message to Client(127.0.0.1)
** From Client : I wans a HTTP Service.


[root@dev-web ch12]# ./ex12_1c.out
** From Server : Welcome to Network Server!!!



동시 동작 서버


앞선 예제는 서버 프로그램이 클라이언트의 요청을 직접 처리합니다. 그러나 클라이언트의 요청에 따라 수행해야 할 작업이 많은 경우에는 클라이언트가 대기하는 시간이 길어진다는 단점이 있습니다. 이를 해결하기 위해 동시 동작 서버 구조를 이용합니다.


서버 프로그램은 클라이언트의 서비스를 대신 처리할 프로세스를 fork 함수로 생성하고, 이 프로세스를 클라이언트와 연결해준 다음 다시 클라이언트의 접속을 기다립니다. 이렇게 하면 동시에 여러 클라이언트에 서비스를 제공할 수 있습니다.

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

#define PORTNUM 9000

int main(void) {
    char buf[256];
    struct sockaddr_in sin, cli;
    int sd, ns, clientlen = sizeof(cli);

    // socket 함수로 소켓을 생성하고
    if((sd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        perror("socket");
        exit(1);
    }

    memset((char *)&sin, '\0', sizeof(sin));
    sin.sin_family  = AF_INET;
    sin.sin_port    = htons(PORTNUM);
    sin.sin_addr.s_addr = inet_addr("127.0.0.1");

    // 특정 IP 주소와 연결한 후
    if(bind(sd, (struct sockaddr *)&sin, sizeof(sin))) {
        perror("bind");
        exit(1);
    }

    // listen 함수로 클라이언트의 접속을 5개까지 처리하도록 설정
    if(listen(sd, 5)) {
        perror("listen");
        exit(1);
    }

    while(1) {
        // accept 함수를 사용해 클라이언트의 접속 요청을 받는다.
        if((ns = accept(sd, (struct sockaddr *)&cli, &clientlen)) == -1) {
            perror("accept");
            exit(1);
        }
        // fork 함수를 수행해 자식 프로세스를 생성하고
        // 자식 프로세스가 클라이언트의 응답을 처리하게 한다.
        switch (fork()) {
            case 0:
                close(sd);
                strcpy(buf, "Welcome to Server");
                if(send(ns, buf, strlen(buf) + 1, 0) == -1) {
                    perror("send");
                    exit(1);
                }

                if(recv(ns, buf, strlen(buf), 0) == -1) {
                    perror("recv");
                    exit(1);
                }
                printf("** From Client: %s\n", buf);
                sleep(5);
                exit(0);
        }
        close(ns);
    }

    return 0;
}

[root@dev-web ch12]# ps -ef | grep ex12
root     14052 10214  0 11:20 pts/0    00:00:00 ./ex12_2s.out
root     14081 14052  0 11:21 pts/0    00:00:00 ./ex12_2s.out


클라이언트가 접속했을 때 서버의 실행상태를 보면 다음과 같이 서버 프로그램이 하나 더 동작하고 있음을 알 수 있습니다. 최초의 서버 프로세스의 ID는 14052이고, 클라이언트가 접속해 생성된 자식 프로세스의 ID는 14081임을 알 수 있습니다.



동시 동작 서버: 서비스 프로세스 호출 1


서버 프로그램은 fork 함수로 생성한 자식 프로세스가 클라이언트와 통신하게 합니다. 이때 클라이언트에서 수행하는 부분이 부모 프로세스와 같은 파일에 작성되어 있습니다. 따라서 실행 결과를 보면 ex12_2s.out 프로세스가 2개 동작하고 있음을 알 수 있습니다.


이번 예제에서는 서버 프로세스가 클라이언트의 요청을 받으면 fork 함수를 호출해 자식 프로세스를 생성한 후 exec 함수로 다른 프로세스를 실행합니다. 이는 inetd 같은 대표 데몬 프로세스가 텔넷이나 FTP 같은 서비스 요청을 받아 해당 서비스 프로세스를 호출하는 것과 유사합니다. 자식 프로세스가 han이라는 프로그램을 호출하는 예제를 작성해보겠습니다.

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

#define PORTNUM 9002

int main(void) {
    struct sockaddr_in sin, cli;
    int sd, ns, clientlen = sizeof(cli);

    // socket 함수로 소켓을 생성하고
    if((sd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        perror("socket");
        exit(1);
    }

    printf("** Create Socket\n");

    memset((char *)&sin, '\0', sizeof(sin));
    sin.sin_family = AF_INET;
    sin.sin_port = htons(PORTNUM);
    sin.sin_addr.s_addr = inet_addr("127.0.0.1");

    // 특정 IP 주소와 연결한 다음
    if(bind(sd, (struct sockaddr *)&sin, sizeof(sin))) {
        perror("bind");
        exit(1);
    }
    printf("** Bind Socket\n");

    // listen 함수로 클라이언트의 접속을 5개까지 처리한다고 설정한다.
    if(listen(sd, 5)) {
        perror("listen");
        exit(1);
    }
    printf("** Listen Socket\n");

    while(1) {
        // accept 함수를 사용해 클라이언트의 접속 요청을 받는다.
        if((ns = accept(sd, (struct sockaddr *)&cli, &clientlen)) == -1) {
            perror("accept");
            exit(1);
        }
        printf("** Accept Client\n");

        // fork 함수로 자식 프로세스를 생성해 클라이언트의 응답을 처리하게 한다.
        switch(fork()) {
            // 클라이언트와 통신할 수 있는 소켓 기술자(ns)를
            // 표준 출력으로 복사한뒤, execl 함수로 han 프로세스를 실행한다.
            // han은 표준 출력을 간단한 메시지를 출력하는 프로세스다.
            case 0:
                printf("** Fork Client\n");
                close(sd);
                dup2(ns, STDIN_FILENO);
                dup2(ns, STDOUT_FILENO);
                close(ns);
                execl("./han", "han", (char *)0);
        }
        close(ns);
    }

    return 0;
}


아래는 서버프로세스에서 실행하는 han 프로세스로 출력문을 실행하고 5초 동안 sleep하는 기능을 수행하는 간단한 프로그램입니다.

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

int main(void) {
    // han 프로세스에서 표준 출력으로 메시지를 출력하면
    // 기본 표준 출력인 모니터가 아닌 클라이언트 프로세스로 전달된다.
    // 이는 부모 프로세스인 서버 프로그램에서 exec 함수를 호출하기 전에
    // 소켓 기술자를 표준 출력으로 복제했기 때문이다.
    printf("Welcome to Server, from Han!");
    // sleep 함수는 프로세스 목록을 확인할 수 있게 시간을 지연시킨다.
    sleep(5);

    return 0;
}


클라이언트 프로그램은 다음과 같으며, 서버 측에서 보낸 메시지를 받아 출력합니다.

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

#define PORTNUM 9000

int main(void) {
    int sd, len;
    char buf[256];
    struct sockaddr_in sin;

    memset((char *)&sin, '\0', sizeof(sin));
    sin.sin_family = AF_INET;
    sin.sin_port = htons(PORTNUM);
    sin.sin_addr.s_addr = inet_addr("127.0.0.1");

    // 서버와 연결하기 위해 서버의 주소를 지정하고 소켓을 생성한다.
    if((sd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        perror("socket");
        exit(1);
    }

    // connect 함수를 호출해 서버와 연결을 요청한다.
    printf("==> Create Socket\n");
    if(connect(sd, (struct sockaddr *)&sin, sizeof(sin))) {
        perror("connect");
        exit(1);
    }

    // 서버에서 메시지를 받아 출력한다.
    printf("==> Connect Server\n");
    if((len = recv(sd, buf, sizeof(buf), 0)) == -1) {
        perror("recv");
        exit(1);
    }
    buf[len] = '\0';

    printf("==> From Server : %s\n", buf);

    // 소켓을 닫는다.
    close(sd);

    return 0;
}

[root@dev-web ch12]# ./ex12_3c.out
==> Create Socket
==> Connect Server
==> From Server : Welcome to Server, from Han!



동시 동작 서버: 서비스 프로세스 호출 2


이전 예제에서는 fork 함수로 생성한 자식 프로세스에서 소켓 기술자를 표준 출력으로 복사해 서비스 프로세스가 클라이언트와 통신하게 했습니다. 이와 달리 소켓 기술자를 서비스 프로세스의 명령행 인자로 전달할 수도 있습니다.


이번에는 서버 프로그램에서 클라이언트와 연결하는 소켓 기술자를 exec 함수로 호출하는 프로세스에 명령행 인자로 전달하도록 지정합니다. 따라서 exec 함수로 실행한 서비스 프로세스는 명령행 인자로 전달받은 소켓 기술자로 클라이언트와 통신합니다.

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

#define PORTNUM 9000

int main(void) {
    char buf[256];
    struct sockaddr_in sin, cli;
    int sd, ns, clientlen = sizeof(cli);

    // socket 함수로 소켓을 생성한다.
    if((sd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        perror("socket");
        exit(1);
    }

    printf("** Create Socket\n");

    memset((char *)&sin, '\0', sizeof(sin));
    sin.sin_family = AF_INET;
    sin.sin_port = htons(PORTNUM);
    sin.sin_addr.s_addr = inet_addr("127.0.0.1");

    // 특정 IP 주소와 연결한다.
    if(bind(sd, (struct sockaddr *)&sin, sizeof(sin))) {
        perror("bind");
        exit(1);
    }
    printf("** Bind Socket\n");

    // listen 함수로 클라이언트의 접속을 5개까지 처리하도록 설정한다.
    if(listen(sd, 5)) {
        perror("listen");
        exit(1);
    }
    printf("** Listen Socket\n");

    // accept 함수를 사용해 클라이언트와 접속 요청을 받는다.
    while(1) {
       if((ns = accept(sd, (struct sockaddr *)&cli, &clientlen)) == -1) {
           perror("accept");
           exit(1);
       }
       printf("** Accept Client\n");

       // fork 함수로 자식 프로세스를 생성하고,
       // 해당 자식 프로세스가 클라이언트의 응답을 처리하게 한다.
       switch(fork()) {
           case 0:
               printf("** Fork Client\n");
               close(sd);
               // 클라이언트와 통신할 수 있는 소켓을 버퍼에 저장한다.
               sprintf(buf, "%d", ns);
               // execlp 함수를 사용해 bit 프로세스를 호출할 때
               // 위에서 저장한 소켓 정보를 인자로 함께 전송한다.
               // bit 프로세스가 클라이언트와 연결되어 서비스를 제공할 것이다.
               execlp("./bit", "bit", buf, (char *)0);
               close(ns);
       }
       close(ns);
    }

    return 0;
}


execlp 함수를 호출하면서 소켓 기술자를 인자로 지정합니다. execlp 함수를 호출했으므로 자식 프로세스는 bit 프로세스 이미지로 바뀌고, buf로 지정한 인자는 bit 프로세스의 명령행 인자로 전달됩니다. execlp 함수로 호출한 bit 프로세스는 명령행 인자로 받은 소켓 기술자로 클라이언트와 통신하며, 간단한 환영 메시지를 클라이언트로 전송하고, 응답 메시지를 받아 출력하는 간단한 프로그램입니다.

#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[]) {
    char buf[256];
    int len, ns;

    // 명령행 인자(문자열)로 전달받은 소켓 기술자를 정수형으로 변환한다.
    ns = atoi(argv[1]);

    // 간단한 환영 메시지를 클라이언트로 전송한다.
    strcpy(buf, "Welcome to Server, from Bit");
    if((send(ns, buf, strlen(buf) + 1, 0)) == -1) {
        perror("send");
        exit(1);
    }

    // 클라이언트에서 보낸 메시지를 받아 출력한다.
    if((len=recv(ns, buf, strlen(buf), 0)) == -1) {
        perror("recv");
        exit(1);
    }
    printf("@@ [Bit] From Client: %s\n", buf);
    close(ns);

    return 0;
}



클라이언트 프로그램은 다음과 같습니다.

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

#define PORTNUM 9000

int main(void) {
    int sd, len;
    char buf[256];
    struct sockaddr_in sin;

    memset((char *)&sin, '\0', sizeof(sin));
    sin.sin_family  = AF_INET;
    sin.sin_port    = htons(PORTNUM);
    sin.sin_addr.s_addr = inet_addr("127.0.0.1");

    // 소켓을 생성하고
    if((sd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        perror("socket");
        exit(1);
    }

    // 생성한 소캣을 통해 서버와 연결한다.
    printf("==> Create Socket\n");
    if(connect(sd, (struct sockaddr *)&sin, sizeof(sin))) {
        perror("connect");
        exit(1);
    }

    printf("==> Connect Server\n");
    // 서버에서 메시지를 전달 받는다.
    if((len = recv(sd, buf, sizeof(buf), 0)) == -1) {
        perror("recv");
        exit(1);
    }
    buf[len] = '\0';

    // 서버의 메시지를 출력하고
    printf("==> From Server : %s\n", buf);

    // 답장 메시지를 작성해 send 함수를 통해 서버로 전송한다.
    strcpy(buf, "I want a TELNET Service.");
    if(send(sd, buf, sizeof(buf) + 1, 0) == -1) {
        perror("send");
        exit(1);
    }

    close(sd);

    return 0;
}

[root@dev-web ch12]# ./ex12_4s.out
** Create Socket
** Bind Socket
** Listen Socket
** Accept Client
** Fork Client
@@ [Bit] From Client: I want a TELNET Service.


[root@dev-web ch12]# ./ex12_4c.out
==> Create Socket
==> Connect Server
==> From Server : Welcome to Server, from Bit



UDP 기반 프로그래밍


UDP 기반 프로그래밍은 TCP와 달리 클라이언트로부터 사전에 연결 요청을 받지 않습니다. 따라서 클라이언트는 connect 함수를 사용하지 않습니다. 클라이언트의 연결 요청이 별도로 없을 것이기 때문에 서버도 bind 함수를 수행한 다음에 listen 함수를 사용하지 않습니다. 클라이언트가 보낸 데이터를 수신한 후 서버의 동작은 TCP를 사용할 때와 같습니다. 단지 서버에서 클라이언트로 데이터를 보낼 때 클라이언트의 주소를 구조체로 지정해야 하는 점이 다를 뿐입니다.


이번 예제는 UDP 기반 클라이언트/서버로, 간단한 메시지 전송을 수행하는 프로그램입니다. 먼저 서버부터 살펴보겠습니다.

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

#define PORTNUM 9000

int main(void) {
    char buf[256];
    struct sockaddr_in sin, cli;
    int sd, clientlen = sizeof(cli);

    // 인터넷 소켓을 사용하는 소켓을 생성한다.
    // 소켓을 생성할 때 통신 방식은 SOCK_DGRAM으로 지정해
    // UDP를 사용하도록 한다.
    if((sd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
        perror("socket");
        exit(1);
    }

    // 소켓 주소 구조체에 서버의 주소를 지정한다.
    memset((char *)&sin, '\0', sizeof(sin));
    sin.sin_family  = AF_INET;
    sin.sin_port    = htons(PORTNUM);
    sin.sin_addr.s_addr = inet_addr("127.0.0.1");

    // 위에서 생성한 소켓을 bind 함수를 사용해
    // 다음 행에서 설정한 IP주소/포트 번호와 연결한다.
    if(bind(sd, (struct sockaddr *)&sin, sizeof(sin))) {
        perror("bind");
        exit(1);
    }

    while(1) {
        // recvfrom 함수를 호출해 클라이언트에서 오는 메시지를 받는다.
        // recvfrom 함수는 메시지를 보낸 클라이언트와 주소도 함께 알려주므로
        // 이 주소를 사용해 클라이언트로 메시지를 보낼 수 있다.
        if((recvfrom(sd, buf, 255, 0, (struct sockaddr *)&cli, &clientlen)) == -1) {
            perror("recvfrom");
            exit(1);
        }
        printf("** From Client : %s\n", buf);

        strcpy(buf, "Hello Client");
        // 클라이언트로 간단한 메시지로 보낸다.
        // sendto 함수에는 목적지의 주소를 인자로 지정해야 한다.
        if((sendto(sd, buf, strlen(buf) + 1, 0, (struct sockaddr *)&cli, sizeof(cli))) == -1) {
            perror("sendto");
            exit(1);
        }
    }

    return 0;
}


다음은 클라이언트 프로그램입니다.

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

#define PORTNUM 9000

int main(void) {
    int sd, n;
    char buf[256];
    struct sockaddr_in sin;

    // 소켓을 생성한다. TCP 기반 프로그램과 달리 connect 함수를 호출하지 않는다.
    if((sd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
        perror("socket");
        exit(1);
    }

    // 서버의 주소를 소켓 주소 구조체에 설정한다.
    memset((char *)&sin, '\0', sizeof(sin));
    sin.sin_family = AF_INET;
    sin.sin_port = htons(PORTNUM);
    sin.sin_addr.s_addr = inet_addr("127.0.0.1");

    // sendto  함수를 호출해 메시지를 서버로 전송한다.
    strcpy(buf, "I am a client");
    if(sendto(sd, buf, strlen(buf)+1, 0, (struct sockaddr *)&sin, sizeof(sin)) == -1) {
        perror("sendto");
        exit(1);
    }

    //서버에서 전송한 응답 메시지를 recvfrom 함수로 읽어 출력한다.
    n = recvfrom(sd, buf, 255, 0, NULL, NULL);
    buf[n] = '\0';
    printf("** From Server : %s\n", buf);

    return 0;
}


[root@dev-web ch12]# ./ex12_5s.out
** From Client : I am a client


[root@dev-web ch12]# ./ex12_5c.out
** From Server : Hello Client