Linux UDP 소켓 프로그래밍: 신속하고 믿을 수 있는 데이터 전송

소켓 프로그래밍을 할 때, 우리는 주로 TCP/IP를 기반으로한 프로그래밍을 다뤘습니다. 이번에는 TCP와는 다른 프로토콜인 UDP(User Datagram Protocol)에 대해 살펴보겠습니다. Linux UDP 소켓 프로그래밍 예제를 첨부합니다.

UDP란

UDP는 TCP/IP 4계층에서 Transport Layer에 위치합니다. 즉, UDP와 TCP는 데이터를 전송하기 위한 동등한 프로토콜입니다. 그러나 TCP는 연결 지향적이며 데이터 흐름을 제공하는 반면, UDP는 비연결 지향적이며 데이터의 무결성을 보장하지 않는 특징이 있습니다.

비연결 지향성

TCP는 통신을 시작하기 전에 상대방을 확인하고 세션을 맺는 작업을 수행하며, 연결된 세션을 통해 데이터 흐름이 이루어집니다. 그러나 UDP는 이런 세션을 만들지 않고 데이터를 그냥 보내고 받기만 합니다. 따라서 UDP를 사용하는 서버로 메시지를 보내더라도 메시지가 실제로 도착했는지 확인할 수 없습니다. 데이터는 보내지거나 보내지지 않을 수 있습니다.

무결성 부재

또한, TCP와 다르게 UDP는 데이터의 무결성을 보장하지 않습니다. TCP는 각 패킷에 순서를 부여하고 순서가 뒤섞이지 않도록 재조립하며, 일정 시간 내에 패킷이 도착하지 않으면 해당 패킷을 다시 요청할 수 있는 다양한 기능을 제공합니다. 그러나 UDP는 이러한 기능을 제공하지 않습니다. UDP로 전송된 패킷은 순서가 뒤바뀔 수 있고, 중간에 패킷이 손실될 수 있습니다. 프로토콜 수준에서 패킷의 무결성을 확인할 방법이 없습니다.

UDP 패킷에 무결성을 부여하려면 어플리케이션 수준에서 직접 코딩을 해주어야 합니다. 일반적으로 데이터 헤더에 일련번호 등을 추가하여 서버로 전송하고 서버에서 응답을 보내는 방식을 사용하여 UDP 패킷에 무결성을 제공합니다.

이러한 이유로 UDP는 단순한 데이터그램 중심의 통신을 수행하기 때문에 데이터그램 지향 프로토콜이라고 불립니다. 사실 UDP는 User Datagram Protocol의 약자입니다.

프로그래머 관점에서의 특징

UDP는 TCP와 비교하여 다양한 기능을 가지고 있지 않습니다. 따라서 더 간단하며 처리 속도가 빠릅니다. 또한 프로그래밍이 더 간단합니다. 나중에 예제를 통해 설명하겠지만, UDP를 사용하는 서버는 연결을 기다릴 필요 없이 소켓을 생성하고 데이터 수신을 기다리면 됩니다. (비연결 지향적이므로 클라이언트의 연결을 기다릴 필요가 없습니다.)

UDP의 사용 사례

UDP는 다음과 같은 상황에서 유용합니다.

1. 실시간 스트리밍 서비스: 음성 및 비디오 스트리밍 서비스에 적합합니다. TCP로 음성 서비스를 제공하면 패킷이 중간에 누락될 경우 음성 서비스가 중단될 수 있습니다. 그러나 UDP를 사용하면 패킷의 일부 손실 정도는 감수할 수 있으므로 연속적인 서비스가 중단되지 않습니다.

2. 다수의 패킷 교환: TCP보다 많은 패킷이 오가는 상황에서 유용합니다. 대표적인 예로 ‘StarCraft’의 Battle.net 서비스가 있습니다. 많은 유저가 접속하고 게임 중 수많은 패킷을 교환하는데, 이 중 상당수의 데이터는 중요하지 않습니다. 게임의 흐름이 중단되어서는 안되므로 UDP로 처리되는게 더 유리할것이다.

UDP 프로그래밍

UDP 서버 작성

UDP 서버도 소켓을 사용하여 통신하지만, TCP와는 다르게 연결 지향적이지 않아 listen 및 accept 과정이 필요하지 않습니다. UDP 서버는 기본적으로 처음에 만든 소켓 번호만으로 통신할 수 있습니다. TCP 프로그래밍에서는 클라이언트와 연결을 맺기 위해 처음에 생성된 하나의 소켓 번호를 엔드포인트로 사용하고, 연결이 설정되면 전용 통신 라인을 위한 소켓 번호를 생성하여 통신합니다. UDP는 이러한 작업이 필요하지 않기 때문에 서버를 간단하고 직관적으로 만들 수 있습니다.

