728x90
반응형
IOCP (I / O Completion Port)가 왜 최종일까?
- 이전 모델들의 단점을 모두 해결했기 때문에
- 이벤트 모델의 단점 : 스레드와 클라이언트가 1:1로 묶여 클라이언트 수 만큼 스레드가 필요했다
- 콜백 모델의 단점 : I/O를 시작한 스레드에서만 콜백이 실행되는 '스레드 종속성' 문제가 있었다
- IOCP는 적은 수의 스레드로 많은 클라이언트의 I/O를 효율적으로 처리하며, I/O 완료 작업을 특정 스레드에 종속시키지 않고 노는 스레드에게 공평하게 분배한다.
IOCP의 3대 핵삼 구성요소
세가지 핵심API함수로 동작한다
- CreateIoCompletionPort
- IOCP의 핵심인 Completion Port 핸들을 생성하는 함수
- 모든 I/O 완료 통지는 이 포트를 통해 이루어진다.
- 또한 클라이언트 소켓이 생길 때마다 이 함수를 다시 호출하여 소켓을 Completion Port에 등록하는 역할을 함
- GetQueuedCompletionStatus
- "일감 내놔" 라고 하는 함수.
- Worker스레드가 이 함수를 호출하면, Completion 포트에 I/O 완료 작업(완료 패킷)이 등록될 때까지 대기
- 작업이 생기면 완료된 작업의 정보와 함께 리턴되어 스레드가 일을 시작하게 한다.
- Worker스레드
- GetQueuedCompletionStatus를 무한 루프로 호출하며 대기하다가, 완료 패킷을 받으면 해당 IO를 처리하는 역할을 하는 일꾼 쓰레드들 (보통 CPU코어 수의 2배 정도를 생성)
IOCP 서버의 전체적인 흐름
- main 스레드 (사장)
- IOCP Completion Port(완료 포트) 생성 (CreateIoCompletionPort)
- Worker 스레드(직원)들을 여러개 생성해서 GetQueuedCompletionStatus를 호출하며 대기시킨다
- 자신은 accept에만 집중하며 새로운 클라이언트 연결을 받는다
- 새 클라이언트가 접속하면 통신용 소켓을 생성하고 IOCP에 등록한다 ( CreateIoCompletionPort )
- 등록된 클라이언트를 위해 첫번째 비동기 I/O 작업(WSARecv)을 시작하고 다시 accept 하러 간다
- Worker 스레드 (직원)
- GetQueuedCompletionStatus를 통해 I/O 완료 통지를 받는다
- 완료된 작업의 종류(Read,Write...등) 와 대상 클라이언트(Session) 정보를 확인
- 데이터를 처리하고, 연쇄적으로 다음 WSARecv를 호출해 데이터 수신을 이어나간다
- 클라이언트 접속이 끊긴 것을 감지하면, 해당 클라이언트의 Session과 socket등 리소스를 안전하게 정리한다
실습 코드 흐름 분석
핵심 자료구조 정의
enum class IO_TYPE
{
READ,
WRITE,
ACCEPT,
// ... 등등
};
// 어떤 종류의 I/O가 완료되었는지 알려주기 위한 확장 구조체
struct OverlappedEx
{
WSAOVERLAPPED overlapped = {};
IO_TYPE ioType;
// 필요에 따라 다른 데이터 추가 가능
};
// 클라이언트 정보를 담는 구조체
struct Session
{
SOCKET socket = INVALID_SOCKET;
char recvBuffer[BUFSIZE] = {};
// ...
};
- Session 이외에 OverlappedEx 구조체를 만들어 I/O 작업의 종류를 구분해준다
- GetQueuedCompetionStatus는 작업이 완료되었다는 사실만 알려줄 뿐 Recv의 결과인지 Send의 결과인지는 알려주지 않기 떄문이다.
Main쓰레드
int main()
{
// 1. 윈속 초기화, 리슨 소켓 준비 (이전과 동일)
// ...
// 2. IOCP 완료 포트 생성
// 마지막 인자 0은 '최적의 스레드 수'를 OS에 맡긴다는 의미 (보통 CPU 코어 수)
HANDLE iocpHandle = ::CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
// 3. Worker 스레드(직원) 생성 및 일 시키기
const int32 threadCount = 5;
vector<thread> workerThreads;
for (int32 i = 0; i < threadCount; i++)
{
// GThreadManager->Launch([=]() { WorkerThreadMain(iocpHandle); });
// 내 프레임워크에선 커스텀해놔서 사용 가능한데 일단은 표준으로 사용
workerThreads.emplace_back(WorkerThreadMain, iocpHandle);
}
// 4. Main 스레드는 Accept만 담당 (사장님은 영업만!)
while (true)
{
// ... accept 로직 (논블로킹 처리 포함) ...
// 성공적으로 clientSocket을 얻었다고 가정
// 5. 새 클라이언트를 위한 Session 생성
Session* session = new Session{ clientSocket };
// 6. 소켓을 IOCP에 등록
// 세 번째 인자(Key)가 매우 중요! 이 소켓에서 발생한 I/O의 Key로 session 포인터를 넘겨줌
::CreateIoCompletionPort((HANDLE)clientSocket, iocpHandle, (ULONG_PTR)session, 0);
// 7. 첫 번째 비동기 읽기(Recv) 작업 시작
OverlappedEx* overlappedEx = new OverlappedEx();
overlappedEx->ioType = IO_TYPE::READ;
WSABUF wsaBuf;
wsaBuf.buf = session->recvBuffer;
wsaBuf.len = BUFSIZE;
DWORD recvLen = 0;
DWORD flags = 0;
::WSARecv(session->socket, &wsaBuf, 1, NULL, &flags, &overlappedEx->overlapped, NULL);
}
// ... 스레드 종료 대기 및 정리 ...
}
WorkerThread
void WorkerThreadMain(HANDLE iocpHandle)
{
while (true)
{
DWORD bytesTransferred = 0;
Session* session = nullptr; // I/O가 발생한 클라이언트 세션
OverlappedEx* overlappedEx = nullptr; // 완료된 I/O 작업 정보
// 1. "일감 내놔!" -> 완료된 작업이 생길 때까지 여기서 대기
BOOL ret = ::GetQueuedCompletionStatus(iocpHandle, &bytesTransferred,
(PULONG_PTR)&session, (LPOVERLAPPED*)&overlappedEx, INFINITE);
// 2. 리소스 정리 (가장 중요!)
// 클라이언트 접속 종료 또는 치명적 오류 발생
if (ret == FALSE || bytesTransferred == 0)
{
cout << "Client Disconnected." << endl;
closesocket(session->socket);
delete session;
delete overlappedEx; // 시작됐지만 완료되지 못한 I/O의 Overlapped 객체도 해제
continue;
}
// 3. 완료된 작업 종류 판단 및 처리
switch (overlappedEx->ioType)
{
case IO_TYPE::READ:
{
cout << "Recv Data: " << bytesTransferred << " bytes" << endl;
// [TODO] 받은 데이터(session->recvBuffer)로 실제 게임 로직 처리
// 4. 처리했으니 다음 Recv 작업을 연쇄적으로 호출
// - 이전 OverlappedEx는 해제하고, 다음 작업을 위해 새로 할당
delete overlappedEx;
OverlappedEx* newOverlappedEx = new OverlappedEx();
newOverlappedEx->ioType = IO_TYPE::READ;
WSABUF wsaBuf;
wsaBuf.buf = session->recvBuffer;
wsaBuf.len = BUFSIZE;
DWORD flags = 0;
::WSARecv(session->socket, &wsaBuf, 1, NULL, &flags, &newOverlappedEx->overlapped, NULL);
}
break;
// 추후 case IO_TYPE::WRITE: 등으로 확장 가능
}
}
}
IOCP가 좋은 이유
- 최고의 확장성 : 클라이언트가 수백, 수천이 되어도 Worker 스레드 수는 늘릴 필요가 없다, 서버의 부하가 스레드 컨텍스트 스위칭이 아닌 실제I/O 처리에 집중된다
- 효율적인 스레드 관리 : I/O를 시작한 스레드와 처리하는 스레드가 달라도 된다. OS가 알아서 노는 쓰레드를 꺠워서 일을 시키므로, 스레드들이 낭비되지 않는다
- 자원 관리의 명확성 : GetQueuedCompetionStatus를 통해 I/O 완료 시점을 명확히 알 수 있으므로, 메모리 해제나 객체 소멸같은 리소스 관리 로직을 일관되게 처리할 수 있다.
※ IOCP는 복잡해보이지만, "사장님(Main)은 영업만, 직원(Worker)은 처리만" 이라는 역할 분담의 원리를 이해하면 그 구조가 한눈에 들어오게 된다. 윈도우 환경에서 고성능 서버를 만들려면 반드시 필요하니 숙지하자
728x90
반응형
'서버 > 네트워크 프로그래밍' 카테고리의 다른 글
입출력 모델 Overlapped - IO (콜백기반) (2) | 2025.06.16 |
---|---|
입출력 모델 - Overlapped I/O (이벤트기반) (1) | 2025.06.16 |
입출력 모델 - WSAEventSelect (1) | 2025.06.12 |
입출력모델 - Select 모델 (0) | 2025.06.12 |
논 블로킹 소켓 (0) | 2025.06.12 |