본문 바로가기

Overlapped 모델

@9ky02025. 9. 22. 14:25

0. 들어가기에 앞서

앞서 살펴본 Select 모델은 클라이언트 측에서 사용할 수 있으나, 서버에서 사용하기에는 무리가 있다고 얘기했다. 오늘 얘기해 볼 Overlapped 모델부터 실제 게임 서버에서 활용되는 개념이 등장하는데, 이를 알아보기 전에 간단하게 용어를 정리하고자 한다.

1. 블로킹 vs 논블로킹

함수를 호출하면 대기하는가? 아니면 바로 빠져나오는가?

 

앞서 소켓 함수 read, write 등을 사용할 때는 기본적으로 블로킹 방식으로 동작했다. 즉 이 함수를 호출하는 순간 작업이 완료되기까지 해당 스레드는 대기 모드에 들어갔다. 이러한 문제를 해결하고자 ioctlsocket 함수를 사용해 소켓을 논블로킹 모드로 전환하였고, 함수 호출 결과에 관계없이 호출하는 즉시 빠져나오는 것을 확인했었다.

2. 동기 vs 비동기

동시에 일어나는 vs 동시에 일어나지 않는

 

그런데 이제껏 사용한 send, recv 같은 함수들은 모두 '동기 방식'으로 동작하였다. 그중 블로킹 방식은 위 그림처럼 작업이 완료될 때까지 계속 Blocking 되는 것이고, 논블로킹 방식은 호출 즉시 다시 돌아와서 확인할 때만 잠시 Blocking 되는 차이이다.

 

반면 비동기 방식은 '우리가 요청한 작업을 지금 당장 하지 않아도 됨'을 의미한다. 일종의 '예약'을 걸어놓은 것과 비슷하다고 생각해도 된다. 실행 시점의 분리가 일어나는 것이다. 비유하자면, 어떤 사람에게 무언가를 묻기 위해 전화를 건다면 이는 동기 방식에 가깝고, 메일에 보낸다면 이는 비동기 방식에 가깝다. 전화를 거는 것은 그 사람이 받아서 답을 받는 동안 다른 작업을 하기 어렵고, 메일은 보내기만 하고 내 작업을 이어서 하다가 메일 알림이 오면 그때 확인하면 되기 때문이다.

 

이를 하나의 그림으로 정리하면 아래와 같다.

3. 비동기 방식

지금까지 살펴본 코드(Select 모델 등)는 비동기 방식을 적용했을 때 '내가 원하는 결과가 도출되었는가'를 수시로 확인하였다. 만약 원하는 결과가 왔다면 그때 동기 방식의 소켓 함수를 호출하는 형식으로 진행하였다.

 

하지만 이제부터 알아볼 모델은, recv나 send 자체를 예약하는 형태로 진행하게 된다. 즉 지금 당장 실행하지 않고, 이후에 '준비가 되었다면' 알아서 실행하게 된다. 그렇다면 우리는 예약한 이 작업이 완료되었는지를 판별할 수 있어야 하는데, 이를 확인하는 방법으로 크게 2가지가 있다. 하나는 위 그림처럼 callback 함수를 호출하도록 하는 것이고, 다른 하나는 이벤트를 발생시켜 이를 탐지하는 것이다.

전체적인 흐름을 요약하면 아래와 같다.

  1. 비동기 방식의 read 함수를 호출한다. 이는 '지금 당장 실행하지 않아도 된다. 대신 완료되면 우리에게 알려만 줘라'라 하는 것이다.
  2. 만약 함수가 완료되었다면 우리의 요청에 따라 signal을 주거나 callback을 호출한다.
  3. 그리고 호출한 후 완료될 때까지 우리는 다른 작업을 수행한다.

1. 이벤트 기반 Overlapped 모델

