오늘도 최대한 메모해 보자.
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. 결과
연결이 성공적으로 이루어졌다.
참 공부하지 쉽지 않다.
'Study > C++ & C#' 카테고리의 다른 글
[C++] RecvBuffer (0) | 2023.06.28 |
---|---|
[C++] Session (0) | 2023.06.24 |
[C++] IOCP Core (0) | 2023.06.21 |
[C++] Socket util 클래스 작성 (0) | 2023.06.20 |
[C++] Completion Port Model (0) | 2023.06.17 |