본문 바로가기
개인공부/윤성우의 열혈 TCP&IP 소켓 프로그래밍

Chapter 06 - UDP 기반 서버 / 클라이언트

by 하고싶은건많은놈 2023. 2. 24.

06-1 : UDP에 대한 이해

UDP는 신뢰할 수 없는 전송방법을 제공하지만 구조가 간결하기 때문에 성능이 우월함

  • UDP는 TCP와 달리 흐름제어(Flow Control)가 존재하지 않음
  • 호스트로 수신된 패킷을 포트정보를 참조하여 최종 목적직인 UDP 소켓의 전달하는 것이 UDP의 가장 중요할 역할
  • 실시간 영상 및 음성과 같은 멀티미디어 데이터는 일부가 손실되어도 크게 문제가 되지 않고, 실시간 서비스이므로 속도가 중요하기 때문에 UDP의 사용을 고려해볼만함
  • 송수신 데이터량이 적으면서 연결이 잦은 경우에는 UDP가 TCP보다 훨씬 빠르고 효율적

06-2 : UDP 기반 서버 / 클라이언트의 구현

UDP에서는 TCP와 달리 서버와 클라이언트가 연결된 상태로 데이터를 송수신하지 않음
따라서 listen()accept() 함수의 호출이 불필요
또한 1대1 관계를 맺는 TCP 소켓과는 달리 하나의 UDP 소켓으로 둘 이상의 호스트와의 통신이 가능

TCP 소켓은 목적지 소켓과 연결된 상태이기 때문에 주소 정보가 따로 필요하지 않음
그러나 UDP 소켓은 서로 연결되어있지 않으므로 데이터 전송시 반드시 목적지의 주소정보가 추가되어야함


UDP에서 사용되는 입출력함수

#include <sys/socket.h>

sszie_t    sendto(int sock, void *buff, size_t nbytes, int flags, struct sockaddr *to, socklen_t addrlen);
// 성공시 전송된 바이트 수, 실패시 -1 반환

sszie_t    recvfrom(int sock, void *buff, size_t nbytes, int flags, struct sockaddr *from, socklen_t *addrlen);
// 성공시 수신한 바이트 수, 실패시 -1 반환

UDP 기반의 에코 서버

// uecho_server.cpp

#include <iostream>
#include <fstream>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

using namespace std;

#define BUF_SIZE 30
void    error_handling(char *message);