Overlapped 모델에서 사용되는 소켓 함수가 별도로 있는데, 그중 WSASend와 WSARecv 2가지 함수에 대해서 우선적으로 알아보자. Accept와 Connect의 경우 사용법이 복잡하여 추후 라이브러리를 설계할 때 알아보고자 한다. 여기서는 기존의 accept과 connect 함수를 사용한다.

 

WSASend MSDN 문서

 

WSASend 함수(winsock2.h) - Win32 apps

연결된 소켓에 데이터를 보냅니다. (WSASend)

learn.microsoft.com

int WSAAPI WSASend(
  [in]  SOCKET                             s,
  [in]  LPWSABUF                           lpBuffers,
  [in]  DWORD                              dwBufferCount,
  [out] LPDWORD                            lpNumberOfBytesSent,
  [in]  DWORD                              dwFlags,
  [in]  LPWSAOVERLAPPED                    lpOverlapped,
  [in]  LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);

API를 확인해 보면 기존의 send 함수와 거의 비슷한 것을 확인할 수 있다.

  • s: 비동기 입출력 소켓
  • lpBuffers: WSABUF 구조체 타입의 입출력 버퍼(배열) 목록의 시작 주소. 버퍼를 1개 이상 넘길 수 있다. 이는 추후 알아볼 Scatter-Gather 패턴과 관련된다.
  • dwBufferCount: 넘겨주는 입출력 버퍼 개수
  • lpNumberOfBytesSent: 전송된 데이터 바이트 크기
  • dwFlags: 추가 옵션. 우리는 0을 넘겨준다.
  • lpOverlapped: WSAOVERLAPPED 구조체의 주소값. 내부적으로 이벤트 핸들을 들고 있어, 실질적으로 작업 완료 시 트리거 시킬 이벤트를 넘겨주는 역할을 한다.
  • lpCompletionRoutine: 입출력 완료 시 호출할 콜백 함수의 주소. 다음에 알아볼 콜백 함수 기반 Overlapped 모델에서 사용한다.

WSABUF 구조체는 char 타입 포인터와 길이 정보를 들고 있다. 또한 이를 여러 개 넘길 수 있으므로, 아래와 같이 사용한다.

WSABUF ioBuffers[2];

char sendBuffer1[100];
ioBuffers[0].buf = sendBuffer1;
ioBuffers[0].len = 100;

char sendBuffer2[100];
ioBuffers[1].buf = sendBuffer2;
ioBuffers[1].len = 100;

 

이벤트 기반 Overlapped 모델의 흐름을 요약하면 아래와 같다.

  1. 비동기 입출력을 지원하는 소켓과, 입출력 완지를 통지받기 위한 이벤트 객체를 생성한다.
  2. WSASend, WSARecv 같은 비동기 입출력 함수를 호출한다. 이때 1에서 만든 이벤트 객체를 같이 넘겨준다.
  3. 비동기 입출력 함수가 완료되면 운영체제가 이벤트 객체를 signal 상태로 만들어 완료 상태를 알려준다.
  4. 비동기 입출력 함수가 완료되지 않으면 WSA_IO_PENDING 오류 코드가 반환되어 넘어간다.

이벤트 완료 상태 판별은 아래와 같은 과정으로 진행된다.

  1. WSAWaitForMultipleEvents 함수를 호출해 이벤트 객체의 signal 상태를 확인한다.
  2. signal 되었다면 WSAGetOverlappedResult 함수를 호출해 비동기 입출력 결과를 확인하고, 관련 데이터를 처리한다.
BOOL WSAAPI WSAGetOverlappedResult(
  [in]  SOCKET          s,
  [in]  LPWSAOVERLAPPED lpOverlapped,
  [out] LPDWORD         lpcbTransfer,
  [in]  BOOL            fWait,
  [out] LPDWORD         lpdwFlags
);

