Study/C++ & C#

[C++] Service

BVM 2023. 6. 23. 03:32

오늘도 최대한 메모해 보자.

 

1. Service란?

여기서 서비스란 흩어져 있는 여러 기능들을 한 곳에 모아둬, 보다 편하게 사용할 수 있게 한 집합이다.

언제 리스너/송신/수신 오브젝트를 일일이 만들고 설정한단 말인가?

이전에 했던 일련의 작업들이 서비스에 포함된 상태로 동작할 것이다.

 

라이브 서비스에선 모든 기능을 하나의 서버에 두는 것이 아니라,
목적에 맞게 서버를 분산해 운용한다.
서버끼리 붙어야 할 일도 생길 수 있기 때문에 이에 대응해야 한다.
세션이 서버일 수도 있고 클라이언트일 수도 있기 때문에,
Service라는 클래스를 두어 정책을 구분해 운용하기로 한 것이다.

 

 

1. 문제 해결 부터

본격적으로 Service에 대한 구현에 들어가기 전에 이전 과정의 문제에 대한 수정이 필요하다.

 

우린 CP에 핸들을 등록하는 것을 아래와 같이 구현했었다.

// IocpCore.cpp

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

키값으로 IocpObject의 포인터를 넘겨줬는데,

만약 저 오브젝트가 사라져 버려서 메모리 오염이 생겨버린다면?

이 부분은 키값과 OVERLAPPED 구조체가 모두 살아있어야 한다는 보장이 필요하다.

 

// IocpCore.cpp

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
	{
    	// ...
    }    
}

IocpEvent는 큰 문제가 없을 수가 있지만 키값이 쓸 수 없게 되면 진짜 큰 문제가 된다.

 

그럼 일이 다 끝나기 전에 메모리 오염이 발생하는 것을 막기 위해 레퍼런스 카운팅을 하는 방법을 떠올릴 수 있다.

하지만 여기엔 문제가 있다.

 

  • 만약 외부에서 다른 스마트 포인터로 카운팅에 들어가 이중으로 카운팅 된다면?
    - 이로 인해 RefCounting은 사용할 수 없게 되었다.
  • shared_ptr을 사용하면 프로그래머가 직접적으로 카운팅을 조작할 수 없음

따라서 Event에 IocpObject를 물려주는 방법을 사용할 것이다.

이러면 키를 0으로 밀고 Event만으로 관리할 수 있다.

 

IocpEvent 클래스에 아래와 같이 레퍼런스 변수를 추가한다.

 

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

	void			Init();

public:
	EventType		eventType;
	IocpObjectRef		owner; // 이벤트의 주인을 기억
};

원래  shared_ptr을 사용해서 선언해야 하지만 역시 그 긴걸 일일 타이핑 하기엔 번거롭다.

따라서 아래와 같이 미리 선언을 해 두고 활용하기로 한다.

 

EventType도 원래 GetType()으로 가져왔지만 public으로 열어두고 eventType을 직접 꺼내쓸 수 있도록 했다.

 

// shared_ptr
// 번거로운 타이핑 피하기 위함
using IocpCoreRef		= std::shared_ptr<class IocpCore>;
using IocpObjectRef		= std::shared_ptr<class IocpObject>;
using SessionRef		= std::shared_ptr<class Session>;
using ListenerRef		= std::shared_ptr<class Listener>;
using ServerServiceRef		= std::shared_ptr<class ServerService>

미리 ServerSerivceRef까지 만들어 둔다.

여기선 각 클래스에 대해 할 수가 없으니 전방선언한다.

 

다시 IocpEvent.h로 돌아와서 AcceptEvent 클래스에 세션 포인터 변수를 추가한다.

// IocpEvent.h
public:
	SessionRef	session = nullptr;

 

 

 

이제 기존 코드들을 수정해 나간다.

// IocpCore.cpp

bool IocpCore::Register(IocpObjectRef iocpObject)
{
	return ::CreateIoCompletionPort(iocpObject->GetHandle(), _iocpHandle, /*key*/0, 0);
}