하지만 한 가지 의문이 생깁니다. 연결을 맺지 않으면 어떻게 여러 개의 클라이언트로부터 요청을 받았을 때 해당 클라이언트에 결과 데이터를 보낼 수 있을까요? 가장 간단한 방법은 데이터를 수신할 때 데이터를 보낸 클라이언트의 정보를 가져와 해당 정보를 사용하여 데이터를 보내는 것입니다. Unix에서는 이러한 기능을 제공합니다.

int recvfrom(int s, void *buf, size_t len, int flags, struct sockaddr *from, socklen_t *fromlen);
int sendto(int s, const void *msg, size_t len, int flags, const struct sockaddr *to, socklen_t tolen);

recvfromsendto를 사용하여 원하는 클라이언트로 데이터를 보낼 수 있습니다. recvfrom은 TCP와 UDP 모두에서 사용할 수 있으며, UDP에서 사용할 때는 sockaddr 구조체가 채워져서 반환됩니다. 따라서 클라이언트의 연결 정보를 알 수 있게 됩니다. INET 서버의 경우 struct sockaddr_in을 사용할 것입니다. sockaddr_in의 멤버 변수를 확인하여 포트와 주소 정보를 얻을 수 있습니다.

UDP를 사용하는 클라이언트 작성

UDP 클라이언트는 매우 간단합니다. 소켓을 열고 데이터를 sendto 함수를 사용하여 보내기만 하면 됩니다.

예제 프로그램

이제 간단한 예제를 작성해 보겠습니다. 두 숫자를 더하는 덧셈 서버 및 클라이언트 예제입니다. 클라이언트는 서버에 두 개의 숫자를 보내고, 서버는 이를 받아 더한 후 결과를 클라이언트로 다시 보냅니다. 이 예제는 UDP 프로그래밍을 이해하기 위한 최소한의 내용을 담고 있습니다.

1. 서버 예제

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>

struct data
{
    int a;
    int b;
    int sum;
};

int main(int argc, char **argv)
{
    int sockfd;
    int clilen;
    int state;
    int n;
    struct data add_data;    

    struct sockaddr_in serveraddr, clientaddr;

    clilen = sizeof(clientaddr);
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        perror("socket 오류 : ");
        exit(0);
    }

    bzero(&serveraddr, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serveraddr.sin_port = htons(1234);

    state = bind(sockfd, (struct sockaddr *)&serveraddr, 
        sizeof(serveraddr);
    if (state == -1)
    {
        perror("bind 오류 : ");
        exit(0);
    }

    while(1)
    {
        n = recvfrom(sockfd, (void *)&add_data, sizeof(add_data), 0, (struct sockaddr *)&clientaddr, &clilen);
        add_data.sum = add_data.a + add_data.b;
        sendto(sockfd, (void *)&add_data, sizeof(add_data), 0, (struct sockaddr *)&clientaddr, clilen);
    }
    close(sockfd);
}

2. 클라이언트 예제

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>

struct data
{
    int a;
    int b;
    int sum;
};
int main(int argc, char **argv)
{
    int sockfd;
    int clilen;
    int state;
    struct sockaddr_in serveraddr;
    struct data add_data;

    clilen = sizeof(serveraddr);
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        perror("socket 오류 : ");
        exit(0);
    }

    bzero(&serveraddr, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    serveraddr.sin_port = htons(1234);

    add_data.a = atoi(argv[1]);
    add_data.b = atoi(argv[2]);

    sendto(sockfd, (void *)&add_data, sizeof(add_data), 0, (struct sockaddr *)&serveraddr, clilen);
    recvfrom(sockfd, (void *)&add_data, sizeof(add_data), 0, NULL, NULL); 

    printf("--> %d + %d = %d", add_data.a, add_data.b, add_data.sum);
    close(sockfd);
}

문제점

UDP를 사용한 서버 및 클라이언트 모델은 몇 가지 문제점을 가지고 있습니다. 위의 예제를 테스트하면 알 수 있지만, 서버 프로그램이 실행되지 않아도 클라이언트는 이를 감지하지 못하고 메시지를 보냅니다. 또한 메시지가 정확히 전달되었는지 여부를 클라이언트는 감지하지 못합니다. 데이터를 보내는 것으로 끝나기 때문입니다. 이러한 문제를 해결하려면 응용 프로그램 수준에서 처리해야 합니다. 처음에 서버에 메시지를 보내고일정 시간 내에 서버로부터 메시지가 도착하는지 확인한 다음 통신을 시작해야 합니다. 통신 중에도 일정 시간 내에 응답 메시지가 서버로부터 도착하는지 확인하여야 합니다.

결론

UDP 소켓 프로그래밍은 빠른 데이터 전송과 다중 클라이언트 통신을 위한 강력한 도구입니다. 이를 통해 실시간 응용 프로그램을 개발하거나 데이터를 빠르게 전송하는 데 도움이 됩니다. UDP 소켓 프로그래밍을 통해 네트워크 프로그래밍에 대한 이해를 높이고 다양한 응용 프로그램을 개발할 수 있을 것입니다.

Leave a Comment