나만의 작은 도서관

[C++] 게임서버 탐구 일지 #2. Winsock TCP 서버 기초- send(), recv() 본문

C++/Windows 게임서버

[C++] 게임서버 탐구 일지 #2. Winsock TCP 서버 기초- send(), recv()

pledge24 2024. 12. 27. 01:49

목차

  • echo 서버 만들기
  • echo 서버에 데이터 전송해보기
  • send(), recv()의 특징
  • 깃헙 코드

echo 서버 만들기

echo 서버란 클라이언트가 전송해주는 데이터를 그대로 되돌려주는 서버이다. echo 서버를 만들기 위해 클라이언트가 전송한 데이터를 받는 recv(), 받은 데이터를 재전송해주는 send()를 순서대로 사용한다.

 

+) 클라이언트, 서버 예제 코드들은 이전 글들에서 작성한 코드 위에서 작성되었다.

 

데이터 수신하기: recv()

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

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

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

char recvBuffer[1000];

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 << '\n';
cout << "Recv Data! Len = " << sizeof(recvBuffer) << '\n';

 

데이터 전송하기: send()

send() 함수를 이용해 클라이언트가 전송한 데이터를 재전송한다. send() 함수를 사용할 때 recv()와 동일하게 4가지 인자가 필요한데 각각 다음과 같다.

send(
    _In_ SOCKET s,
    _In_reads_bytes_(len) const char FAR * buf,
    _In_ int len,
    _In_ int flags
    );
  • s: 송신할 소켓을 지정. 클라이언트 소켓을 넣어주면 된다.
  • buf: 송신할 데이터를 저장한 버퍼. 지금은 echo서버이니 수신 버퍼를 넣어준다. 
  • len: 버퍼의 크기
  • flags: 플래그. 거의 사용할 일 없으니 0으로 설정해둔다.

 

아래 예제에서는 수신 버퍼를 send()의 인자로 넣어주었다.

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

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

echo 서버에 데이터 전송해보기

echo 서버에 데이터를 전송하기 위해선 더미 클라이언트에 서버에 넣은 send, recv코드를 뒤바꿔서 넣어주면 된다. 코드는 다음과 같다. 

 

아래 예제에서는 너무 많은 송수신을 막기 위해 스레드를 1초간 재워 1초에 한 번 씩만 송수신 하도록 했다.

// 통신 중...
while (true)
{
    // 데이터 송신(Hello World! 송신)
    char sendBuffer[1000] = "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! Data = " << sendBuffer << '\n';
    cout << "Send Data! Len = " << sizeof(sendBuffer) << '\n';

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

    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 << '\n';
    cout << "Recv Data! Len = " << sizeof(recvBuffer) << '\n';

    this_thread::sleep_for(1s);
}

 

실행 결과

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


send(), recv()의 특징

두 함수는 블로킹 함수이다.

send()와 recv()는 블로킹 함수이기 때문에 리턴을 할 때까지 호출한 메인 스레드는 멍을 때리게 된다.

 

recv()의 경우, 만약 서버가 수신 버퍼가 비어 있는 상태에서 recv()를 호출했다면, 메인 스레드는 recv()가 리턴 될 때까지 무한정 기다리게 된다.

// 정확히 이 줄에서 멈춘다
int32 recvLen = ::recv(clientSocket, recvBuffer, sizeof(recvBuffer), 0);

 

send()의 경우, 만약 이런저런 이유로 클라이언트의 OS 송신 버퍼가 꽉 차있다면, 메인 스레드는 OS 송신 버퍼에 자리가 빌 때까지 잠들게 된다. (잠들기 때문에 대기하는데 CPU 리소스를 크게 잡아먹진 않는다)

// 정확히 이 줄에서 멈춘다
int32 resultCode = ::send(clientSocket, sendBuffer, sizeof(sendBuffer), 0);

 

이렇게 데이터를 송수신 할 때마다 블로킹되는 경우 네트워크 처리가 되기 전까지 컴퓨터가 먹통이 되는 문제가 있다. 안그래도 클라이언트는 메시지 수신말고도 그래픽 처리와 같은 할 일이 많은데 이러한 블로킹으로 인한 성능 저하는 납득할 수 없을 것이다. 따라서, 이런 블로킹 함수를 사용하는 것은 합리적인 선택이라 할 수 없다.

 

send() 함수는 상대가 수신받지 못해도 성공 리턴을 한다.

send() 함수는 이름이 무색하게도 상대 호스트에게 데이터 전송 성공과 관련없이 리턴을 하는데, 그 이유는 실제 데이터 송수신과 관련된 실행 권한은 유저 레벨에선 없기 때문이다. 실행 권한은 하드웨어를 관리하는 운영체제(OS)에게 있으며, 운영체제는 소켓을 만들 때 같이 생성된 소켓 입출력 버퍼(송/수신 버퍼)에 데이터를 커널 모드를 통해 저장하게 된다.

 

따라서, 유저 레벨에서의 코드일 뿐인 send() 함수는 운영체제에게 데이터 송신에 대한 요청만 가능하며, 운영체제가 송신 버퍼에 성공적으로 데이터를 복사하여 요청을 완료했다면 send() 함수는 "송신에 성공했다"고 가정한다.


깃헙 코드

https://github.com/pledge24/WinGameServerPractice/tree/e78353626a0f9f0b6169b91b64c2dc62b3203343

 

GitHub - pledge24/WinGameServerPractice

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

github.com