더 이상 키값이 필요 없기 때문에 키값을 0으로 밀어준 상태에서 걸어준다.

 

 

// IocpCore.cpp

bool IocpCore::Dispatch(uint32 timeoutMs)
{
	DWORD numOfBytes = 0;
	ULONG_PTR key = 0;
	IocpEvent* iocpEvent = nullptr;
    
	if (::GetQueuedCompletionStatus(_iocpHandle, OUT & numOfBytes, OUT & key, OUT reinterpret_cast<LPOVERLAPPED*>(&iocpEvent), timeoutMs))
	{
		IocpObjectRef iocpObject = iocpEvent->owner; // 여기서 IocpObject를 넘겨주고
		iocpObject->Dispatch(iocpEvent, numOfBytes); // 그걸로 Dispatch
	}
	else
	{
		int32 errCode = ::WSAGetLastError();
		switch (errCode)
		{
		case WAIT_TIMEOUT:
			return false;
		default:
			// TODO : 로그 찍기
			IocpObjectRef iocpObject = iocpEvent->owner;
			iocpObject->Dispatch(iocpEvent, numOfBytes);
			break;
			return true;
		}

	}
}

기존의 IocpObject를 선언하는 것은 지우고, Key도 0으로 밀어준다.

IocpObjectRef를 선언해 이벤트에서 오브젝트를 받아 둔다.

넘겨받은 오브젝트로 Dispatch를 호출하게 한다.

아래쪽에도 위와 동일하게 작성하여 Dispatch 할 수 있도록 한다.

 

수정사항이 생겼으니 빌드하며 문제점을 잡아나간다.

아래와 같이 코드들을 수정해 나간다.

// 이 코드가
Session* session = xnew<Session>();

// 이렇게 변하는 식
SessionRef session = MakeShared<Session>();

사실 세션을 MakeShared로 생성하는 것은 구조 상 문제가 있지만,

일단 임시로 이렇게 둔다.

 

그리고 AcceptEx()를 호출하는 데 있어서, 이 이벤트는 acceptEvent라 볼 수 있는데,

아직 acceptEvent()에 이벤트의 주인이라 할 수 있는 IocpObject를 연결해 주지 않았다.

StartAccept()로 돌아가서 acceptEvent()를 만드는 부분을 수정하자.

 

// Listener.cpp

const int32 acceptCount = 1;
for (int32 i = 0; i < acceptCount; i++)
{
	AcceptEvent* acceptEvent = xnew<AcceptEvent>();
    	acceptEvent->owner = shared_from_this(); // 추가!
	_acceptEvents.push_back(acceptEvent);
	RegisterAccept(acceptEvent);
}

여기서 왜 shared_ptr<IocpObject>(this) 의 형태로 넘겨주지 않았냐 라는 의문이 생길 수도 있다.

이렇게 해 버리면 카운트가 1인 새로운 포인터가 생기는 것이기 때문에 치명적인 문제가 생길 수 있다.

기존의 포인터를 쓰게 하지 못해서 결국 오브젝트가 소멸해 버리는 결과가 생길 수 있다.

이 문제를 해결하기 위해 shared_from_this()를 사용했다.

 

이걸 사용하기 위해선 먼저 Listener 클래스의 부모인 IocpObject클래스의 헤더로 가서 어떤 클래스를 상속받아야 한다.

 

// IocpCore.h

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

enable_shared_from_this라는 클래스를 상속받는다.

이는 내부적으로 weak_ptr을 생성하고 이를 lock을 걸어서 넘겨주는 것으로 동작한다.

이 클래스를 상속받음으로써 이제 모든 IocpObject를 상속받는 클래스들은 shared_ptr의 형태로만 활용할 수 있다.

따라서 쌩 포인터를 만들어 스택에 올리는 것이 아닌 Make Shared를 통해 만들어야 한다는 것을 염두에 두어야 한다.

 

 

2. 클래스 구현

