나만의 작은 도서관

[C++] 게임서버 탐구 일지 #3. Winsock UDP echo 서버 본문

C++/Windows 게임서버

[C++] 게임서버 탐구 일지 #3. Winsock UDP echo 서버

pledge24 2024. 12. 27. 23:28

목차

  • UDP echo 서버 만들기
  • echo 서버에 데이터 전송해보기(Unconnected UDP)
  • echo 서버에 데이터 전송해보기(Connected UDP)
  • 깃헙코드

UDP echo 서버 만들기

 

이번에는 TCP가 아닌, UDP를 이용한 echo 서버를 만들어 보도록 하겠다.

 

Winsock 초기화, 소켓 생성

Winsock을 사용하려면 항상 초기화를 해줘야하기 때문에 Wincosk 초기화를 하고, 통신을 위한 소켓을 생성한다. 이부분은 TCP때에서 SOCK_STREAM -> SOCK_DGRAM 변경을 제외한 다른 부분은 동일하다.

// Winsock 초기화
WSAData wsaData;                                    
if (::WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)    
    return 0;

// 소켓 생성 SOCK_STREAM이 아닌, SOCK_DGRAM으로 설정
SOCKET serverSocket = ::socket(AF_INET, SOCK_DGRAM, 0);
if (serverSocket == SOCKET_ERROR)
{
    HandleError("Socket");
    return 0;
}

 

서버 주소 생성, bind()

bind() 또한 TCP랑 동일하게 해준다. bind()까지 성공적으로 했다면 TCP랑 다르게 listen() 없이 바로 통신이 가능한 상태가 된다. 

// 서버 주소 생성
SOCKADDR_IN serverAddr;
::memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = ::htonl(INADDR_ANY);
serverAddr.sin_port = ::htons(7777);