int main(int argc, char *argv[])
{
    int            serv_sock, str_len;
    char        message[BUF_SIZE];
    socklen_t    clnt_adr_sz;

    struct    sockaddr_in serv_adr, clnt_adr;
    if (argc != 2)
    {
        cout << "Usage : " << argv[0] << " <port>\n";
        exit(1);
    }

    serv_sock = socket(PF_INET, SOCK_DGRAM, 0);
    if (serv_sock == -1)
        error_handling("UDP socket creation error");

    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_adr.sin_port = htons(atoi(argv[1]));

    if (::bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
        error_handling("bind() error");

    while (1)
    {
        clnt_adr_sz = sizeof(clnt_adr);
        str_len = recvfrom(serv_sock, message, BUF_SIZE, 0,
                           (struct sockaddr*)&clnt_adr, &clnt_adr_sz);
        sendto(serv_sock, message, str_len, 0,
               (struct sockaddr*)&clnt_adr, clnt_adr_sz);
    }

    close(serv_sock);
    return 0;
}

void    error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

UDP 기반의 에코 클라이언트

// uecho_client.cpp

#include <iostream>
#include <fstream>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

using namespace std;

#define BUF_SIZE 30
void    error_handling(char *message);

int main(int argc, char *argv[])
{
    int            sock, str_len;
    char        message[BUF_SIZE];
    socklen_t    adr_sz;

    struct    sockaddr_in serv_adr, from_adr;
    if(argc != 3)
    {
        cout << "Usage : " << argv[0] << " <IP> <port>\n";
        exit(1);
    }

    sock = socket(PF_INET, SOCK_DGRAM, 0);
    if (sock == -1)
        error_handling("socket() error");

    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
    serv_adr.sin_port = htons(atoi(argv[2]));

    while (1)
    {
        cout << "Input message(Q to quit) : ";
        cin.getline(message, BUF_SIZE);

        if (!strcmp(message, "q") || !strcmp(message, "Q"))
            break;

        sendto(sock, message, strlen(message), 0,
               (struct sockaddr*)&serv_adr, sizeof(serv_adr));
        adr_sz = sizeof(from_adr);
        str_len = recvfrom(sock, message, BUF_SIZE, 0,
                           (struct sockaddr*)&from_adr, &adr_sz);
        message[str_len] = 0;
        cout << "Message from server : " << message << endl;
    }

    close (sock);
    return 0;
}

void    error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

UDP에서는 connect() 과정이 없기 때문에 IP와 포트가 bind() 함수를 통해 할당되거나, sendto() 함수의 첫 호출시에 자동으로 할당됨
주소정보를 한번 할당시 프로그램 종료시까지 정보가 그대로 유지됨

  • IP는 호스트의 IP
  • 포트는 사용하지 않는 포트 번호 하나를 임의로 골라서 할당

06-3 : UDP의 데이터 송수신 특성과 UDP에서의 connect 함수호출

UDP 기반 송수신 데이터에는 경계가 존재
따라서 송수신 과정에서 호출하는 입력함수와 출력함수의 호출횟수가 완벽히 일치해야 데이터를 온전히 수신할 수 있음


데이터의 경계가 존재하는 UDP 프로그램(서버) 예시

// bound_host1.cpp

#include <iostream>
#include <fstream>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

using namespace std;

#define BUF_SIZE 30
void    error_handling(char *message);

int main(int argc, char *argv[])
{
    int                    sock, str_len;
    char                message[BUF_SIZE];
    socklen_t            adr_sz;
    struct sockaddr_in    my_adr, your_adr;

    if (argc != 2)
    {
        cout << "Usage : " << argv[0] << " <port>\n";
        exit(1);
    }

    sock = socket(PF_INET, SOCK_DGRAM, 0);
    if (sock == -1)
        error_handling("socket() error");

    memset(&my_adr, 0, sizeof(my_adr));
    my_adr.sin_family = AF_INET;
    my_adr.sin_addr.s_addr = htonl(INADDR_ANY);
    my_adr.sin_port = htons(atoi(argv[1]));

    if (::bind(sock, (struct sockaddr*)&my_adr, sizeof(my_adr)) == -1)
        error_handling("bind() error");

    for (int i = 0; i < 3; i++)
    {
        sleep(5);
        adr_sz = sizeof(your_adr);
        str_len = recvfrom(sock, message, BUF_SIZE, 0,
                           (struct sockaddr*)&your_adr, &adr_sz);
        cout << "Message " << i + 1 << " : " << message << endl;
    }

    close(sock);
    return 0;
}

void    error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

데이터의 경계가 존재하는 UDP 프로그램(클라이언트) 예시

// bound_host2.cpp

#include <iostream>
#include <fstream>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

using namespace std;

#define BUF_SIZE 30
void    error_handling(char *message);

int main(int argc, char *argv[])
{
    int                    sock;
    char                msg1[] = "Hi!";
    char                msg2[] = "I'm another UDP host!";
    char                msg3[] = "Nice to meet you";
    //socklen_t            your_adr_sz;
    struct sockaddr_in    your_adr;

    if (argc != 3)
    {
        cout << "Usage : " << argv[0] << " <IP> <port>\n";
        exit(1);
    }

    sock = socket(PF_INET, SOCK_DGRAM, 0);
    if (sock == -1)
        error_handling("socket() error");

    memset(&your_adr, 0, sizeof(your_adr));
    your_adr.sin_family = AF_INET;
    your_adr.sin_addr.s_addr = inet_addr(argv[1]);
    your_adr.sin_port = htons(atoi(argv[2]));

    sendto(sock, msg1, sizeof(msg1), 0,
           (struct sockaddr*)&your_adr, sizeof(your_adr));
    sendto(sock, msg2, sizeof(msg2), 0,
           (struct sockaddr*)&your_adr, sizeof(your_adr));
    sendto(sock, msg3, sizeof(msg3), 0,
           (struct sockaddr*)&your_adr, sizeof(your_adr));
    cout << "finish\n";

    close(sock);
    return 0;
}

void    error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

UDP 소켓이 전송하는 패킷은 경계의 존재로인해 그 자체로 하나의 데이터로서 의미를 가지기 때문에 데이터그램(Datagram)이라고 표현하기도 함


sendto() 함수호출을 통한 데이터의 전송 과정

  1. UDP 소켓에 목적지의 ip와 포트번호 등록
  2. 데이터 전송
  3. UDP 소켓에 등록된 목적지 정보 삭제

목적지 정보가 등록되지 않은 소켓은 unconnected 소켓, 등록된 소켓은 connected 소켓
UDP 소켓은 기본적으로 unconnected 소켓이지만 하나의 호스트와 오래 송수신 해야할경우 비효율적
따라서 이 경우 해당 소켓을 대상으로 connect() 함수를 호출하여 connected 소켓을 생성해 사용할 수 있음

  • 단, TCP처럼 목적지 소켓과 연결설정 과정을 거치지는 않음
  • 주소정보가 저장되었기 때문에 write()read() 함수로도 입출력이 가능

connected UDP 소켓 클라이언트 예시

// uecho_con_client.cpp

#include <iostream>
#include <fstream>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

using namespace std;

#define BUF_SIZE 30
void    error_handling(char *message);

int main(int argc, char *argv[])
{
    int            sock, str_len;
    char        message[BUF_SIZE];
    socklen_t    adr_sz;

    struct    sockaddr_in serv_adr, from_adr;
    if (argc != 3)
    {
        cout << "Usage : " << argv[0] << " <IP> <port>\n";
        exit(1);
    }

    sock = socket(PF_INET, SOCK_DGRAM, 0);
    if (sock == -1)
        error_handling("socket() error");

    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
    serv_adr.sin_port = htons(atoi(argv[2]));
    connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr));

    while (1)
    {
        cout << "Input message(Q to quit) : ";
        cin.getline(message, BUF_SIZE);

        if (!strcmp(message, "q") || !strcmp(message, "Q"))
            break;

        /*
        sendto(sock, message, strlen(message), 0,
               (struct sockaddr*)&serv_adr, sizeof(serv_adr));
        */
        write(sock, message, strlen(message));

        /*
        adr_sz = sizeof(from_adr);
        str_len = recvfrom(sock, message, BUF_SIZE, 0,
                           (struct sockaddr*)&from_adr, &adr_sz);
        */
        str_len = read(sock, message, sizeof(message) - 1);
        message[str_len] = 0;
        cout << "Message from server : " << message << endl;
    }

    close (sock);
    return 0;
}