WSAGetOverlappedResult API에 대해 잠시 확인해 보자.

  • s: 앞서 넘겨준 비동기 소켓을 넘겨준다.
  • lpOverlapped: 앞서 넘겨준 Overlapped 구조체를 넘겨준다.
  • lpcbTransfer: 전송된 바이트 수를 의미한다.
  • fWait: 비동기 입출력 작업이 끝날 때까지 대기할지 결정하는 bool 변수이다. 
  • lpdwFlags: 부가 정보를 얻기 위해 사용하는 플래그로, 거의 사용하지 않는다. 

코드

이전까지는 에코 서버 방식으로 테스트했지만, 이번에는 클라이언트는 Send만 하고 서버에서는 Recv만 하도록 간단하게 해 본다.

while (true)
{
	SOCKADDR_IN ClientAddr;
	int32 AddrLen = sizeof(ClientAddr);

	SOCKET ClientSocket;
	while (true)
	{
		ClientSocket = ::accept(ListenSocket, reinterpret_cast<SOCKADDR*>(&ClientAddr), &AddrLen);
		if (ClientSocket != INVALID_SOCKET)
		{
			break;
		}

		if (::WSAGetLastError() == WSAEWOULDBLOCK)
		{
			continue;
		}

		// 에러가 있는 상황
		return 1;
	}

	...
}

일단은 최초에 listen 소켓을 이용해 클라이언트를 accept 하는 부분은 기존과 같은 방식으로 처리한다.

struct FSession
{
	static constexpr int32 BufferSize = 1000;
	FSession(SOCKET InSocket = INVALID_SOCKET)
    	: Socket(InSocket)
    	, Buffer{}
    	, BytesRecv(0)
    	, Overlapped{}
    {
    	if(Socket != INVALID_SOCKET)
        {
        	Overlapped.hEvent = ::WSACreateEvent();
        }
    }
    
    ~FSession()
    {
    	if(Socket != INVALID_SOCKET)
        {
        	::WSACloseEvent(Overlapped.hEvent);
            ::closesocket(Socket);
        }
    }

	SOCKET Socket;
    char Buffer[BufferSize];
    int32 BytesRecv;
    WSAOVERLAPPED Overlapped;
}

이제부터 Session 구조체가 Overlapped 구조체를 들고 있도록 수정한다. WSAOVERLAPPED 구조체는 내부적으로 이벤트 핸들을 들고 있기 때문에, 생성자에서 WSACreateEvent API를 통해 이벤트를 생성해 준다.

while (true)
{
	...
	FSession Session(ClientSocket);
	cout << "Client Connected!" << endl;

	while (true)
	{
		WSABUF Buffer(FSession::BufferSize, Session.Buffer);
		DWORD BytesRecv = 0;
		DWORD Flags = 0;
		if (::WSARecv(ClientSocket, &Buffer, 1, &BytesRecv, &Flags, &Session.Overlapped, nullptr) == SOCKET_ERROR)
		{
			if (::WSAGetLastError() == WSA_IO_PENDING)
			{
				::WSAWaitForMultipleEvents(1, &Session.Overlapped.hEvent, true, WSA_INFINITE, false);
				::WSAGetOverlappedResult(Session.Socket, &Session.Overlapped, &BytesRecv, false, &Flags);
			}
			else
			{
				break;
			}
		}

		cout << "Data Recv Len = " << BytesRecv << endl;
	}
}

이제 비동기 방식의 Recv 함수를 호출한다. 이를 위해 위에서 살펴본 것처럼 WSABUF 구조체를 세팅한 뒤, 소켓과 버퍼, Overlapped 구조체 주소 등등을 넘겨준다.

  • 해당 함수는 호출 즉시 완료가 될 수도, 아닐 수도 있다.
  • 만약 함수의 반환값이 SOCKET_ERROR가 아니라면 즉시 완료된 것이므로, 데이터를 수신받았음을 의미한다.
  • 만약 SOCKET_ERROR가 반환됐다면 데이터가 오지 않은 것인데, 이는 Pending 상태일 수 있다.
    • 이 경우 WSAWaitForMultipleEvents를 사용해 데이터가 와서 Signal 상태가 될 때까지 계속 대기하도록 한다.
    • Signal 상태가 되었다면 이를 빠져나오게 되고, 이때 우리는 WSAGetOverlappedResult 함수를 통해 결과를 확인한다.
  • 만약 Pending 상태가 아니라면 다른 문제가 발생한 것으로 즉시 반복문을 중단한다.