// 소켓과 서버 주소 엮기(바인딩)
if (::bind(serverSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
{
    HandleError("Bind");
    return 0;
}

 

왜 listen()을 사용하지 않을까?

TCP에서는 listen 전용 소켓을 만들었다. 예를 들어, 10명의 유저와 통신하고 싶다면 서버 측은 1개의 listen 소켓과 10개의 소켓이 들고 있어야 했다.

 

하지만 UDP는 오로지 하나의 소켓만 들고있는데, 그 이유는 UDP가 TCP처럼 연결을 유지하지 않아 각 클라이언트에 대한 연결 소켓이 없기 때문이다. 즉, 연결 요청을 listen해서 accept하는 과정 없이 서로 데이터를 주고 받기 때문에 listen()을 사용하지 않는다.

 

 

데이터 수신하기: recvfrom()

recvfrom() 함수를 이용해 클라이언트가 전송한 데이터를 받는다. recvfrom() 함수를 사용하기 위해서는 기존 recv()의 4가지 인자 + 2가지 인자가 필요하며 각각 다음과 같다.

recvfrom(
    _In_ SOCKET s,
    _Out_writes_bytes_to_(len, return) __out_data_source(NETWORK) char FAR * buf,
    _In_ int len,
    _In_ int flags,
    _Out_writes_bytes_to_opt_(*fromlen, *fromlen) struct sockaddr FAR * from,
    _Inout_opt_ int FAR * fromlen
    );
  • s: 수신 받을 소켓을 지정. 클라이언트 소켓을 넣어주면 된다.
  • buf: 수신 받은 데이터를 저장할 버퍼. 
  • len: 버퍼의 크기
  • flags: 플래그. 거의 사용할 일 없으니 0으로 설정해둔다.
  • from: 데이터를 전송한 소켓의 sockaddr 구조체
  • fromlen: from의 길이

아래 예제에서는 TCP때와 동일하게 1000 크기의 char 배열을 수신 버퍼로 사용하여 recv()의 인자로 넣어주었다.

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

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 << '\n';
cout << "Recv Data! Len = " << recvLen << '\n';

 

데이터 전송하기: sendto()

sendto() 함수를 이용해 클라이언트가 전송한 데이터를 재전송한다. recvfrom() 함수때와 동일하게 기존 send()의 4가지 인자 + 2가지 인자가 필요하며 각각 다음과 같다.

sendto(
    _In_ SOCKET s,
    _In_reads_bytes_(len) const char FAR * buf,
    _In_ int len,
    _In_ int flags,
    _In_reads_bytes_(tolen) const struct sockaddr FAR * to,
    _In_ int tolen
    );
  • s: 송신할 소켓을 지정. 클라이언트 소켓을 넣어주면 된다.
  • buf: 송신할 데이터를 저장할 버퍼. 
  • len: 버퍼의 크기
  • flags: 플래그. 거의 사용할 일 없으니 0으로 설정해둔다.
  • to: 데이터를 보낼 소켓의 sockaddr 구조체
  • tolen: to의 길이

아래 예제에서는 TCP때와 동일하게 수신 버퍼를 sendto()의 인자로 넣어주고, 위에서 만들어준 클라이언트 주소를 인자로 넣어줬다.

// 데이터 송신
int32 resultCode = ::sendto(serverSocket, recvBuffer, recvLen, 0,
    (SOCKADDR*)&clientAddr, sizeof(clientAddr));

// 원래는 소켓이 에러났다고 이렇게 프로그램을 강제로 끄는 일은 있으면 안된다.
if (resultCode == SOCKET_ERROR)
{
    HandleError("sendto");
    return 0;
}

cout << "Send Data! Data = " << recvBuffer << '\n';
cout << "Send Data! Len = " << recvLen << '\n';

echo 서버에 데이터 전송해보기(Unconnected UDP)

클라이언트에서의 UDP는 2가지 방식이 있다. 우선 기본 형태인 Unconnected UDP 방식부터 알아본다.

 

echo 서버에 데이터를 전송하기 위해선 더미 클라이언트에 서버에 넣은 sendto, recvfrom 코드를 뒤바꿔서 넣어주면 된다. TCP 때랑 크게 다른 점이 없으며, 코드는 다음과 같다. 

// 통신 중...
while (true)
{
    // 데이터 송신
    char sendBuffer[100] = "Hello World!";

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

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

    cout << "Send Data! Data = " << sendBuffer << '\n';
    cout << "Send Data! Len = " << sizeof(sendBuffer) << '\n';

    // 데이터 수신
    char recvBuffer[1000];

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

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

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

    cout << "Recv Data! Data = " << recvBuffer << '\n';
    cout << "Recv Data! Len = " << recvLen << '\n';

    this_thread::sleep_for(1s);
}

 

클라이언트의 포트 설정?

클라이언트 소켓과 주소를 bind하는 코드가 없는데 사실 sendto할 때 나의 IP주소 + 포트 번호로 주소를 자동으로 설정한다.(포트 번호는 사용할 수 있는 아무거나 쓴다)

이와 같은 이유로 TCP는 sendto가 아닌 connect 함수 호출 시 자동으로 설정된다.

 

실행 결과

클라이언트, 서버가 정상적으로 Hello World를 주고 받는 것을 확인할 수 있다. 

 

echo 서버에 데이터 전송해보기(Connected UDP)

Connected UDP는 기존 UDP(Unconnected UDP)에서 매번 서버의 주소를 지정해서 전송하는 것이 불편함을 해소하기 위해 만들어진 방식이다. 기존 UDP와 다르게 Connected UDP는 처음 통신할 때 즐겨찾기처럼 해당 서버의 주소를 소켓에 저장한 다음, 다음 통신부터 자동으로 해당 서버로 통신하는 방법이다. 

 

기존 UDP와 동작 방식은 동일하며, 서버와 오랫동안 통신을 하고자 할 때 유용하다. 코드는 다음과 같다.

// Connected UDP 다른점 1. connect 추가
// connect를 썼을 뿐, TCP처럼 서버와 연결되거나 하진 않는다.
::connect(clientSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr));

while (true)
{
    // 데이터 송신
    char sendBuffer[100] = "Hello World!";

    // Connected UDP 다른점 2. sendto() -> send()
    int32 resultCode = ::send(clientSocket, sendBuffer, sizeof(sendBuffer), 0);

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

    cout << "Send Data! Data = " << sendBuffer << '\n';
    cout << "Send Data! Len = " << sizeof(sendBuffer) << '\n';

    // 데이터 수신
    char recvBuffer[1000];

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

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

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

    cout << "Recv Data! Data = " << recvBuffer << '\n';
    cout << "Recv Data! Len = " << recvLen << '\n';

    this_thread::sleep_for(1s);
}

 

실행 결과

클라이언트, 서버가 정상적으로 Hello World를 주고 받는 것을 확인할 수 있다. 


깃헙 코드

Unconnceted UDP

https://github.com/pledge24/WinGameServerPractice/tree/55a0f71a2cf5160ac5d24a1134957275516270f4

 

GitHub - pledge24/WinGameServerPractice

Contribute to pledge24/WinGameServerPractice development by creating an account on GitHub.

github.com

 

Connceted UDP

https://github.com/pledge24/WinGameServerPractice/tree/6ea4fcb399b22fba2f24c02972d20d8f337f4c44

 

GitHub - pledge24/WinGameServerPractice

Contribute to pledge24/WinGameServerPractice development by creating an account on GitHub.

github.com