void    error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

06-4 : 윈도우 기반으로 구현하기

윈도우 기반 UDP 에코 서버 예제

// uecho_server_win.cpp

#include <iostream>
#include <winsock2.h>

using namespace std;

#define BUF_SIZE 30
void    ErrorHandling(char* message);

int main(int argc, char *argv[])
{
    WSADATA        wsaData;
    SOCKET        servSock;
    char        message[BUF_SIZE];
    int            strLen, clntAdrSz;
    SOCKADDR_IN    servAdr, clntAdr;

    if (argc != 2)
    {
        cout << "Usage : " << argv[0] << " <port>\n";
        exit(1);
    }

    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
        ErrorHandling("WSAStartup() error");

    servSock = socket(PF_INET, SOCK_DGRAM, 0);
    if (servSock == INVALID_SOCKET)
        ErrorHandling("socket() error");

    memset(&servAddr, 0, sizeof(servAdr));
    servAddr.sin_family = AF_INET;
    servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servAddr.sin_port = htons(atoi(argv[1]));

    if (::bind(servSock, (SOCKADDR*)&servAdr, sizeof(servAdr)) == SOCKET_ERROR)
        ErrorHandling("bind() error");

    while (1)
    {
        clntAdrSz = sizeof(clntAdr);
        strLen = recvfrom(servSock, message, BUF_SIZE, 0,
                          (SOCKADDR*)&clntAdr, &clntAdrSz);
        sendto(servSock, message, strLen, 0,
               (SOCKADDR*)&clntAdr, sizeof(clntAdr));
    }

    closesocket(servSock);
    WSACleanup();
    return 0;
}

