KoreanFoodie's Study

[C++ 게임 서버] 4-2. IocpCore 본문

Game Dev/Game Server

[C++ 게임 서버] 4-2. IocpCore

GoldGiver 2023. 10. 26. 23:02

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

[C++ 게임 서버] 4-2. IocpCore

핵심 :

1. IOCP 모델의 큰 그림을 이해하는 것이 중요하다. Completion Port, Socket, Session 등의 개념을 잘 알아두자.

2. 앞으로는 IocpCore 를 활용하여 API 를 캡슐화한 API 를 사용할 것이다. IocpCore 는 IocpObject 를 간접적으로 활용한다.

3. OVERLAPPED 를 상속받은 IocpEvent 와, HANDLE 을 편리하게 사용할 수 있는 IocpObject 를 상속받아 세부 사항을 구현할 것이다.

이제 IOCP 모델을 구현함에 있어 줄곧 사용할 IOCP 관련 객체들을 만들어보자.

사실 IocpCore 라고 거창하게 이름을 지었지만, 실상 기존에 사용하던 HANDLE, Socket, Session 관련 함수들을 편하게 사용하기 위한 Wrapper 클래스 정도에 지나지 않는다.

중요한 것은 IOCP 모델의 큰 그림이 머릿속에 그려져야 한다는 것이다. 이전에 이 글에서 내용을 정리한 바 있으니, 헷갈린다면 다시 읽어보도록 하자.

 

어쨌든, 이제 차근차근 살펴보자. 이번 시간에는 Bottom-Up 방식 대신, Top-Down 방식으로 코드를 살펴볼 것이다.

 

GameServer.cpp

#include "Listener.h"

int main()
{
	Listener listener;
	listener.StartAccept(NetAddress(L"127.0.0.1", 7777));

	for (int32 i = 0; i < 5; i++)
	{
		GThreadManager->Launch([=]()
			{
				while (true)
				{
					GIocpCore.Dispatch();
				}				
			});
	}	

	GThreadManager->Join();
}

먼저, 게임 서버에서는 Listener 소켓을 127.0.0.1 주소의 7777 번 포트에 연결한 후, 쓰레드 5개를 돌리면서 Dispatch 를 시킬 것이다.

자, 일단 Listener 객체가 무엇이길래, StartAccept 를 위처럼 간단하게 호출할 수 있는 것일까? Listener 함수는 다음과 같이 생겼다 :

class AcceptEvent;

/*--------------
	Listener
---------------*/

class Listener : public IocpObject
{
public:
	Listener() = default;
	~Listener();

public:
	/* 외부에서 사용 */
	bool StartAccept(NetAddress netAddress);
	void CloseSocket();

public:
	/* 인터페이스 구현 */
	virtual HANDLE GetHandle() override;
	virtual void Dispatch(class IocpEvent* iocpEvent, int32 numOfBytes = 0) override;

private:
	/* 수신 관련 */
	void RegisterAccept(AcceptEvent* acceptEvent);
	void ProcessAccept(AcceptEvent* acceptEvent);

protected:
	SOCKET _socket = INVALID_SOCKET;
	Vector<AcceptEvent*> _acceptEvents;
};

IocpObject 를 상속받아, Accept, CloseSocket 등 편의성 함수를 가지고 있는 녀석으로 보이는데... 여기에도 Dispatch 가 있는 것을 확인할 수 있다.

 

그럼 이제 IocpObject 클래스가 뭔지 확인해 보자.

IocpCore.h

/*----------------
	IocpObject
-----------------*/

class IocpObject
{
public:
	virtual HANDLE GetHandle() abstract;
	virtual void Dispatch(class IocpEvent* iocpEvent, int32 numOfBytes = 0) abstract;
};

/*--------------
	IocpCore
---------------*/

class IocpCore
{
public:
	IocpCore();
	~IocpCore();

	HANDLE		GetHandle() { return _iocpHandle; }

	bool		Register(class IocpObject* iocpObject);
	bool		Dispatch(uint32 timeoutMs = INFINITE);

private:
	HANDLE		_iocpHandle;
};

// TEMP
extern IocpCore GIocpCore;

IocpObject 는 간단히 말해서, IocpCore 라는 클래스를 통해 Completion Port 객체를 다루게 되는데, 이때 핸들을 반환하거나, Dispatch 에 대한 동작을 정의하기 위해 쓰이는 부모 추상 클래스이다.

 

IocpCore.cpp

/*--------------
	IocpCore
---------------*/

IocpCore::IocpCore()
{
	_iocpHandle = ::CreateIoCompletionPort(INVALID_HANDLE_VALUE, 0, 0, 0);
	ASSERT_CRASH(_iocpHandle != INVALID_HANDLE_VALUE);
}

