| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 |
Tags
- select 모델
- 그래프 탐색
- Delete
- 강한 연결 요소
- 벨만-포드
- PROJECT
- Binary Lifting
- Strongly Connected Component
- trie
- JavaScript
- HTTP
- reference counting
- ccw 알고리즘
- 게임 서버 아키텍처
- Lock-free Stack
- Overlapped Model
- 2-SAT
- 최소 공통 조상
- Github
- Prisma
- 자바스크립트
- 트라이
- 비트마스킹
- SCC
- 비트필드를 이용한 dp
- 이분 탐색
- map
- Spin Lock
- Behavior Design Pattern
- DP
Archives
- Today
- Total
dh_0e
[C++/Game Server] IocpCore 본문

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:
};
- IocpEvent는 OVERLAPPED를 상속받아 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에게 전달해야 함
- 그냥 accept()를 쓰는 것과 다르게 Client Socket을 미리 빈 객체로 만들어 AcceptEx에 전달
- Listener::Dispatch() - EventType이 Accept인지 확인 후 acceptEvent로 형변환 후 ProcessAccept 호출
- Listene::ProcessAccept() - 새로 연결된 Client에 대한 세부 세팅
- SocketUtils::SetUpdateAcceptSocket()로 Listen Socket의 속성을 Client Socket에 부여
- getpeername(): 내 소켓과 연결된 상대방(Peer)의 주소 정보를 가져오는 함수
- 이를 가져와서 NetAddress에 저장

'C++ > Game Server' 카테고리의 다른 글
| [C++/Game Server] Socket Utils (WinSock Wrapper) (0) | 2026.03.29 |
|---|---|
| [C++/Game Server] Completion Port 모델 (IOCP) (0) | 2026.03.27 |
| [C++/Game Server] Overlapped Model (Callback 함수 기반) (0) | 2026.03.25 |
| [C++/Game Server] Overlapped Model (Event 기반) (0) | 2026.03.24 |
| [C++/Game Server] WSAEventSelect Model (0) | 2026.03.23 |
