KoreanFoodie's Study

[C++ 게임 서버] 4-6. Session #3 본문

Game Dev/Game Server

[C++ 게임 서버] 4-6. Session #3

GoldGiver 2023. 12. 12. 20:17

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

[C++ 게임 서버] 4-6. Session #3

핵심 :

1. 소켓을 세션이 닫힐 때마다 Close 하지 않고, 재사용할 수 있음을 알아 두자(DisconnectEx)

2. 세션 클래스를 통신의 얼개를 만들어 두면, 서버-클라이언트 단에서는 앞으로 사실상 로직과 관련된 구현만 집중해도 된다!

이번 글에서는 세션의 구현을 마무리 지으면서, 동시에 DummyClient 에서도 세션을 통한 구현을 적용해 보도록 하겠다.

일단 이전에 Session 에서 구현했던 Disconnect 함수를 다시 보면...

void Session::Disconnect(const WCHAR* cause)
{
	if (_connected.exchange(false) == false)
		return;

	// TEMP
	wcout << "Disconnect : " << cause << endl;

	OnDisconnected(); // 컨텐츠 코드에서 오버로딩
	SocketUtils::Close(_socket);
	GetService()->ReleaseSession(GetSessionRef());
}

위와 같은 식으로, 연결이 끊겼을 때 소켓을 닫아 주고 있는 것을 알 수 있다.

그런데, 사실 소켓을 만들고 여닫는 비용은 생각보다 크기 때문에, 세션이 끝나도 기존에 사용하고 있던 소켓을 재활용하고 싶다는 생각이 들 수도 있다! 🤔

만약 그런 경우에는 어떤 식으로 구현해야 할까? 아래 함수를 한 번 참고해 보자.

void Session::Disconnect(const WCHAR* cause)
{
	if (_connected.exchange(false) == false)
		return;

	// TEMP
	wcout << "Disconnect : " << cause << endl;

	OnDisconnected(); // 컨텐츠 코드에서 재정의
	GetService()->ReleaseSession(GetSessionRef());

	RegisterDisconnect();
}

위의 함수를 보면, 소켓을 바로 닫는 것이 아니라 RegisterDisconnect 라는 함수를 호출하고 있다. 이 함수는 아래와 같이 생겼는데...

bool Session::RegisterDisconnect()
{
	_disconnectEvent.Init();
	_disconnectEvent.owner = shared_from_this(); // ADD_REF

	if (false == SocketUtils::DisconnectEx(_socket, &_disconnectEvent, TF_REUSE_SOCKET, 0))
	{
		int32 errorCode = ::WSAGetLastError();
		if (errorCode != WSA_IO_PENDING)
		{
			_disconnectEvent.owner = nullptr; // RELEASE_REF
			return false;
		}
	}

	return true;
}

Disconnect 이벤트를 처리하면서, 소켓과의 연결을 끊어주고 있다(DisconnectEx 함수). 이 함수의 설명을 MS 문서에서 살펴보면,

소켓의 연결을 종료하면서, 소켓의 핸들의 재사용이 가능하도록 만드는 함수라고 되어 있다. 사실, 위에서 우리가 넣었던 TF_REUSE_SOCKET 인자가 그 기능을 하도록 만들어 준다. 😉

어쨌든, 위 함수의 구현으로 소켓을 재활용할 수 있게 되었다. 자세히 보면 RegisterDisconnect 함수는 bool 을 리턴하는 것을 알 수 있는데... RegisterConnect 의 경우에도, 연결이 잘 되었는지 아닌지 여부를 bool 값으로 리턴하도록 만들 것이다!

bool Session::RegisterConnect()
{
	if (IsConnected())
		return false;

	if (GetService()->GetServiceType() != ServiceType::Client)
		return false;

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

	if (SocketUtils::BindAnyAddress(_socket, 0/*남는거*/) == false)
		return false;

	_connectEvent.Init();
	_connectEvent.owner = shared_from_this(); // ADD_REF

	DWORD numOfBytes = 0;
	SOCKADDR_IN sockAddr = GetService()->GetNetAddress().GetSockAddr();
	if (false == SocketUtils::ConnectEx(_socket, reinterpret_cast<SOCKADDR*>(&sockAddr), sizeof(sockAddr), nullptr, 0, &numOfBytes, &_connectEvent))
	{
		int32 errorCode = ::WSAGetLastError();
		if (errorCode != WSA_IO_PENDING)
		{
			_connectEvent.owner = nullptr; // RELEASE_REF
			return false;
		}
	}

	return true;
}

위 함수의 동작은 RegisterDisconnect 함수와 매우 흡사해 보인다. 물론 몇몇 예외 처리가 추가되었지만. 😁

그 중 BindAnyAddress 를 보면, 실제로 다음과 같이 구현되어 있는데...

bool SocketUtils::BindAnyAddress(SOCKET socket, uint16 port)
{
	SOCKADDR_IN myAddress;
	myAddress.sin_family = AF_INET;
	myAddress.sin_addr.s_addr = ::htonl(INADDR_ANY);
	myAddress.sin_port = ::htons(port);

	return SOCKET_ERROR != ::bind(socket, reinterpret_cast<const SOCKADDR*>(&myAddress), sizeof(myAddress));
}

두 번째 인자로 0 을 넣게 되면, 남는 port 를 알아서 소켓이 가져가게 된다. 😊

우리는 위 RegisterConnect 함수를, Session 의 Connect 함수에서 사용할 것이다.

bool Session::Connect()
{
	return RegisterConnect();
}

 

자, 이제 Session 의 구현은 사실상 거의 끝났으니, 이것을 본격적으로 DummyClient 에도 적용해 보자. 기존에 WSA 관련 함수들로 점철된 것들을 날리고, 우리가 구현한 Session 클래스를 이용하여 Server 에서 처럼 깔끔하게 코드를 정리할 수 있을 것이다.

#include "pch.h"
#include "ThreadManager.h"
#include "Service.h"
#include "Session.h"

char sendBuffer[] = "Hello World";

class ServerSession : public Session
{
public:
	~ServerSession()
	{
		cout << "~ServerSession" << endl;
	}

	virtual void OnConnected() override
	{
		cout << "Connected To Server" << endl;
		Send((BYTE*)sendBuffer, sizeof(sendBuffer));
	}

	virtual int32 OnRecv(BYTE* buffer, int32 len) override
	{
		cout << "OnRecv Len = " << len << endl;

		this_thread::sleep_for(1s);

		Send((BYTE*)sendBuffer, sizeof(sendBuffer));
		return len;
	}

	virtual void OnSend(int32 len) override
	{
		cout << "OnSend Len = " << len << endl;
	}

	virtual void OnDisconnected() override
	{
		cout << "Disconnected" << endl;
	}
};

int main()
{
	this_thread::sleep_for(1s);

	ClientServiceRef service = MakeShared<ClientService>(
		NetAddress(L"127.0.0.1", 7777),
		MakeShared<IocpCore>(),
		MakeShared<ServerSession>, // TODO : SessionManager 등
		1);

	ASSERT_CRASH(service->Start());

	for (int32 i = 0; i < 100; i++)
	{
		GThreadManager->Launch([=]()
			{
				while (true)
				{
					service->GetIocpCore()->Dispatch();
				}
			});
	}

	GThreadManager->Join();
}

ServerSession 을 따로 정의하고, 연결 및 전송과 관련된 함수를 정의한 후, Dispatch 를 시켜주기만 하면, 기존과 동일하게 잘 동작함을 확인할 수 있을 것이다! 😄

Comments