지난 시간에 작성한 코드에는 문제가 하나 있다.
GetQueuedCompletionStatus 키값으로 복원한 포인터가 살아있는 상태인지 체크를 하지 않고 있다. 엉뚱한 주소에 접근하여 오류를 만들 여지가 있다. 이번시간에는 이 오류를 해결해보자.
키값으로 사용하는 IocpObject를 기존에 구현한 refcount를 이용하여 관리하는 방법이 있다. 이 방법의 단점은 외부에서 shared_ptr로 IocpObject를 만들었을 경우 refcount가 각각 관리되는 점이다.
다른 방법은 iocpEvent에 iocpObject shared_ptr을 멤버변수로 관리하는 방법이다. 이 방법을 코드로 구현해보자.
shared_ptr을 좀 더 편하게 사용하기 위해 using문을 추가한다.
Types.h
...
// 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>;
...
IocpEvent가 IocpObject 포인터를 가지고 있게 만든다.
IocpEvent.h
/*-----------------
IocpEvent
-----------------*/
class IocpEvent : public OVERLAPPED
{
public:
IocpEvent(EventType type);
void Init();
public:
EventType eventType;
IocpObjectRef owner;
};
...
/*-----------------
AcceptEvent
-----------------*/
class AcceptEvent : public IocpEvent
{
public:
AcceptEvent() : IocpEvent(EventType::Accept) {}
public:
SessionRef session = nullptr;
};
...
IocpObject주소를 키로 넘겨주는 코드를 수정한다.
IocpCore.cpp
...
bool IocpCore::Register(IocpObjectRef iocpObject)
{
return ::CreateIoCompletionPort(iocpObject->GetHandle(), _iocpHandle, /*key*/0, 0);
}
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->Dispatch(iocpEvent, numOfBytes);
}
else
{
int32 errCode = ::WSAGetLastError();
switch (errCode)
{
case WAIT_TIMEOUT:
return false;
default:
// TODO : 로그 찍기
IocpObjectRef iocpObject = iocpEvent->owner;
iocpObject->Dispatch(iocpEvent, numOfBytes);
break;
}
}
return false;
}
...
위 수정에 따라 Listener 코드도 손 봐준다
Listener.cpp
#include "pch.h"
#include "Listener.h"
#include "SocketUtils.h"
#include "IocpEvent.h"
#include "Session.h"
#include "Service.h"
/*----------------
Listener
----------------*/
Listener::~Listener()
{
SocketUtils::Close(_socket);
for (AcceptEvent* acceptEvent : _acceptEvents)
{
xdelete(acceptEvent);
}
}
bool Listener::StartAccept(ServerServiceRef service)
{
_service = service;
if (_service == nullptr)
return false;
_socket = SocketUtils::CreateSocket();
if (_socket == INVALID_SOCKET)
return false;
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;
if (SocketUtils::Bind(_socket, service->GetNetAddress()) == false)
return false;
if (SocketUtils::Listen(_socket) == false)
return false;
const int32 acceptCount = service->GetMaxSessionCount();
for (int32 i = 0; i < acceptCount; i++)
{
AcceptEvent* acceptEvent = xnew<AcceptEvent>();
acceptEvent->owner = shared_from_this(); // shared_ptr<IocpObject>(this) 주의
_acceptEvents.push_back(acceptEvent);
RegisterAccept(acceptEvent);
}
return true;
}
void Listener::CloseSocket()
{
SocketUtils::Close(_socket);
}
HANDLE Listener::GetHandle()
{
return reinterpret_cast<HANDLE>(_socket);
}
void Listener::Dispatch(IocpEvent* iocpEvent, int32 numOfBytes)
{
ASSERT_CRASH(iocpEvent->eventType == EventType::Accept);
AcceptEvent* acceptEvent = static_cast<AcceptEvent*>(iocpEvent);
ProcessAccept(acceptEvent);
}
void Listener::RegisterAccept(AcceptEvent* acceptEvent)
{
SessionRef session = _service->CreateSession(); // Register IOCP
acceptEvent->Init();
acceptEvent->session = 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)
{
SessionRef session = acceptEvent->session;
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;
RegisterAccept(acceptEvent);
}
AcceptEvent owner로 Listener를 넣어줄 때 새로운 shared_ptr을 만들지 않아야 한다. 그러면 같은 주소를 두 개의 서로다른 shared_ptr이 관리하는데, Refcount가 0이 되어 메모리 해제할 수 있기 때문이다.
위의 오류를 막기 위해 IocpObject를 수정하고 shared_from_this를 사용하도록 한다. weak_ptr을 이용하여 새로운 포인터를 만들지 않고 refcount를 증가시킨다.
IocpCore.h
/*----------------
IocpObject
----------------*/
class IocpObject : public enable_shared_from_this<IocpObject>/*내부적 weak_ptr 사용*/
{
public:
virtual HANDLE GetHandle() abstract;
virtual void Dispatch(class IocpEvent* iocpEvent, int32 numOfBytes = 0) abstract;
};
...
현재 IocpCore가 global로 사용되고 있는데 경우에 따라 IocpCore를 여러 개 만들어 사용할 수 있다. 분산서버를 만드는 경우가 그렇다. 게임서버가 다른 서버와 통신하는 클라이언트가 될 수 있다. 이를 가능하게 하도록 클래스를 추가한다.
Service.h
#pragma once
#include "NetAddress.h"
#include "IocpCore.h"
#include "Listener.h"
#include <functional>
enum class ServiceType : uint8
{
Server, Client
};
/*-------------------
Service
-------------------*/
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();
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; }
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;
};
/*-------------------
ClientService
-------------------*/
class ClientService : public Service
{
public:
ClientService(NetAddress targetAddress, IocpCoreRef core, SessionFactory factory, int32 maxSessionCount = 1);
virtual ~ClientService() {}
virtual bool Start() override;
};
/*-------------------
ServerService
-------------------*/
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:
ListenerRef _listener = nullptr;
};
Service.cpp
#include "pch.h"
#include "Service.h"
#include "Session.h"
#include "Listener.h"
/*-------------------
Service
-------------------*/
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;
}
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--;
}
/*-------------------
ClientService
-------------------*/
ClientService::ClientService(NetAddress targetAddress, IocpCoreRef core, SessionFactory factory, int32 maxSessionCount)
: Service(ServiceType::Client, targetAddress, core, factory, maxSessionCount)
{
}
bool ClientService::Start()
{
// TODO
return true;
}
/*-------------------
ServerService
-------------------*/
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;
ServerServiceRef service = static_pointer_cast<ServerService>(shared_from_this());
if (_listener->StartAccept(service) == false)
return false;
return true;
}
void ServerService::CloseService()
{
// TODO
Service::CloseService();
}
사용법은 아래처럼.
GameServer.cpp
#include "pch.h"
#include "ThreadManager.h"
#include "SocketUtils.h"
#include "Listener.h"
#include "Service.h"
#include "Session.h"
int main()
{
ServerServiceRef service = MakeShared<ServerService>(
NetAddress(L"127.0.0.1", 7777),
MakeShared<IocpCore>(),
MakeShared<Session>,
100);
ASSERT_CRASH(service->Start());
for (int32 i = 0; i < 5; i++)
{
GThreadManager->Launch([=]()
{
while (true)
{
service->GetIocpCore()->Dispatch();
}
});
}
GThreadManager->Join();
}
1. IocpObject를 key값으로 사용하지 않는 대신에, 이용하는 방법은?
- Overlapped 구조체가 IocpObject를 멤버변수를 갖고 있게 만들어서 사용.
2. enable_shared_from_this<T> 가 하는 역할은?
- 멤버변수에 weak_ptr<T> 추가.
- shared_from_this로 weak_ptr<T>::lock 호출하여 새로운 메모리 할당 없이 reference count 만 증가 시킨다.
- shared_from_this를 상속하면 항상 shared_ptr로 사용하여야 한다. 스택에 할당 안됨
3. service 기능중 session 과 관련된 기능은?
- session을 외부에서 설정하여 원하는 session을 만드는 기능.
출처 : [C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버 - 인프런 | 강의 (inflearn.com)
네트워크 라이브러리 만들기(Session #2) (0) | 2021.12.08 |
---|---|
네트워크 라이브러리 만들기(Session #1) (0) | 2021.12.06 |
네트워크 라이브러리 만들기(Iocp Core) (0) | 2021.11.29 |
네트워크 라이브러리 제작(Socket Utils) (0) | 2021.11.27 |
Completion Port 모델 (0) | 2021.11.19 |
댓글 영역