char SendBuffer[100] = "Hello, Server!";
WSAEVENT Event = ::WSACreateEvent();
WSAOVERLAPPED Overlapped{};
Overlapped.hEvent = Event;

while (true)
{
	WSABUF Buffer(100, SendBuffer);
	DWORD BytesSent = 0;
	DWORD Flags = 0;

	if (::WSASend(ClientSocket, &Buffer, 1, &BytesSent, Flags, &Overlapped, nullptr) == SOCKET_ERROR)
	{
		if (::WSAGetLastError() == WSA_IO_PENDING)
		{
			::WSAWaitForMultipleEvents(1, &Event, true, WSA_INFINITE, false);
			::WSAGetOverlappedResult(ClientSocket, &Overlapped, &BytesSent, false, &Flags);
		}
		else
		{
			// Error occurred
			break;
		}
	}

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

	this_thread::sleep_for(1s);
}

테스트를 위해 클라이언트 측 코드도 수정한다. 서버와 대칭되는 구조를 가져, Recv가 Send로 변경되는 것 외에는 거의 동일하다.

정상적으로 데이터를 전송/수신하는 모습

2. 콜백 기반 Overlapped 모델

이벤트 방식에 대해 알아봤으니, 이번에는 콜백 방식에 대해 알아보자. 참고로 이번 콜백 방식이 이후 알아볼 IOCP(I/O Completion Port) 모델에서도 사용되는 방식이다.

 

콜백(Completion Routine) 기반 Overlapped 모델의 흐름을 요약하면 아래와 같다.

  1. 비동기 입출력을 지원하는 소켓을 생성한다.
  2. WSASend, WSARecv 같은 비동기 입출력 함수를 호출한다. 이때 이벤트 완료 시 호출할 콜백 함수(Completion Routine)의 시작 주소를 같이 넘겨준다.
  3. 비동기 입출력 함수가 완료되면 운영체제는 콜백 함수를 호출한다.
  4. 비동기 입출력 함수가 완료되지 않으면 WSA_IO_PENDING 오류 코드가 반환되어 넘어간다.

다만 운영체제가 콜백 함수를 호출하는 시점에 대해서 고민해 볼 부분이 있다.

  • 비동기 방식이므로, 입출력 함수를 통해 작업을 예약해 놓고 우리는 다른 작업을 수행할 수 있다.
  • 그런데 우리의 작업 도중, 운영체제의 Completion Routine이 갑자기 호출되면 문제가 발생할 수도 있다.
  • 예를 들어, Lock을 걸어 놓아서 최대한 빠르게 처리해야 하는 코드를 실행하고 있는데 갑자기 운영체제가 콜백 함수를 호출한다면 문제가 될 수 있다.
  • 따라서 운영체제가 입출력 작업을 완료할 뿐만 아니라, 우리도 "콜백 함수를 호출해도 돼"라는 상태임을 나타내야 한다. 
  • 이를 위해 비동기 입출력 함수를 호출한 스레드를 Alertable Wait 상태로 만들어야 한다.
    • WaitForSingleObjectEx, WaitForMultipleObjectsEx, SleepEx, WSAWaitForMultipleEvents,...
  • Alertable Wait 상태에서 콜백 함수 호출이 완료되면, 해당 스레드는 해당 상태에서 빠져나온다.

말로 설명하니 조금 복잡해 보이지만, 코드는 생각보다 단순하다. 이를 한 번 확인해 보자.