void    ErrorHandling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

윈도우 기반 UDP 에코 클라이언트 예제

// uecho_client_win.cpp

#include <iostream>
#include <winsock2.h>

using namespace std;

#define BUF_SIZE 30
void    ErrorHandling(char *message);

int        main(int argc, char *argv[])
{
    WSADATA        wsaData;
    SOCKET        sock;
    char        message[BUF_SIZE];
    int            strLen;
    SOCKADDR_IN    servAdr;

    if(argc != 3)
    {
        cout << "Usage : " << argv[0] << " <IP> <port>\n";
        exit(1);
    }

    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
        ErrorHandling("WSAStartup() error");

    sock = socket(PF_INET, SOCK_DGRAM, 0);
    if (sock == INVALID_SOCKET)
        ErrorHandling("socket() error");

    memset(&servAdr, 0, sizeof(servAdr));
    servAddr.sin_family = AF_INET;
    servAddr.sin_addr.s_addr = inet_addr(argv[1]);
    servAddr.sin_port = htons(atoi(argv[2]));
    connect(sock, (SOCKADDR*)&servAdr, sizeof(servAdr));

    while (1)
    {
        cout << "Input message(Q to quit) : ";
        cin.getline(message, BUF_SIZE);

        if (!strcmp(message, "q") || !strcmp(message, "Q"))
            break;

        send(sock, message, strlen(message), 0);
        strLen = recv(sock, message, sizeof(message) - 1, 0);
        message[strLen] = 0;
        cout << "Message from server : " << message << endl;
    }

    closesocket(sock);
    WSACleanup();
    return 0;
}

void    ErrorHandling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

내용 확인문제

  1. TCP보다 UDP가 빠른 이유는 무엇인가? 그리고 TCP는 데이터의 전송을 신뢰할 수 있지만 UDP는 신뢰할 수 없는 이유는 또 무엇인가?

UDP는 TCP와 달리 흐름제어가 존재하지 않기 때문에 빠르면서도 동시에 신뢰성이 떨어짐


  1. 다음 중 UDP의 특성이 아닌 것을 모두 고르면?
  • UDP는 TCP와 달리 연결의 개념이 존재하지 않는다. 따라서 반드시 TCP에서 보인 것처럼 1대 1의 형태로 데이터를 송수신하지 않을 수 있다. (O)
  • UDP 기반으로 데이터를 전송할 목적지가 두 군데라면, 총 두 개의 소켓을 생성해야한다. (X)
  • UDP 소켓은 TCP 소켓이 할당한 동일한 번호의 포트에 재할당이 불가능하다. (X)
  • UDP 소켓과 TCP 소켓은 공존할 수 있다. 따라서 필요하다면 한 호스트상에서 TCP 방식과 UDP 방식의 데이터 송수신을 모두 진행할 수 있다. (O)
  • UDP 소켓을 대상으로도 connect 함수를 호출할 수 있는데, 이러한 경우 UDP 소켓도 TCP 소켓과 마찬가지로 Three-way handshaking 과정을 거치게된다. (X)

  1. UDP 데이터그램이 최종 목적지인 상대 호스트의 UDP 소켓에 전달되는데 있어서, IP가 담당하는 부분과 UDP가 담당하는 부분을 구분지어 설명해보자.

IP는 목적지 호스트까지 전달, UDP는 호스트의 소켓들 중 하나에게 최종적으로 전달


  1. UDP는 일반적으로 TCP보다 빠르다. 그러나 송수신하는 데이터의 성격에 따라서 그 차이는 미미할 수도 있고, 반대로 매우 클 수도 있다. 그렇다면 어떠한 상황에서 UDP는 TCP보다 매우 좋은 성능을 보이는지 설명해보자.

