iBetter Books
수정

UDP 서버와 클라이언트

이론은 충분히 들었습니다. 이제 UDP 에코 서버와 클라이언트를 직접 만들어 봅니다. TCP 에코와 비교하면 UDP가 얼마나 단출한지 한눈에 보입니다. 늘 그렇듯 Python으로 먼저 만들고 C로 다시 옮깁니다.

Python UDP 에코 서버

# 새 파일: udp_server.pyimport socketserver = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)server.bind(("127.0.0.1", 9500))print("UDP 에코 서버 대기 중")for _ in range(3):    data, addr = server.recvfrom(1024)    print(f"{addr} 가 보냄: {data.decode()}")    server.sendto(data, addr)server.close()

TCP 서버와 견주어 보면 사라진 것들이 눈에 띕니다. listen이 없고 accept가 없습니다. 연결이라는 개념 자체가 없으니 당연합니다.

소켓을 만들 때 SOCK_DGRAM을 쓴 것이 첫 번째 차이입니다. UDP 소켓을 만들겠다는 뜻입니다. bind로 주소와 포트에 묶는 것은 TCP와 같습니다. 서버는 어느 포트에서 엽서를 기다릴지 정해 두어야 하니까요.

핵심은 recvfrom입니다. 이 함수는 데이터그램을 받으면서 동시에 그것을 보낸 사람의 주소 addr도 함께 돌려줍니다. TCP에서는 연결이 이미 맺어져 있어 상대가 누구인지 알았지만, UDP에는 연결이 없으니 매번 누가 보냈는지를 받아야 합니다. 그래야 sendto(data, addr)로 바로 그 사람에게 답장을 보낼 수 있습니다. 받은 주소로 그대로 되돌려보내는 것이 에코입니다.

Python UDP 클라이언트

# 새 파일: udp_client.pyimport socketclient = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)server_addr = ("127.0.0.1", 9500)for message in ["하나", "둘", "셋"]:    client.sendto(message.encode(), server_addr)    data, _ = client.recvfrom(1024)    print(f"메아리: {data.decode()}")client.close()

클라이언트는 더욱 단출합니다. connect가 없으니 곧바로 sendto로 서버 주소를 적어 데이터를 던집니다. 그리고 recvfrom으로 답장을 받습니다. 답장을 보낸 주소는 서버임이 뻔하니 밑줄로 받아 버렸습니다.

여기서 한 가지 짚을 점이 있습니다. 이 클라이언트는 서버가 답장을 줄 것이라 믿고 recvfrom에서 기다립니다. 하지만 앞 장에서 배웠듯 UDP는 도착을 보장하지 않습니다. 만약 서버의 답장이 사라지면 이 클라이언트는 영영 기다리게 됩니다. 지금은 같은 컴퓨터의 루프백이라 거의 손실이 없어 잘 동작하지만, 실제 네트워크라면 타임아웃을 걸어야 합니다. 이 문제를 어떻게 다루는지는 다음 장에서 정면으로 마주합니다.

실행해 보기

서버를 띄우고 클라이언트를 실행합니다.

# 파일: 터미널 2 (클라이언트)python3 udp_client.py
메아리: 하나
메아리: 둘
메아리: 셋

서버 쪽에는 보낸 사람의 주소와 함께 기록이 남습니다.

UDP 에코 서버 대기 중
('127.0.0.1', 51993) 가 보냄: 하나
('127.0.0.1', 51993) 가 보냄: 둘
('127.0.0.1', 51993) 가 보냄: 셋

같은 일을 C로

C로 옮겨 봅니다. 서버입니다.

// 새 파일: udp_server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

int main(void) {
    int fd = socket(AF_INET, SOCK_DGRAM, 0);
    int yes = 1;
    setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    addr.sin_port = htons(9501);
    bind(fd, (struct sockaddr *)&addr, sizeof(addr));
    printf("UDP 에코 서버(C) 대기 중\n");

    char buf[1024];
    struct sockaddr_in client;
    socklen_t client_len = sizeof(client);
    for (int i = 0; i < 3; i++) {
        ssize_t n = recvfrom(fd, buf, sizeof(buf) - 1, 0,
                             (struct sockaddr *)&client, &client_len);
        if (n > 0) {
            buf[n] = '\0';
            printf("받음: %s\n", buf);
            sendto(fd, buf, n, 0, (struct sockaddr *)&client, client_len);
        }
    }
    close(fd);
    return 0;
}

소켓을 SOCK_DGRAM으로 만들고, bind까지는 TCP 서버와 같습니다. 다만 listenaccept가 사라졌습니다. 그리고 recvfrom에 클라이언트의 주소를 담을 구조체와 그 크기의 포인터를 넘기는 점을 보세요. Python에서 recvfrom이 주소를 함께 돌려주던 것을, C에서는 우리가 미리 마련한 구조체에 채워 받습니다. 받은 그 주소를 그대로 sendto에 넘겨 답장을 보냅니다.

클라이언트입니다.

// 새 파일: udp_client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

int main(void) {
    int fd = socket(AF_INET, SOCK_DGRAM, 0);

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    addr.sin_port = htons(9501);

    const char *messages[] = {"하나", "둘", "셋"};
    char buf[1024];
    for (int i = 0; i < 3; i++) {
        sendto(fd, messages[i], strlen(messages[i]), 0,
               (struct sockaddr *)&addr, sizeof(addr));
        ssize_t n = recvfrom(fd, buf, sizeof(buf) - 1, 0, NULL, NULL);
        if (n > 0) {
            buf[n] = '\0';
            printf("메아리: %s\n", buf);
        }
    }
    close(fd);
    return 0;
}

클라이언트는 connect 없이 곧바로 sendto로 서버 주소를 적어 보냅니다. 답장을 받을 때는 보낸 사람 주소가 필요 없으므로 recvfrom의 주소 자리에 NULL을 넘겼습니다. 관심 없는 정보는 받지 않겠다는 뜻입니다.

컴파일하고 실행하면 Python 때와 똑같은 메아리가 돌아옵니다.

# 파일: 터미널cc -Wall -o udp_server udp_server.ccc -Wall -o udp_client udp_client.c

UDP의 단출함을 확인했습니다. 그런데 이 단출함의 이면에는 앞서 본 불안함이 있습니다. 데이터가 사라지면 어떻게 될까요. 다음 장에서 UDP가 버린 신뢰성을 우리 손으로 직접 만들어 보며, TCP가 왜 그렇게 복잡해졌는지를 깨닫습니다.