dh_0e

[C++/Game Server] IocpCore 본문

C++/Game Server

[C++/Game Server] IocpCore

dh_0e 2026. 3. 31. 17:32

IocpCore Diagram

 

IocpCore.h

/*-----------------
	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;
  • IocpObject: Completion Port에 등록될 객체를 관리하는 클래스
    • Completion Port에는 소켓뿐만 아니라 다양한 범위로 활용할 수 있음
    • 이를 상속하는 Listener, Session 클래스가 있음
      • 작업 완료 보고를 받을 수 있음
      • Listener: Server의 소켓인 Listener Socket 생성CP 등록 등의 관리
      • Session: Client의 소켓과 NetAddress(SOCKADDR_IN 구조체)를 관리

 

IocpCore.cpp

// TEMP(임시)
IocpCore GIocpCore;

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

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, /*key*/ reinterpret_cast<ULONG_PTR>(iocpObject), 0);
}

bool IocpCore::Dispatch(uint32 timeoutMs) {
	DWORD numOfBytes = 0;
	IocpObject* iocpObject = nullptr;
	IocpEvent* iocpEvent = nullptr;

	// 원래는 refrence Counting을 사용하는데, 여기서는 그냥 포인터를 사용
	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 true;
}
  • IocpCore::IocpCore() - IocpCore 생성자
    • CP(Completion Port) 생성 및 에러 확인
  • IocpCore::~IocpCore() - IocpCore 소멸자
    • iocpHandle 종료
  • IocpCore::Register() - iocpObject->getHandle()을 CP에 등록
    • getHandle()로 iocpObject에서 Client Socket or Listener Socket을 가져와서 CP에 등록
  • IocpCore::Dispatch() - CP에 완료된 작업이 있나 탐색 
    • GQCS IocpCore::_iocpHandle(CP)에 완료된 작업이 있나 탐색
    • 스레드로 호출하여 CP에 완료된 I/O 작업을 찾은 뒤 iocpObject(Session or Listener)->Dispatch() 수행
    • 원래는 스마트 포인터(reference counting)를 사용하여 CP에 들어간 작업의 객체(세션)가 I/O 작업 완료 전에 삭제되지 않게 관리해야 함
      • 이번 코드는 일단 포인터로 진행

 


 

IocpEvent.h

  • 어떤 이벤트(I/O 작업)인지(OVERLAPPED) 파악
enum class EventType : uint8 {
	Connect,
	Accept,
	// PreRecv, // 0 byte recv
	Recv,
	Send,
};

class Session;

/*------------------
	IocpEvent
-------------------*/

// 상속을 받으면 무조건 Offset 0번에 OVERLAPPED 구조체가 있게 됨
// OVERLAPPED 처럼 사용 가능함
class IocpEvent : public OVERLAPPED {
public:
	IocpEvent(EventType type);
	// virtual 쓰면 안 됨
	// virtual 때문에 가상 함수 테이블이 Offset 0번에 생기는데
	// 그러면 맨 처음에 있던 OVERLAPPED 구조체가 사라지게 됨

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

protected:
	EventType _type;
};

/*------------------
	ConnectEvent
-------------------*/

class ConnectEvent : public IocpEvent {
public:
	ConnectEvent() : IocpEvent(EventType::Connect) {}

private:
};

/*------------------
	AcceptEvent
-------------------*/

class AcceptEvent : public IocpEvent {
public:
	AcceptEvent() : IocpEvent(EventType::Accept) {}

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

private:
	Session* _session = nullptr;  // client session
};

/*------------------
	RecvEvent
-------------------*/

class RecvEvent : public IocpEvent {
public:
	RecvEvent() : IocpEvent(EventType::Recv) {}

private:
};

/*------------------
	SendEvent
-------------------*/

class SendEvent : public IocpEvent {
public:
	SendEvent() : IocpEvent(EventType::Send) {}

private:
};
  • IocpEventOVERLAPPED를 상속받아 offset 0번에 위치시켜 OVERLAPPED 역할을 할 수 있음
    • OVERLAPPED: 비동기 작업이 완료되었을 때, 어떤  I/O 요청이었는지 파악하는 데 쓰임
  • 소멸자에 virtual을 사용(부모 객체의 메모리도 깔끔하게 지우기 위해)하면 안 됨
    • virtual를 사용하면 가상 함수 테이블(vtable 포인터)이 offset 0번을 자동으로 차지해서 IocpEvent 객체를 OVERLAPPED로 쓸 수 없게 됨
  • AcceptEvent는 Session을 저장하여 어떤 클라이언트에 accept 했는지 저장

 

IocpEvent.cpp

#include "IocpEvent.h"

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

void IocpEvent::Init() {
	OVERLAPPED::hEvent = 0;
	OVERLAPPED::Internal = 0;
	OVERLAPPED::InternalHigh = 0;
	OVERLAPPED::Offset = 0;
	OVERLAPPED::OffsetHigh = 0;
}
  • 생성자에서 type을 입력하고, Init을 호출해 OVERLAPPED를 초기화해 줌

 

 


 

Session.h

  • 서버에 접속한 Client 한 명을 추상화한 객체
    • Client의 소켓 및 주소(NetAddress)를 저장 및 관리
#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 getNetAdress() { 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> _connected = false;
};
  • IocpObject를 상속: IOCP로부터 작업 완료 보고를 받을 수 있는 객체

 