1. 코드

struct FSession
{
	static constexpr int32 BufferSize = 1000;
	FSession(SOCKET InSocket = INVALID_SOCKET)
    	: Socket(InSocket)
    	, Buffer{}
    	, BytesRecv(0)
    	, Overlapped{}
    {
    
    }
    
    ~FSession()
    {
    	if(Socket != INVALID_SOCKET)
        {
            ::closesocket(Socket);
        }
    }

	SOCKET Socket;
    char Buffer[BufferSize];
    int32 BytesRecv;
    WSAOVERLAPPED Overlapped;
}

더 이상 이벤트를 사용하지 않기 때문에, 세션에서 Overlapped 구조체 내 이벤트 핸들에 CreateEvent 해주는 코드를 제거했다.

...
FSession Session(ClientSocket);

while (true)
{
	WSABUF Buffer(FSession::BufferSize, Session.Buffer);
	DWORD BytesRecv = 0;
	DWORD Flags = 0;
	if (::WSARecv(ClientSocket, &Buffer, 1, &BytesRecv, &Flags, &Session.Overlapped, ??) == SOCKET_ERROR)
 	{
		if (::WSAGetLastError() == WSA_IO_PENDING)
		{
			...
		}
		else
		{
			// TODO: 문제 있는 상황
			break;
		}
	}
	else
	{
		cout << "Data Recv Len = " << BytesRecv << endl;
	}
}

ListenSocket을 통해 Client를 accept 하는 부분은 동일하게 처리한다.

 

이제부터 WSARecv 함수를 호출할 때, 아까 전에는 nullptr를 넘겨줬던 CompletionRoutine 인자에 LPWSAOVERLAPPED_COMPLETION_ROUTINE 타입의 콜백 함수 주소를 넘겨주게 된다.

typedef
void
(CALLBACK * LPWSAOVERLAPPED_COMPLETION_ROUTINE)(
    IN DWORD dwError,
    IN DWORD cbTransferred,
    IN LPWSAOVERLAPPED lpOverlapped,
    IN DWORD dwFlags
    );

이는 함수 포인터로, 위와 같은 형태의 함수 포인터만 받을 수 있도록 제한되어 있다. 각각의 인자는 다음을 의미한다.

  • dwError: 오류 발생 시 0이 아닌 값이 들어온다.
  • cbTransferred: 전송된 데이터 바이트 수를 의미한다.
  • lpOverlapped: 비동기 입출력 함수 호출 시 넘겨준 Overlapped 구조체의 주소값을 넘겨준다.
  • dwFlags: 추가 옵션을 의미하는데, 사용하지 않을 예정이라 0을 넘겨준다.

이 규칙에 맞게 콜백 함수를 아래와 같이 정의해 본다.

void CALLBACK ReceiveCallback(DWORD InError, DWORD InBytesRecvd, LPWSAOVERLAPPED InOverlapped, DWORD InFlags)
{
	if(InError != 0)
    {
    	// ERROR!
        return;
    }
    
    // Echo 서버를 만들고자 하면 여기서 WSASend를 다시 호출해주면 됨
    cout << "Data Recv Len Callback = " << InBytesRecvd << endl;
}

이제 WSARecv를 호출할 때, 마지막 인자로 위에서 정의한 콜백 함수 주소를 넘겨준다.

...
FSession Session(ClientSocket);

while (true)
{
	WSABUF Buffer(FSession::BufferSize, Session.Buffer);
	DWORD BytesRecv = 0;
	DWORD Flags = 0;
	if (::WSARecv(ClientSocket, &Buffer, 1, &BytesRecv, &Flags, &Session.Overlapped, ReceiveCallback) == SOCKET_ERROR)
 	{
		if (::WSAGetLastError() == WSA_IO_PENDING)
		{
			// Pending
			// Alertable Wait
			::SleepEx(INFINITE, true);
			//::WSAWaitForMultipleEvents(1, &Session.Event, true, WSA_INFINITE, true);
		}
		else
		{
			// TODO: 문제 있는 상황
			break;
		}
	}
	else
	{
		cout << "Data Recv Len = " << BytesRecv << endl;
	}
}