송수신 데이터량이 적으면서 연결이 잦은 경우


  1. 클라이언트의 TCP 소켓은 connect 함수를 호출할 때 자동으로 IP와 포트가 할당된다. 그렇다면 bind 함수를 호출하지 않는 UDP 소켓은 언제 IP와 포트가 할당되는가?

sendto() 함수의 첫 호출시 자동으로 할당


  1. connect 함수의 호출문장은 TCP 클라이언트의 경우 반드시 삽입해야하는 문장이다. 그러나 UDP의 경우는 선택적으로 삽입이 가능하다. 그렇다면 UDP에서 connect 함수를 호출하면 어떠한 이점이 있는가?

하나의 호스트와 오래 송수신할 경우 주소정보의 할당 및 초기화 과정을 생략하기 때문에 성능이 향상됨


  1. 본문에서 보인 예제 uecho_server.c와 uecho_client.c를 참고해서 서버와 클라이언트 상호간에 한번씩 메세지를 주고받는 형태로 대화를 진행하는 예제를 작성해보자. 단, 주고받는 대화는 콘솔상에 출력되어야한다.
// conversation_server.cpp

#include <iostream>
#include <fstream>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

using namespace std;

#define BUF_SIZE 256
void    error_handling(char *message);

int main(int argc, char *argv[])
{
    int            serv_sock, str_len;
    char        message[BUF_SIZE];
    socklen_t    clnt_adr_sz;

    struct    sockaddr_in serv_adr, clnt_adr;
    if (argc != 2)
    {
        cout << "Usage : " << argv[0] << " <port>\n";
        exit(1);
    }

    serv_sock = socket(PF_INET, SOCK_DGRAM, 0);
    if (serv_sock == -1)
        error_handling("UDP socket creation error");

    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_adr.sin_port = htons(atoi(argv[1]));

    if (::bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
        error_handling("bind() error");

    while (1)
    {
        clnt_adr_sz = sizeof(clnt_adr);
        str_len = recvfrom(serv_sock, message, BUF_SIZE, 0,
                           (struct sockaddr*)&clnt_adr, &clnt_adr_sz);
        message[str_len] = 0;
        cout << "Message from Client: " << message << endl;

        cout << "Message to Client(Q to quit) : ";
        cin.getline(message, BUF_SIZE);

        if (!strcmp(message, "q") || !strcmp(message, "Q"))
            break;

        sendto(serv_sock, message, strlen(message), 0,
               (struct sockaddr*)&clnt_adr, clnt_adr_sz);
    }

    close(serv_sock);
    return 0;
}

void    error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}
// conversation_client.cpp

#include <iostream>
#include <fstream>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

using namespace std;

#define BUF_SIZE 256
void    error_handling(char *message);

int main(int argc, char *argv[])
{
    int            sock, str_len;
    char        message[BUF_SIZE];
    socklen_t    adr_sz;

    struct    sockaddr_in serv_adr, from_adr;
    if (argc != 3)
    {
        cout << "Usage : " << argv[0] << " <IP> <port>\n";
        exit(1);
    }

    sock = socket(PF_INET, SOCK_DGRAM, 0);
    if (sock == -1)
        error_handling("socket() error");

    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
    serv_adr.sin_port = htons(atoi(argv[2]));

    while (1)
    {
        cout << "Message to Server(Q to quit) : ";
        cin.getline(message, BUF_SIZE);

        if (!strcmp(message, "q") || !strcmp(message, "Q"))
            break;

        sendto(sock, message, strlen(message), 0,
               (struct sockaddr*)&serv_adr, sizeof(serv_adr));

        adr_sz = sizeof(from_adr);
        str_len = recvfrom(sock, message, BUF_SIZE, 0,
                           (struct sockaddr*)&from_adr, &adr_sz);
        message[str_len] = 0;
        cout << "Message from Server : " << message << endl;
    }

    close (sock);
    return 0;
}

void    error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

댓글