Service 클래스엔 많은 것들이 모일 것이다.

 

// Service.h

#pragma once
#include "NetAddress.h"
#include "IocpCore.h"
#include "Listener.h"
#include <functional>

enum class ServiceType : uint8
{
	Server,
	Client
};

using SessionFactory = function<SessionRef(void)>;

class Service : public enable_shared_from_this<Service>
{
public:
	Service(ServiceType type, NetAddress address, IocpCoreRef core, SessionFactory factory, int32 maxSessionCount = 1);
	virtual ~Service();

	// 자식 클래스는 무조건 Start()를 구현해야 함
	virtual bool			Start() abstract;
	bool				CanStart() { return _sessionFactory != nullptr; }

	virtual void			CloseService();
	void				SetSessionFactory(SessionFactory func) { _sessionFactory = func; }

	SessionRef			CreateSession();
	void				AddSession(SessionRef session);
	void				ReleaseSession(SessionRef session);
	int32				GetCurrentSessionCount() { return _sessionCount; }
	int32				GetMaxSessionCount() { return _maxSessionCount; }

public:
	ServiceType			GetServiceType() { return _type; }
	NetAddress			GetNetAddress() { return _netAddress; }
	// 외부에서 꺼내서 카운트를 늘리기 보단
	// 참조를 넘겨주게 해 부하를 줄일 수 있다
	IocpCoreRef&			GetIocpCore() { return _iocpCore; }

protected:
	USE_LOCK;
	ServiceType			_type;
	NetAddress			_netAddress = {};
	IocpCoreRef			_iocpCore;

	Set<SessionRef>			_sessions;
	int32				_sessionCount = 0;
	int32				_maxSessionCount = 0;
	SessionFactory			_sessionFactory;
};

일단 서비스 타입을 서버와 클라이언트의 2종류로 나눠놨다.

클래스는 만약을 대비해 shared ptr을 넘겨줄 수 있도록 enable_shared_from_this를 상속받는다.

SessionFactory라는 함수를 받는 녀석을 볼 수 있는데, 세션을 만들어 줄 함수를 정의한 것이다.

 

생성자로는 서비스를 정의할 정보들을 받는다.

무슨 타입인지, 클라/서버의 주소는 뭔지, 서비스를 정의할 함수는 뭔지, 동접은 얼마나 받을 것인지 등의 정보를 받는다.

 

이외의 필요한 변수화 함수들을 선언 및 정의해 준다.

 

// Service.cpp

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

Service::Service(ServiceType type, NetAddress address, IocpCoreRef core, SessionFactory factory, int32 maxSessionCount)
	: _type(type), _netAddress(address), _iocpCore(core), _sessionFactory(factory), _maxSessionCount(maxSessionCount)
{

}

Service::~Service()
{
}

void Service::CloseService()
{
	// TODO
}

SessionRef Service::CreateSession()
{
	SessionRef session = _sessionFactory();

	if (_iocpCore->Register(session) == false)
		return nullptr;

	return session;
}

// 세션에 ID를 할당해 그걸로 관리할 수는 있지만,
// 일단은 세션의 주소(포인터)를 갖고 있는 식으로 구현
void Service::AddSession(SessionRef session)
{
	WRITE_LOCK;
	_sessionCount++;
	_sessions.insert(session);
}

void Service::ReleaseSession(SessionRef session)
{
	WRITE_LOCK;
	ASSERT_CRASH(_sessions.erase(session) != 0);
	_sessionCount--;
}

생성자는 받은 인자들을 멤버변수에 세팅한다.

소멸자와 CloseService()에서는 일단은 아직 아무 동작도 하지 않을 것이다.

 

CreateSession()은 세션을 만들고(소켓 생성), 그 핸들을 CP에 등록해 주는 역할을 한다.

 

Addsession()과 ReleaseSession()은 이름 그대로 세션을 추가하고 풀어주는 역할을 한다.

혹시 모를 메모리 오염 방지를 위해 WRITE LOCK도 걸어준다.

 