해당 함수의 반환값이 SOCKET_ERROR일 경우 Pending 상태를 체크하는 부분이 있는데, 이 부분에서 스레드를 Alertable로 바꿔줘야 한다.

  • 기존에 WSAWaitForMultipleEvents 함수의 마지막 인자가 alertable 상태로 세팅할 것인지 결정하는 bool 타입 변수이다. 해당 인자에 true를 넘겨줘도 된다.
  • 여기서는 SleepEx API를 호출해 본다. 이는 호출할 콜백 함수가 있을 때까지 대기하도록 만들어주는 역할을 한다.

APC Queue

스레드는 내부적으로 APC Queue라는 것을 들고 있게 된다. 콜백 함수가 호출되어야 하면, 내부적으로 들고 있는 이 APC 큐에 콜백 함수를 Push 하게 된다. 그리고 SleepEx와 같은 API를 통해 스레드가 Alertable 상태가 되면, Queue에 있는 콜백 함수들을 하나씩 Pop 하며 처리하는 구조로 되어 있다. 콜백 함수 호출이 완료되면 Alertable 상태를 빠져나온다. 그 후 나머지 코드들을 실행하게 된다.

 

2. 이벤트 방식과의 차이점

이전에 이벤트 기반 모델을 사용할 때는 세션마다 CreateEvent를 통해 이벤트 객체를 하나씩 들고 있었다. 즉 소켓과 이벤트가 1:1로 대응시켜서 연동해 줘야 사용할 수 있었다. 반면 현재 콜백 방식은 이벤트 객체를 만드는 것 없이, 콜백 함수만 정의해서 넘겨주면 처리가 되기 때문에 흐름이 단순해졌다.

 

동시에 접속하는 클라이언트 수가 많아지면 이벤트 방식에서는 Pending 시 WaitForMultipleEvents를 사용하게 될 것이다. 다만 해당 API는 최대 64개까지만 이벤트를 탐지할 수 있기 때문에, 이보다 더 많은 클라이언트를 처리하기 위해 스레드를 더 늘리는 등의 추가 작업을 해줘야 한다. 하지만 콜백 방식은 스레드를 Alertable 상태로 만드는 순간 예약된 콜백 함수를 모두 처리해 주기 때문에, 클라이언트 개수에 비례해 이벤트를 생성하는 등의 작업을 해줄 필요가 없다.

 

3. 콜백 함수의 한계와 개선

void CALLBACK ReceiveCallback(DWORD InError, DWORD InBytesRecvd, LPWSAOVERLAPPED InOverlapped, DWORD InFlags)
{
	...
}

잠시 콜백 함수의 파라미터를 다시 확인해 보자.

  • InError: 에러 확인용 변수
  • InBytesRecvd: 전송된 바이트 수
  • InOverlapped: Overlapped 구조체 주소
  • InFlags: 추가 옵션(여기선 사용하지 않음)

현재 4개의 파라미터 중 우리가 유의미하게 사용할 수 있는 정보가 없다. Overlapped 구조체에 이벤트 객체를 들고 있을 수 있지만, 콜백 함수 기반으로 동작하기 때문에 이벤트를 사용하지 않아 이조차 쓸모없다. 만약 이런 상황에서 클라이언트 소켓이 여러 개 들어와서 '어떤 클라이언트 대상으로 해당 콜백 함수가 호출되었는지'를 알고 싶다고 하면 굉장히 난감하다.

 

이를 해결하기 위해, LPWSAOVERLAPPED 타입 파라미터에 단순히 Overlapped 구조체를 넘기는 것이 아닌 추가 정보를 같이 넘기도록 수정할 수 있다.

