상세 컨텐츠

본문 제목

네트워크 라이브러리 만들기(Iocp Core)

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

by 성댕쓰 2021. 11. 29. 17:12

본문

네트워크 라이브러리에서 사용할 Iocp Core 부분을 만들어보자.

IocpCore.h

#pragma once

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

/*----------------
 IocpCore
----------------*/

class IocpCore
{
public:
    IocpCore();
    ~IocpCore();

    HANDLE GetHandle() { return _iocpHandle; }

    bool Register(class IocpObject* iocpObject);
    bool Dispatch(uint32 timeoutMs = INFINITE);

private:
    HANDLE _iocpHandle;
};

// TEMP
extern IocpCore GIocpCore;

Register함수를 socket과 iocp핸들 연결이외의 용도로도 사용하기 위해 IocpObject class를 정의했다.

추후에 IocpCore를 관리하는 별도의 class가 있을 예정이다. 지금은 임시로 전역객체를 만들어 사용한다.

 

IocpCore.cpp

#include "pch.h"
#include "IocpCore.h"
#include "IocpEvent.h"

// TEMP
IocpCore GIocpCore;

IocpCore::IocpCore()
{
    _iocpHandle = ::CreateIoCompletionPort(INVALID_HANDLE_VALUE, 0, 0, 0);
    ASSERT_CRASH(_iocpHandle != INVALID_HANDLE_VALUE);
}

IocpCore::~IocpCore()
{
    ::CloseHandle(_iocpHandle);
}

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

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
    {
        int32 errCode = ::WSAGetLastError();
        switch (errCode)
        {
        case WAIT_TIMEOUT:
            return false;
        default:
            // TODO : 로그 찍기
            iocpObject->Dispatch(iocpEvent, numOfBytes);
            break;
        }
    }

    return false;
}

 

IocpCore는 iocp handle을 가지고 있고 그와 관련 함수 iocp socket연결, iocp 작업처리등을 하는 클래스다.

위에서도 언급했듯이 Register 함수에서 연결 뿐 아니라 일처리도 진행할 수 있도록 IocpObject를 인자로 받는다.

IocpObject는 IocpEvent라는 클래스 인자로 사용하는 함수를 가지고 있다. IocpEvent 종류에 따라서 다른 일을 처리하게 만들 것이다.

 

IocpEvent.h

#pragma once

class Session;

enum class EventType : uint8
{
    Connect,
    Accept,
    //PreRecv,
    Recv,
    Send
};

/*-----------------
  IocpEvent
-----------------*/
class IocpEvent : public OVERLAPPED
{
public:
    IocpEvent(EventType type);

    void Init();
    EventType GetType() { return _type; }

protected:
    EventType _type;
};

/*-----------------
  ConnectEvent
-----------------*/
class ConnectEvent : public IocpEvent
{
public:
    ConnectEvent() : IocpEvent(EventType::Connect) {}
};


/*-----------------
  AcceptEvent
-----------------*/
class AcceptEvent : public IocpEvent
{
public:
    AcceptEvent() : IocpEvent(EventType::Accept) {}

    void SetSession(Session* session) { _session = session; }
    Session* GetSession() { return _session; }

private:
    Session* _session = nullptr;
};

/*-----------------
  RecvEvent
-----------------*/
class RecvEvent : public IocpEvent
{
public:
    RecvEvent() : IocpEvent(EventType::Recv) {}

private:

};

/*-----------------
  SendEvent
-----------------*/
class SendEvent : public IocpEvent
{
public:
    SendEvent() : IocpEvent(EventType::Send) {}

private:

};

IocpEvent에 가상함수를 만들지 않도록 유의한다. 자료구조의 0번 필드에 Overlapped 0번 필드가 아닌, 가상함수가 들어가는 것을 막기 위함이다.

 

IocpEvent.cpp

#include "pch.h"
#include "IocpEvent.h"

IocpEvent::IocpEvent(EventType type) : _type(type)
{
}

void IocpEvent::Init()
{
    OVERLAPPED::hEvent = 0;
    OVERLAPPED::Internal = 0;
    OVERLAPPED::InternalHigh = 0;
    OVERLAPPED::Offset = 0;
    OVERLAPPED::OffsetHigh = 0;
}

 

IocpCore 함수를 콜하는 class를 만들어보자. 접속 요청을 기다리다가 소켓을 만들고 iocp 연결을 도와주는 문지기다. 이름은 Listener이다.

Listener.h

#pragma once
#include "IocpCore.h"
#include "NetAddress.h"

class AcceptEvent;

/*----------------
  Listener
----------------*/

class Listener : public IocpObject
{
public:
    Listener() = default;
    ~Listener();

public:
    /* 외부에서 사용 */
    bool StartAccept(NetAddress netAddress);
    void CloseSocket();
public:
    /* 인터페이스 구현 */
    virtual HANDLE GetHandle() override;
    virtual void Dispatch(class IocpEvent* iocpEvent, int32 numOfBytes = 0) override;
private:
    /* 수신 관련 */
    void RegisterAccept(AcceptEvent* acceptEvent);
    void ProcessAccept(AcceptEvent* acceptEvent);
protected:
    SOCKET _socket = INVALID_SOCKET;
    Vector<AcceptEvent*> _acceptEvents;
};

