나만의 작은 도서관

[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 시스템 콜을 통해 데이터 송신 요청을 운영체제에게 전달한다.

 

요청을 전달받은 운영체제는 해당 소켓의 송신 버퍼에 전달받은 데이터를 밀어 넣는 작업을 실행하고, 작업 성공 시, 성공을 리턴한다.

 

즉, send() 함수의 리턴값은 '상대 호스트에게 성공적으로 데이터를 송신했는가?'의 의미가 아닌, '운영체제가 소켓 SendBuffer에 데이터를 성공적으로 넣었는가?'의 의미를 지닌다.


깃헙 코드

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

 

GitHub - pledge24/WinGameServerPractice

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

github.com