IocpCore::~IocpCore()
{
	::CloseHandle(_iocpHandle);
}

bool IocpCore::Register(IocpObject* iocpObject)
{
	return ::CreateIoCompletionPort(iocpObject->GetHandle(), _iocpHandle, reinterpret_cast<ULONG_PTR>(iocpObject), 0);
}

bool IocpCore::Dispatch(uint32 timeoutMs)
{
	DWORD numOfBytes = 0;
	IocpObject* iocpObject = nullptr;
	IocpEvent* iocpEvent = nullptr;

	if (::GetQueuedCompletionStatus(_iocpHandle, OUT &numOfBytes, OUT reinterpret_cast<PULONG_PTR>(&iocpObject), OUT reinterpret_cast<LPOVERLAPPED*>(&iocpEvent), timeoutMs))
	{
		iocpObject->Dispatch(iocpEvent, numOfBytes);
	}
	else
	{
		int32 errCode = ::WSAGetLastError();
		switch (errCode)
		{
		case WAIT_TIMEOUT:
			return false;
		default:
			// TODO : 로그 찍기
			iocpObject->Dispatch(iocpEvent, numOfBytes);
			break;
		}
	}

	return true;
}

실제 구현은 위처럼 되어 있는데, 위 코드를 잘 보면... 생성자에서 핸들을 만들고, 소멸자에서는 핸들을 닫아 주고 있다.

Register 함수를 이용해 Completion Port 와 지정된 파일 핸들을 연결하고, Dispatch 함수를 이용해 연동된 iocpHandle 이 받아야 할 정보 및 이벤트를 받고, iocpObject 를 통해 지정된 동작을 수행 (iocpObject->Dispatch) 함을 알 수 있다.

아까 맨 처음 GameServer 에서 아래 코드가 있었는데...

// 임시적으로 전역 객체로 선언
IocpCore GIocpCore;

/* ... */
// Main 함수 내에서...
GIocpCore.Dispatch();

이는 즉, GIocpCore 가 등록한 iocpObject 들에 대해, 각 쓰레드가 자신에게 할당된 파일 핸들에 맞게 적절한 Dispatch 작업을 할 것임을 기대할 수 있다. 😎

 

그럼 이제 이어서 Listener 쪽의 실제 구현을 보면...

/*--------------
	Listener
---------------*/

Listener::~Listener()
{
	SocketUtils::Close(_socket);

	for (AcceptEvent* acceptEvent : _acceptEvents)
	{
		// TODO

		xdelete(acceptEvent);
	}
}

bool Listener::StartAccept(NetAddress netAddress)
{
	_socket = SocketUtils::CreateSocket();
	if (_socket == INVALID_SOCKET)
		return false;

	if (GIocpCore.Register(this) == false)
		return false;

	if (SocketUtils::SetReuseAddress(_socket, true) == false)
		return false;

	if (SocketUtils::SetLinger(_socket, 0, 0) == false)
		return false;

	if (SocketUtils::Bind(_socket, netAddress) == false)
		return false;

	if (SocketUtils::Listen(_socket) == false)
		return false;

	const int32 acceptCount = 1;
	for (int32 i = 0; i < acceptCount; i++)
	{
		AcceptEvent* acceptEvent = xnew<AcceptEvent>();
		_acceptEvents.push_back(acceptEvent);
		RegisterAccept(acceptEvent);
	}

	return false;
}

void Listener::CloseSocket()
{
	SocketUtils::Close(_socket);
}

HANDLE Listener::GetHandle()
{
	return reinterpret_cast<HANDLE>(_socket);
}

void Listener::Dispatch(IocpEvent* iocpEvent, int32 numOfBytes)
{
	ASSERT_CRASH(iocpEvent->GetType() == EventType::Accept);
	AcceptEvent* acceptEvent = static_cast<AcceptEvent*>(iocpEvent);
	ProcessAccept(acceptEvent);
}

void Listener::RegisterAccept(AcceptEvent* acceptEvent)
{
	Session* session = xnew<Session>();

	acceptEvent->Init();
	acceptEvent->SetSession(session);

	DWORD bytesReceived = 0;
	if (false == SocketUtils::AcceptEx(_socket, session->GetSocket(), session->_recvBuffer, 0, sizeof(SOCKADDR_IN) + 16, sizeof(SOCKADDR_IN) + 16, OUT & bytesReceived, static_cast<LPOVERLAPPED>(acceptEvent)))
	{
		const int32 errorCode = ::WSAGetLastError();
		if (errorCode != WSA_IO_PENDING)
		{
			// 일단 다시 Accept 걸어준다
			RegisterAccept(acceptEvent);
		}
	}
}