Listener.cpp

#include "pch.h"
#include "Listener.h"
#include "SocketUtils.h"
#include "IocpEvent.h"
#include "Session.h"

/*----------------
  Listener
----------------*/

Listener::~Listener()
{
    SocketUtils::Close(_socket);

    for (AcceptEvent* acceptEvent : _acceptEvents)
    {
        xdelete(acceptEvent);
    }
}

bool Listener::StartAccept(NetAddress netAddress)
{
    _socket = SocketUtils::CreateSocket();
    if (_socket == INVALID_SOCKET)
        return false;

    if (GIocpCore.Register(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, netAddress) == false)
        return false;

    if (SocketUtils::Listen(_socket) == false)
        return false;

    const int32 acceptCount = 1;
    for (int32 i = 0; i < acceptCount; i++)
    {
        AcceptEvent* acceptEvent = xnew<AcceptEvent>();
        _acceptEvents.push_back(acceptEvent);
        RegisterAccept(acceptEvent);
    }

    return false;
}

void Listener::CloseSocket()
{
    SocketUtils::Close(_socket);
}

HANDLE Listener::GetHandle()
{
    return reinterpret_cast<HANDLE>(_socket);
}

void Listener::Dispatch(IocpEvent* iocpEvent, int32 numOfBytes)
{
    ASSERT_CRASH(iocpEvent->GetType() == EventType::Accept);

    AcceptEvent* acceptEvent = static_cast<AcceptEvent*>(iocpEvent);
    ProcessAccept(acceptEvent);
}

void Listener::RegisterAccept(AcceptEvent* acceptEvent)
{
    Session* session = xnew<Session>();

    acceptEvent->Init();
    acceptEvent->SetSession(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)
{
    Session* session = acceptEvent->GetSession();

    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);
}

 

클라이언트 소켓정보를 저장하는 클래스 Session을 만든다.

Session.h

#pragma once
#include "IocpCore.h"
#include "IocpEvent.h"
#include "NetAddress.h"

/*----------------
  Session
----------------*/

class Session : public IocpObject
{
public:
    Session();
    virtual ~Session();

public:
    /* 정보 관련 */
    void SetNetAddress(NetAddress address) { _netAddress = address; }
    NetAddress GetAddress() { return _netAddress; }
    SOCKET GetSocket() { return _socket; }

public:
    /* 인터페이스 구현 */
    virtual HANDLE GetHandle() override;
    virtual void Dispatch(class IocpEvent* iocpEvent, int32 numOfBytes = 0) override;

public:
    // TEMP
    char _recvBuffer[1000];

private:
    SOCKET _socket = INVALID_SOCKET;
    NetAddress _netAddress = {};
    Atomic<bool> _connecte4d = false;
};

Session.cpp

#include "pch.h"
#include "Session.h"
#include "SocketUtils.h"

/*----------------
  Session
----------------*/

Session::Session()
{
    _socket = SocketUtils::CreateSocket();
}

Session::~Session()
{
    SocketUtils::Close(_socket);
}

HANDLE Session::GetHandle()
{
    return reinterpret_cast<HANDLE>(_socket);
}

void Session::Dispatch(IocpEvent* iocpEvent, int32 numOfBytes)
{
    // TODO
}

 

사용은 아래와 같이 하면 된다.

GameServer.cpp

int main()
{
    Listener listener;
    listener.StartAccept(NetAddress(L"127.0.0.1", 7777));

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

    GThreadManager->Join();
}

 iocp 커넥션을 만들고 accept 를 등록하고 이벤트에 따라 process하는 과정을 잘 이해해보도록 하자.

 

1. CreateCompletionPort에 넘길 수 있는 커스텀 가능한 파라메터는 몇 개이고 각각 무엇?

- 2개, 하나는 key값, 다른 하나는 overlapped 구조체 포인터.

2. IocpObject 역할은?

- GetQueuedCompletionStatus를 네트워크 처리 용도로만 쓰는 것이 아닌, 다양한 용도로 사용하기 위한 클래스.

3. IocpEvent에 가상함수를 만들면 안되는 이유는?

- 가상함수 테이블이 멤버변수 첫 번째에 위치하게 되어, Overlapped 포인터로 사용하지 못하기 때문.

4. Listener가 하는 역할은?

- socket 설정들 모두 설정 후, socket listen 등록하고 accept 등록하여 연결 요청 처리하는 역할.

5. RegisterAccept 역할은?

- 클라이언트 소켓 만들고 AcceptEx 함수 포인터 호출함.

- AcceptEx 파라메터 중 sizeof(SOCKADDR_IN)+16 은 그냥 이렇게 쓰는 것으로 넘어가기.

6. AcceptEx 걸어준 후, 클라 접속하면 결국 Listener에서 어떤 함수가 불리는지?

- Dispatch

출처 : [C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버 - 인프런 | 강의 (inflearn.com)

관련글 더보기

댓글 영역