KoreanFoodie's Study

[C++ 게임 서버] 3-5. UDP 서버 실습 본문

Game Dev/Game Server

[C++ 게임 서버] 3-5. UDP 서버 실습

GoldGiver 2023. 9. 22. 14:18

Rookiss 님의 '[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버' 를 들으며 배운 내용을 정리하고 있습니다. 관심이 있으신 분은 꼭 한 번 들어보시기를 추천합니다!

[C++ 게임 서버] 3-5. UDP 서버 실습

핵심 :

1. UDP 는 TCP 와 다르게, 패킷 간의 경계가 있고 속도가 빠르다. 다만 패킷의 순서가 보장되지 않으며, 데이터가 유실될 위험이 존재한다.

2. UDP 실습의 경우, TCP 실습 코드에서 ::connect 와 ::listen 만 없으면 로직은 거의 동일하다. 

3. UDP 는 Connected UDP(일종의 즐겨찾기 기능)을 활용하면, sendto 대신 send 를, recvfrom 대신 recv 를 사용하여 API 를 더 간단하게 사용 가능하다.

저번에는 TCP 방식으로 데이터를 전송하는 실습을 했으니, 이번에는 UDP 방식으로 데이터를 전송해 보자. 사실 그리 다르지는 않다. 조금 간소화되었다고 하면 될까?

3-2 에 보면 기본적으로 소켓을 만들고 클라이언트-서버 간 통신을 구축하는 기본 작업을 하였는데, UDP 는 기존 작업에서 ::connect 쪽을 굳이 안해줘도 된다(클라이언트 쪽에서)!

WSAData wsaData;
if (::WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
    return 0;

SOCKET clientSocket = ::socket(AF_INET, SOCK_DGRAM, 0);
if (clientSocket == INVALID_SOCKET)
{
    HandleError("Socket");
    return 0;
}

SOCKADDR_IN serverAddr;
::memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
::inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);
serverAddr.sin_port = ::htons(7777);

이제 while 문에 내용을 채워주자.

 

while (true)
{
    char sendBuffer[100] = "Hello World!";

    // 나의 IP 주소 + 포트 번호 설정

    // Unconnected UDP
    int32 resultCode = ::sendto(clientSocket, sendBuffer, sizeof(sendBuffer), 0,
        (SOCKADDR*)&serverAddr, sizeof(serverAddr));

    if (resultCode == SOCKET_ERROR)
    {
        HandleError("SendTo");
        return 0;
    }

    cout << "Send Data! Len = " << sizeof(sendBuffer) << endl;

    SOCKADDR_IN recvAddr;
    ::memset(&recvAddr, 0, sizeof(recvAddr));
    int32 addrLen = sizeof(recvAddr);

    char recvBuffer[1000];

    // Unconnected UDP
    int32 recvLen = ::recvfrom(clientSocket, recvBuffer, sizeof(recvBuffer), 0,
    	(SOCKADDR*)&recvAddr, &addrLen);

    if (recvLen <= 0)
    {
        HandleError("RecvFrom");
        return 0;
    }

    cout << "Recv Data! Data = " << recvBuffer << endl;
    cout << "Recv Data! Len = " << recvLen << endl;

    this_thread::sleep_for(1s);
}

사실 뭐... 크게 달라진 것은 없다. 중요한 함수는 sendto 와 recvfrom 정도이다.

::sendto 를 보면, 클라이언트 소켓과 서버 주소만 가지고 정보를 보내고, ::recvfrom 을 보면 클라이언트 소켓과 수령 주소만 가지고 정보를 받는다.

즉, 그냥 막(?) 주고 받는다고 보면 된다 😅

 

그렇다면 서버는 어떨까?

// 영업 시작!
if (::listen(listenSocket, 10) == SOCKET_ERROR)
{
    int32 errCode = ::WSAGetLastError();
    cout << "Listen ErrorCode : " << errCode << endl;
    return 0;
}

클라에서는 ::connect 가 빠졌다면, 서버는 ::listen 부분이 빠지게 된다. 그외의 것들은.. 사실 클라와 비슷하다.

 

while (true)
{
    SOCKADDR_IN clientAddr;
    ::memset(&clientAddr, 0, sizeof(clientAddr));
    int32 addrLen = sizeof(clientAddr);

    this_thread::sleep_for(1s);

    char recvBuffer[1000];

    int32 recvLen = ::recvfrom(serverSocket, recvBuffer, sizeof(recvBuffer), 0,
        (SOCKADDR*)&clientAddr, &addrLen);

    if (recvLen <= 0)
    {
        HandleError("RecvFrom");
        return 0;
    }

    cout << "Recv Data! Data = " << recvBuffer << endl;
    cout << "Recv Data! Len = " << recvLen << endl;

    int32 errorCode = ::sendto(serverSocket, recvBuffer, recvLen, 0,
        (SOCKADDR*)&clientAddr, sizeof(clientAddr));

    if (errorCode == SOCKET_ERROR)
    {
        HandleError("SendTo");
        return 0;
    }

    cout << "Send Data! Len = " << recvLen << endl;
}

서버에서도 ::recvfrom 과 ::sendto 를 썼다는 것에 유의하자.

 

아, 참고로 UDP 의 경우, 보내고자하는 서버 주소를 미리 즐겨찾기처럼 미리 지정해 놓고, sendto/recvfrom 을 할 때 주소를 미리 적지 않아도 되는 방법이 있다.

이를 Connected UDP 라고 하는데... 다음 구문을 추가하면 된다 :

// Connected UDP
::connect(clientSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr));

 

그리고 나서, ::sendto 와 ::recvfrom 을 각각 아래 ::send 와 ::recv 로 대체해주기만하면 끝이다! 😉

// Connected UDP
int32 resultCode = ::send(clientSocket, sendBuffer, sizeof(sendBuffer), 0);

int32 recvLen = ::recv(clientSocket, recvBuffer, sizeof(recvBuffer), 0);

 

Comments