2-1. Client Service

아까 서비스 타입을 클라이언트와 서버 2가지로 나눴다.

먼저 클라이언트 클래스부터 만들어 보자.

 

// Service.h

class ClientService : public Service
{
public:
	ClientService(NetAddress targetAddress, IocpCoreRef core, SessionFactory factory, int32 maxSessionCount = 1);
	virtual ~ClientService() {}

	virtual bool	Start() override;
};

지금은 클라이언트 서비스에 대해 생각 할 단계는 아닌 듯 하니 최소한의 구색만 갖췄다.

 

// Service.cpp

ClientService::ClientService(NetAddress targetAddress, IocpCoreRef core, SessionFactory factory, int32 maxSessionCount)
	: Service(ServiceType::Client, targetAddress, core, factory, maxSessionCount)
{
}

bool ClientService::Start()
{
	// TODO
	return true;
}

정의도 심플하다.

대신 클라이언트기 때문에 주소를 받는 부분의 네이밍은 targetAddress가 되었다.

 

 

2-2. Server Service

이제 서버 클래스를 만들자.

 

class ServerService : public Service
{
public:
	ServerService(NetAddress targetAddress, IocpCoreRef core, SessionFactory factory, int32 maxSessionCount = 1);
	virtual ~ServerService() {}

	virtual bool	Start() override;
	virtual void	CloseService() override;

private:
	// 서버 역할을 할 것이기 때문에 Listener가 필요
	ListenerRef		_listener = nullptr;
};

클라이언트 클래스에 비해 몇 가지가 추가되었다.

선언한 함수도 그 내용물을 어느 정도 채울 것이다.

서버 서비스기 때문에 리스너 오브젝트의 레퍼런스도 받는다.

 

ServerService::ServerService(NetAddress address, IocpCoreRef core, SessionFactory factory, int32 maxSessionCount)
	: Service(ServiceType::Server, address, core, factory, maxSessionCount)
{
}

bool ServerService::Start()
{
	if (CanStart() == false)
		return false;

	_listener = MakeShared<Listener>();
	if (_listener == nullptr)
		return false;

	// enable_shared_from_this가 ServerSerivce가 아니라 Service에 걸려있기 때문에
	// 포인터 캐스팅으로 넘겨줘야 함
	ServerServiceRef service = static_pointer_cast<ServerService>(shared_from_this());
	if (_listener->StartAccept(service) == false)
		return false;

	return true;
}

void ServerService::CloseService()
{
	// TODO

	Service::CloseService();
}

생성자는 크게 다를 것이 없다.

클라이언트가 아니라 서버기 때문에 자신의 주소를 받을 것이므로 targetAddress가 아니라 그냥 address가 됐다.

 

CanStart()로 functionFactory가 nullptr이 아닌지 체크하고,

Listener를 만들어 그 레퍼런스를 멤버변수에 넘겨준다.

 

ServerService의 정보를 활용해 리스너가 작동해야 하므로 ServerService 자신의 레퍼런스를 넘겨받아야 하는데,

당연히 여기서도 그냥 쌩 포인터로 넘겨주면 안 된다.

그래서 바로 shared_from_this()로 넘겨주려니 여기서도 문제가 생긴다.

그냥 저렇게 넘겨주면 Service를 넘겨주는 꼴이 된다.

enable_shared_from_this 클래스를 상속받은 것은 ServerService가 아니라 Service이기 때문이다.

따라서 포인터 캐스팅으로 ServerService로 캐스팅한 후 넘겨주어야 한다

그리고 AcceptEx()를 걸어주며 결과를 리턴한다.

 

CloseService()는 부모 클래스에서도 제대로 정의하지 않았으므로 이대로 둔다.

 

3. Listener 클래스 수정

기존의 StartAccept()는 NetAddress를 받아서 동작했지만,

이번에 그런 분산된 기능들을 Service에 다 모았으므로, 이에 맞게 수정해 주어야 한다.

