상세 컨텐츠

본문 제목

네트워크 라이브러리 만들기(Server Service)

똑똑한 개발/C++ 게임개발

by 성댕쓰 2021. 12. 1. 22:29

본문

지난 시간에 작성한 코드에는 문제가 하나 있다. 

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)

관련글 더보기

댓글 영역