KoreanFoodie's Study

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

Game Dev/Game Server

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

GoldGiver 2023. 9. 21. 13:29

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

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

핵심 :

1. TCP 에서는 송수신 버퍼가 존재하며, 통신을 할 때 버퍼에 데이터를 담고 꺼내는 방식으로 통신이 이루어진다.

2. 클라에서 송신을 하려고 하는데 송신 버퍼가 가득 차 있거나, 서버에서 수신을 하려고 하는데 수신 버퍼가 비어 있으면 Blocking 상태에 돌입한다.

3. 신기하게도, 버퍼 내 각 데이터 별로는 명확한 경계가 없어 데이터는 합쳐지거나 쪼개져서 인식될 수 있다.

저번에 소켓 프로그래밍 기초를 다루면서, 소켓을 만들고, 주소를 할당하고, 소켓과 주소를 할당한 후, 실제로 통신을 해보는 실습을 진행했다.

이번 시간에는 TCP 실습을 해 볼 예정이다. 코드와 PPT 를 보면서, 실습을 진행해 보자.

일단 저번 시간에 소켓 바인딩 작업 등등은 완료를 했으니, while 문 안에서 클라이언트와 서버가 각각 어떤 역할을 수행할지만 조금 바꿔보겠다.

일단, 클라이언트 쪽 코드부터 보자.

char sendBuffer[100] = "Hello World!";

int32 resultCode = ::send(clientSocket, sendBuffer, sizeof(sendBuffer), 0);
if (resultCode == SOCKET_ERROR)
{
    int32 errCode = ::WSAGetLastError();
    cout << "Send ErrorCode : " << errCode << endl;
    return 0;
}

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

char recvBuffer[100];

int32 recvLen = ::recv(clientSocket, recvBuffer, sizeof(recvBuffer), 0);
if (recvLen <= 0)
{
    int32 errCode = ::WSAGetLastError();
    cout << "Recv ErrorCode : " << errCode << endl;
    return 0;
}

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

this_thread::sleep_for(1s);

일단, 100바이트 짜리 버퍼를 만들고, 내용은 "Hello World!" 로 채운다. 그리고 1초 간격으로 ::send 를 하고, 보낸 후, 서버에서 다시 응답을 보내면 이를 recvBuffer 로 받아서 내용을 출력한다.

서버도 이와 비슷하게 만들어 주면 된다. 다만 Recv 를 먼저 한다는 점에 차이가 있겠다. 서버 코드는 다음과 같을 것이다 :

char recvBuffer[100];

this_thread::sleep_for(1s);

int32 recvLen = ::recv(clientSocket, recvBuffer, sizeof(recvBuffer), 0);
if (recvLen <= 0)
{
    int32 errCode = ::WSAGetLastError();
    cout << "Recv ErrorCode : " << errCode << endl;
    return 0;
}

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

int32 resultCode = ::send(clientSocket, recvBuffer, recvLen, 0);
if (resultCode == SOCKET_ERROR)
{
    int32 errCode = ::WSAGetLastError();
    cout << "Send ErrorCode : " << errCode << endl;
    return 0;
}

 

그럼 결과는 다음과 같이 나오게 된다 :

 

 

그럼 이제 조금 다른 실험을 해 보자. 만약 클라이언트는 데이터를 계속 보내는데, 만약 서버가 받고 있지 않다면 어떻게 될까?

즉, 서버 코드에서 while 문 안을 전부 주석처리하면 어떻게 될까? 🤔

신기하게도, 클라이언트는 데이터를 한 번 보내고, 더 이상 보내지 못한다. 그럼 Send Data!... 라는 성공 로그는 왜 뜨는 것일까? 그것을 이해하려면, 소켓 입출력 버퍼에 대해 이해해야 한다.

 

우리가 Client 에서 Server 로 Hello 를 보낸다고 해 보자. 개념적으로는 아래와 같을 것이지만...

 

실제로는 바로 보내지 않고, 아래에 빨간색으로 표시된 송수신 버퍼에 담아서, 전달을 하게 된다.

클라이언트는 SendBuffer 에 Hello 를 넣게 되는데, 사실 클라이언트는 여기서 이제 할 일을 다 했다고 보면 된다. 즉, Hello 가 실제로 서버로 갔는지 아닌지 여부는 그다지 중요한 게 아닌 것이다.

 

클라이언트 쪽 SendBuffer 에 Hello 가 담긴 후, 이 데이터는 서버의 RecvBuffer 에 담기고, 그 후 실제로 Server에게 전달된다. 아래 그림을 보면 이해가 쉬울 것이다.

 

그런데 만약 아래와 같이, 클라이언트의 SendBuffer 가 가득차 있다고 하면 어떻게 될까?

이런 경우에는, 클라이언트가 Hello 를 보내지 못하고 Blocking 된 상태가 유지될 것이다. 그렇다고 해서 SpinLock 처럼 CPU 자원을 낭비하는 것은 아니고, Sleep 상태에 들어갔다가 버퍼가 비워졌다는 신호를 받으면 다시 전송을 재개할 것이다. 😁

 

그렇다면 반대로, 데이터가 오지 않았는데 서버가 Receive 요청을 하면 어떻게 될까?

위 상황에서도, 서버는 마찬가지로 Blocking 상태에 돌입하게 될 것이다. 데이터가 없으니까. 🤣

따라서, TCP 통신에서는 서버와 클라이언트가 어느 정도 합의를 하여 동작을 해야 한다는 것을 알 수 있다. 🤩

 

 

그럼 이제 조금 다른 실습을 해 보자.

우리는 이전에 100 바이트 공간에 Hello 를 담아 보냈는데, 만약 클라 쪽에서 이것을 10개 동시에(for-loop 이니, 엄밀하게 동시는 아니고, 한꺼번에) 보내보겠다.

for (int32 i = 0; i < 10; i++)
{
    int32 resultCode = ::send(clientSocket, sendBuffer, sizeof(sendBuffer), 0);
    if (resultCode == SOCKET_ERROR)
    {
        int32 errCode = ::WSAGetLastError();
        cout << "Send ErrorCode : " << errCode << endl;
        return 0;
    }
}

 

그리고 서버 쪽에서 Recv 버퍼의 크기를 1000 으로 잡아주면... 다음과 같은 결과가 나온다.

오우, 이제는 버퍼를 1000 씩 받고 있다. 물론 타이밍이 조금 어긋나서 900/100 으로 쪼개지기도 하지만..

어쨋든, 핵심은 버퍼와 버퍼 사이에 '명확한 경계' 가 없다는 것이다. 즉, 아래 그림과 같이...

클라이언트는 Hello 를 나눠서 보내지만, 서버 쪽에서는 받을 때 그것을 뭉텅이로 인식할 수도 있다는 것이다!

 

반대로, 클라에서 100 을 보내려고 하는데 서버 수신 버퍼에서 50만큼의 공간밖에 없다면, 100 중 50만 잘려서 전송이 될 수도 있다는 것을 알 수 있다.

위 그림처럼 말이다 😂

Comments