Session.cpp

#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
	// Send, Recv 처리
}
  • TODO: 이후에 Listener.cpp의 Dispatch에서 Process를 호출하는 것처럼 WSASend, WSARecv 작업이 완료되면 Session::Dispatch()에서 완료된 I/O 작업 종류에 맞게 ProcessRecv(), ProcessSend()를 호출할 것임

 


 

Listener.h

  • 서버가 처음 켜졌을 때 가장 먼저 일을 시작하며, 새로운 손님(클라이언트)이 들어오는지 감시하고 안내하는 역할을 담당
#include "IocpCore.h"
#include "NetAddress.h"

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

class AcceptEvent;

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

public:
	/* 외부에서 사용 */
	bool StartAccept(NetAddress netAddr);
	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;
};
  • IocpObject를 상속: IOCP로부터 작업 완료 보고를 받을 수 있는 객체
  • _socket: 손님의 노크를 기다리는 리스닝 소켓(Server)
  • _acceptEvents: AcceptEx라는 낚싯대를 여러 개 던져놓기 위해 관리하는 이벤트 객체들의 바구니

 

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 netAddr) {
	_socket = SocketUtils::CreateSocket();
	if (_socket == INVALID_SOCKET)
		return false;

	// 이때 Listener 객체를 전달해서 key로 IocpObject(Listener) 전달
	// 후에 IocpCore::Dispatch에서 이 key를 통해 IocpObject(Listener)를 찾아서 처리
	if (GIocpCore.Register(this) == false)
		return false;

	if (SocketUtils::SetReuseAddress(_socket, true) == false)
		return false;

	if (SocketUtils::SetLinger(_socket, 1, 0) == false)
		return false;

	if (SocketUtils::Bind(_socket, netAddr) == 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 true;
}

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

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

void Listener::RegisterAccept(AcceptEvent* acceptEvent) {
    Session* session = xnew<Session>();
	acceptEvent->Init();
	acceptEvent->SetSession(session);
    
	DWORD bytesRecived = 0;
	// Client  socket은 미리 만들어서 CP에 올라간 상태
	// AcceptEx 이후 어떤 클라가 접속 시도 하면 IocpCore::Dispatch에서 반응 -> IocpObject::Dispatch(Listener::Dispatch)
	if (false == SocketUtils::AcceptEx(_socket, session->GetSocket(), session->_recvBuffer, 0, sizeof(SOCKADDR_IN) + 16,
    sizeof(SOCKADDR_IN) + 16, OUT & bytesRecived,
    static_cast<LPOVERLAPPED>(acceptEvent))) {
        const int32 errorCode = ::WSAGetLastError();
		if (errorCode != WSA_IO_PENDING) {
            // RegisterAccept는 끊기면 안 됨 (낚시대가 미끄러진 상황. 낚시대를 다시 던져야 함)
			RegisterAccept(acceptEvent);
			return;
		}
	}
}

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

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

    ProcessAccept(acceptEvent);
}

void Listener::ProcessAccept(AcceptEvent* acceptEvent) {
	Session* session = acceptEvent->GetSession();

	// Client socket은 원래 listenSocket의 속성을 그대로 받아야 하는데 AcceptEx에선 이 작업을 해주지 않음
	// SetUpdateAcceptSocket 함수를 통해 이 작업을 해줌
	if (false == SocketUtils::SetUpdateAcceptSocket(session->GetSocket(), _socket)) {
		RegisterAccept(acceptEvent);
		return;
	}

	SOCKADDR_IN sockAddress;
	int32 sizeOfSockAddress = sizeof(sockAddress);
	if (SOCKET_ERROR ==
		::getpeername(session->GetSocket(), reinterpret_cast<SOCKADDR*>(&sockAddress), &sizeOfSockAddress)) {
		RegisterAccept(acceptEvent);
		return;
	}

	session->SetNetAddress(NetAddress(sockAddress));

	cout << "Client Connected!!" << endl;

	// TODO

	RegisterAccept(acceptEvent);
}
  • Listener::StartAccept() - Socket Option 설정 및 CP 생성
    • GIocpCore.Register(this)CP를 생성하며 Listener 객체를 전달
    • 소켓 생성 -> IOCP 등록 -> Bind(주소 할당) -> Listen(대기 상태) -> RegisterAccept() 호출
  • Listener::CloseSocket() - 소켓 종료
  • Listener::RegisterAccept() - 비동기 접속 수락 함수인 AcceptEx를 호출
    • 그냥 accept()를 쓰는 것과 다르게 Client Socket을 미리 빈 객체로 만들어 AcceptEx에 전달
      • Listen Socket의 속성을 그대로 받지 못하기 때문에 나중에 SocketUtils::SetUpdateAcceptSocket()을 통해 listenSocket의 속성을 clientSocket에게 전달해야 함
  • Listener::Dispatch() - EventType이 Accept인지 확인 후 acceptEvent로 형변환 후 ProcessAccept 호출
  • Listene::ProcessAccept() - 새로 연결된 Client에 대한 세부 세팅
    • SocketUtils::SetUpdateAcceptSocket()로 Listen Socket의 속성을 Client Socket에 부여
    • getpeername(): 내 소켓과 연결된 상대방(Peer)의 주소 정보를 가져오는 함수
      • 이를 가져와서 NetAddress에 저장

IocpCore Diagram