void Listener::ProcessAccept(AcceptEvent* acceptEvent)
{
	Session* session = acceptEvent->GetSession();

	if (false == SocketUtils::SetUpdateAcceptSocket(session->GetSocket(), _socket))
	{
		RegisterAccept(acceptEvent);
		return;
	}

	SOCKADDR_IN sockAddress;
	int32 sizeOfSockAddr = sizeof(sockAddress);
	if (SOCKET_ERROR == ::getpeername(session->GetSocket(), OUT reinterpret_cast<SOCKADDR*>(&sockAddress), &sizeOfSockAddr))
	{
		RegisterAccept(acceptEvent);
		return;
	}

	session->SetNetAddress(NetAddress(sockAddress));

	cout << "Client Connected!" << endl;

	// TODO

	RegisterAccept(acceptEvent);
}

StartAccept 함수를 통해 소켓을 만들고... acceptCount 갯수만큼 AcceptEvent 를 만들고, RegisterAccept 함수를 호출해 해당 AcceptEvent 객체에 대해 세션을 설정해 주고 있다.

AcceptEvent 객체는, 단순히 OVERLAPPEP 구조체를 상속받은 IocpEvent 를 만든 다음, 우리가 편하게 구별하기 쉽게 EventType 을 정의해서, 각 타입별로 상속을 한 녀석이라고 보면 된다(AcceptEvent 의 경우에는 Accept 이벤트에 대한 항목을 위해 상속 및 구현).

코드는 다음과 같다 :

enum class EventType : uint8
{
	Connect,
	Accept,
	//PreRecv,
	Recv,
	Send
};

/*--------------
	IocpEvent
---------------*/

class IocpEvent : public OVERLAPPED
{
public:
	IocpEvent(EventType type);

	void		Init();
	EventType	GetType() { return _type; }

protected:
	EventType	_type;
};

/*----------------
	ConnectEvent
-----------------*/

class ConnectEvent : public IocpEvent
{
public:
	ConnectEvent() : IocpEvent(EventType::Connect) { }
};

/*----------------
	AcceptEvent
-----------------*/

class AcceptEvent : public IocpEvent
{
public:
	AcceptEvent() : IocpEvent(EventType::Accept) { }

	void		SetSession(Session* session) { _session = session; }
	Session*	GetSession() { return _session; }

private:
	Session*	_session = nullptr;
};

/*----------------
	RecvEvent
-----------------*/

class RecvEvent : public IocpEvent
{
public:
	RecvEvent() : IocpEvent(EventType::Recv) { }
};

/*----------------
	SendEvent
-----------------*/

class SendEvent : public IocpEvent
{
public:
	SendEvent() : IocpEvent(EventType::Send) { }
};


/********************************************************************************/
/********************************************************************************/
/********************************************************************************/


/*--------------
	IocpEvent
---------------*/

IocpEvent::IocpEvent(EventType type) : _type(type)
{
	Init();
}

void IocpEvent::Init()
{
	OVERLAPPED::hEvent = 0;
	OVERLAPPED::Internal = 0;
	OVERLAPPED::InternalHigh = 0;
	OVERLAPPED::Offset = 0;
	OVERLAPPED::OffsetHigh = 0;
}

 

이제 마지막으로, Listener 쪽에서 Dispatch 를 하는 흐름을 보면...

void Listener::Dispatch(IocpEvent* iocpEvent, int32 numOfBytes)
{
	ASSERT_CRASH(iocpEvent->GetType() == EventType::Accept);
	AcceptEvent* acceptEvent = static_cast<AcceptEvent*>(iocpEvent);
	ProcessAccept(acceptEvent);
}

void Listener::ProcessAccept(AcceptEvent* acceptEvent)
{
	Session* session = acceptEvent->GetSession();

	if (false == SocketUtils::SetUpdateAcceptSocket(session->GetSocket(), _socket))
	{
		RegisterAccept(acceptEvent);
		return;
	}

	SOCKADDR_IN sockAddress;
	int32 sizeOfSockAddr = sizeof(sockAddress);
	if (SOCKET_ERROR == ::getpeername(session->GetSocket(), OUT reinterpret_cast<SOCKADDR*>(&sockAddress), &sizeOfSockAddr))
	{
		RegisterAccept(acceptEvent);
		return;
	}

	session->SetNetAddress(NetAddress(sockAddress));

	cout << "Client Connected!" << endl;

	// TODO

	RegisterAccept(acceptEvent);
}

Dispatch 를 호출해 ProcessAccept 를 불러 주는 것을 알 수 있다.

그럼 적절한 이벤트가 등록되고, RegisterAccept 를 해 주는 것을 알 수 있다. 참고로, 실패할 경우에는 다시 낚싯대를 드리우듯 RegisterAccept 를 다시 호출해 주어야 한다! 🤣

Comments