struct FSession : WSAOVERLAPPED
{
	static constexpr int32 BufferSize = 1000;

	SOCKET Socket;
	char Buffer[BufferSize];
	int32 BytesRecv;

	explicit FSession(SOCKET InSocket = INVALID_SOCKET)
		: Socket(InSocket)
		, Buffer{}
		, BytesRecv(0)
	{
		
	}

	~FSession()
	{
		if(Socket != INVALID_SOCKET)
        {
        	::closesocket(Socket);
        }
	}
};

여러 방법이 있을 수 있는데, 여기서는 세션 구조체가 WSAOVERLAPPED를 상속받도록 수정해 본다.

if (::WSARecv(ClientSocket, &Buffer, 1, &BytesRecv, &Flags, &Session, ReceiveCallback) == SOCKET_ERROR)
{
	if (::WSAGetLastError() == WSA_IO_PENDING)
	{
		// Pending
		// Alertable Wait
		::SleepEx(INFINITE, true);
		//::WSAWaitForMultipleEvents(1, &Session.Event, true, WSA_INFINITE, true);
	}
	else
	{
		// TODO: 문제 있는 상황
		break;
	}
}

이렇게 되면, 기존에 Overlapped 구조체를 받는 인자에 Session을 넘길 수 있게 된다. 이는 WSAOVERLAPPED를 상속받으면서 IsA 관계에 따라 자식은 부모로 캐스팅될 수 있기 때문이다.

void CALLBACK ReceiveCallback(DWORD InError, DWORD InBytesRecvd, LPWSAOVERLAPPED InOverlappedPtr, DWORD Flags)
{
	FSession* Session = static_cast<FSession*>(InOverlappedPtr);
    Session->BytesRecvd = InBytesRecvd;
    ...
}

즉 이런 식으로 콜백 함수 내부에서 세션으로 캐스팅한 뒤, 세션에 들고 있도록 한 추가 정보를 여기서 기입하도록 한다.

콜백 함수 내부에서 세션으로 캐스팅하니 소켓, 버퍼, 수신받은 바이트 수를 볼 수 있음을 확인할 수 있다

4. 단점

Select 모델에 비해 성능도 좋고, 앞서 본 이벤트 기반 Overlapped 모델에 비해 클라이언트 개수 제한에서도 자유롭다. 하지만 이 방식에 대해 단점도 존재한다.

  • 첫 번째는 모든 비동기 소켓 함수를 지원하지 않는다. 예를 들어 accept 함수의 경우 AcceptEx라는 버전을 사용하게 되는데, 해당 API를 확인해 보면 Callback 함수를 넘겨주는 인자가 존재하지 않는다.
  • 두 번째는 스레드가 수시로 Alertable 상태로 전환되어 성능 저하가 발생한다. 게임 서버의 경우 최대한 성능을 끌어올려야 하는데, Select 모델보다 낫다고는 해도 여전히 이것만으로는 부족하다.
  • 세 번째는 입출력 작업 분배가 불편하다. 스레드 별로 APC Queue를 들고 있기 때문에, Recv/Send 같은 소켓 함수를 호출한 스레드가 최종적으로 콜백 함수도 처리하게 된다. 이러한 구조 때문에 작업 분배에 아쉬움이 생긴다.

이러한 문제를 해결하기 위해, 최종적으로 IOCP라는 모델을 채택하여 사용한다. 이를 다음 포스팅에서 알아본다.

'Game Development > Server' 카테고리의 다른 글

IOCP(Input/Output Completion Port) 모델  (1) 2025.09.23
Select 모델  (4) 2025.08.25
논블로킹 소켓  (6) 2025.08.14
TCP 서버  (2) 2025.07.28
소켓 프로그래밍 기초  (5) 2025.07.22
9ky0
@9ky0 :: Hello, 9ky0!

공감하셨다면 ❤️ 구독도 환영합니다! 🤗

목차