네트워크 라이브러리에서 사용할 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)
네트워크 라이브러리 만들기(Session #1) (0) | 2021.12.06 |
---|---|
네트워크 라이브러리 만들기(Server Service) (0) | 2021.12.01 |
네트워크 라이브러리 제작(Socket Utils) (0) | 2021.11.27 |
Completion Port 모델 (0) | 2021.11.19 |
Overlapped모델 (콜백기반) (0) | 2021.11.18 |
댓글 영역