전역으로 선언한 IocpCore 변수도 사용하지 않을 것이기에 제거한다.

 

// Listener.cpp

bool Listener::StartAccept(ServerServiceRef service)
{
	_service = service;
	if (_service == nullptr)
		return false;

	_socket = SocketUtils::CreateSocket();
	if (_socket == INVALID_SOCKET)
		return false;

	// 전역 선언한 IocpCore를 대체
	// Listener가 IocpObject를 상속받기 때문에 shared_from_this() 사용 가능
	if (_service->GetIocpCore()->Register(shared_from_this()) == false)
		return false;

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

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

	// NetAddress도 Service에서 가져옴
	if (SocketUtils::Bind(_socket, _service->GetNetAddress()) == false)
		return false;
        
    	// ...
}

이렇게 필요한 모든 정보들을 Service라는 단일 클래스에서 가져올 수 있게 되었다.

지금은 IocpCore 클래스의 Register()에 변경사항이 없지만, 인자로 shared ptr을 받게 고칠 것이다.

미리 고쳐두었다.

 

RegisterAccept()도 아래와 같이 수정한다.

void Listener::RegisterAccept(AcceptEvent* acceptEvent)
{
	// CP에 걸어주는 것 까지 한방에 처리
	// SessionFactory만 갈아주면서 간편하게 원하는 대로 세션 생성 가능
	SessionRef session = _service->CreateSession(); // Register IOCP

	acceptEvent->Init();
	acceptEvent->session = session;
    
	// ...
}

세션도 서비스에서 세션을 생성해 넘겨줄 수 있다.

세션을 생성하면서 IOCP에 등록까지 이루어진다.

 

이 방법엔 확실한 장점이 있는데,

서비스를 상속받아 원하는 형태로 만들고 SessionFactory만 원하는 형태로 연결해 주면,

나중에 컨텐츠 단에서 어떤 형식으로 세션을 만들어 줄지 지정할 수 있다는 점이다.

컨텐츠 별로 작업해야 하는 번거로움이 어느 정도 해소된다 할 수 있다.

 

IocpCore 클래스의 Register()도 아래와 같이 인자를 고쳐준다.

bool IocpCore::Register(IocpObjectRef iocpObject)
{
	return ::CreateIoCompletionPort(iocpObject->GetHandle(), _iocpHandle, /*key*/0, 0);
}

IocpObject 클래스는 share ptr을 사용하기로 했으므로 전체 프로젝트에서 이걸 통일시켜 주어야 한다.

그냥 섞어 쓴다면 큰 문제가 생길 수 있다.

 

 

4. 서버 작성

이전엔 Listener를 직접 가져와 사용했지만, 이제 더 이상 그런 직접 사용은 하지 않을 것이다.

ServerService를 생성해 이의 Start()를 실행하게끔 하면 이전의 코드를 대체할 수 있다.

 

class GameSession : public Session
{

};

int main()
{
	ServerServiceRef service = MakeShared<ServerService>(
		NetAddress(L"127.0.0.1", 7777),
		MakeShared<IocpCore>(),
		MakeShared<GameSession>, // TODO : SessionManager 등을 활용할 수도 있음
		100);

	ASSERT_CRASH(service->Start());

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

기존의 MakeShared는 Variadic Template를 지원하지 않았는데, 아래와 같이 고쳤다.

 

// Memory.h

template<typename Type, typename... Args>
shared_ptr<Type> MakeShared(Args&&... args)
{
	return shared_ptr<Type>{ xnew<Type>(forward<Args>(args)...), xdelete<Type> };
}
// ObjectPool.h

template<typename... Args>
static shared_ptr<Type> MakeShared(Args&&... args)
{
	shared_ptr<Type> ptr = { Pop(forward<Args>(args)...), Push };
	return ptr;
}

 

세션을 만드는 것은 SessionManager 같은 것을 만들어서 거기서 관리하게끔 할 수도 있다.

 

5. 결과

연결이 성공적으로 이루어졌다.

 

참 공부하지 쉽지 않다.