Windows 환경에서 IOCP(I/O Completion Port)를 사용해 서버를 만들면, 대규모 동시 연결을 효율적으로 처리할 수 있습니다. 아래 예제는 파일이 아닌 소켓(네트워크)에 IOCP를 적용한 코드 구조입니다. 각 단계별로 “어떻게 IOCP와 스레드 풀이 연동해 클라이언트 요청을 처리”하는지 살펴보겠습니다.
1. 핵심 자료구조와 전역 변수
typedef struct _USERSESSION
{
SOCKET hSocket;
char buffer[8192]; //8KB 버퍼
} USERSESSION;
// 스레드 개수(워크 스레드 수)
#define MAX_THREAD_CNT 4
CRITICAL_SECTION g_cs; // 스레드 동기화
std::list<SOCKET> g_listClient; // 연결된 클라이언트 소켓 리스트
SOCKET g_hSocket; // 서버 리슨 소켓
HANDLE g_hIocp; // IOCP 핸들
- USERSESSION:
- 각 클라이언트(소켓) 별로 세션 객체를 만든다.
- 소켓 핸들과 임시 입출력 버퍼(8KB)를 포함.
- g_hIocp:
- CreateIoCompletionPort로 생성되는 IOCP 객체.
- IOCP 큐를 사용해 소켓 I/O 완료를 통지.
- 스레드 4개:
- 주로 GetQueuedCompletionStatus를 통해 I/O 완료 패킷을 끄집어낸 뒤 처리.
2. IOCP와 작업자(Worker) 스레드 생성
// 1) IOCP 객체 생성
g_hIocp = ::CreateIoCompletionPort(
INVALID_HANDLE_VALUE, // 아직 연결된 핸들 없음
NULL,
0,
0 // 스레드 수 OS가 결정(또는 지정 가능)
);
if (g_hIocp == NULL) {
puts("ERORR: IOCP 생성 실패");
return 0;
}
// 2) 작업자 스레드 N개 생성
HANDLE hThread;
DWORD dwThreadID;
for (int i = 0; i < MAX_THREAD_CNT; ++i)
{
hThread = ::CreateThread(
NULL, // 보안 속성
0, // 기본 스택 크기
ThreadComplete, // 워커 스레드 함수
(LPVOID)NULL,
0,
&dwThreadID
);
::CloseHandle(hThread);
}
- CreateIoCompletionPort(1단계):
- IOCP를 생성해 “I/O 완료를 모아두는 큐”를 마련.
- 워커 스레드(2단계):
- 4개 스레드가 ThreadComplete 함수를 실행.
- 이 스레드들은 GetQueuedCompletionStatus를 호출해 I/O 완료 통지를 대기.
3. 서버 소켓 생성과 Accept 스레드
g_hSocket = ::WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP,
NULL, 0, WSA_FLAG_OVERLAPPED);
// bind(), listen() ...
// ...
// accept전용 스레드 생성
hThread = ::CreateThread(NULL, 0, ThreadAcceptLoop,
(LPVOID)NULL, 0, &dwThreadID);
::CloseHandle(hThread);
- WSASocket:
- Overlapped 플래그(WSA_FLAG_OVERLAPPED)로 소켓 생성.
- ThreadAcceptLoop:
- Accept를 반복 수행하며 새 클라이언트가 접속할 때마다 IOCP에 등록하고, 첫 번째 WSARecv를 비동기로 걸어둠.
4. ThreadAcceptLoop: 새 클라이언트 연결 처리
DWORD WINAPI ThreadAcceptLoop(LPVOID pParam)
{
int nAddrSize = sizeof(SOCKADDR);
SOCKADDR ClientAddr;
SOCKET hClient;
while ((hClient = ::accept(g_hSocket, &ClientAddr, &nAddrSize)) != INVALID_SOCKET)
{
puts("새 클라이언트 연결");
// 1) 세션 객체 생성
USERSESSION *pNewUser = new USERSESSION;
memset(pNewUser, 0, sizeof(USERSESSION));
pNewUser->hSocket = hClient;
// 2) 소켓을 IOCP에 연결(Attach)
CreateIoCompletionPort(
(HANDLE)hClient,
g_hIocp,
(ULONG_PTR)pNewUser, // KEY: USERSESSION 포인터
0
);
// 3) 첫 번째 비동기 recv를 걸어둠
WSAOVERLAPPED *pWol = new WSAOVERLAPPED;
memset(pWol, 0, sizeof(WSAOVERLAPPED));
WSABUF wsaBuf = { sizeof(pNewUser->buffer), pNewUser->buffer };
DWORD dwReceiveSize = 0, dwFlag = 0;
int nRecvResult = ::WSARecv(hClient, &wsaBuf, 1,
&dwReceiveSize,
&dwFlag,
pWol, // Overlapped
NULL
);
if (::WSAGetLastError() != WSA_IO_PENDING)
puts("ERROR: WSARecv() != WSA_IO_PENDING");
}
return 0;
}
- Accept 반복:
- 클라이언트 소켓(hClient) 생성.
- USERSESSION 생성:
- 소켓 + 8KB 버퍼를 갖는 세션 객체.
- IOCP에 소켓 등록:
- CreateIoCompletionPort((HANDLE)hClient, g_hIocp, (ULONG_PTR)pNewUser, 0)
- 이로써 “이 소켓의 I/O가 끝나면 IOCP 큐에 (pNewUser, Overlapped, …)” 알림이 들어감.
- WSARecv(비동기) 호출:
- 이로써 OS가 이 소켓에 데이터 들어오면 Overlapped 구조체에 기록, 완료 시점에 IOCP 알림.
5. ThreadComplete: IOCP에서 I/O 완료 처리
DWORD WINAPI ThreadComplete(LPVOID pParam)
{
DWORD dwTransferredSize = 0;
USERSESSION *pSession = NULL;
WSAOVERLAPPED *pWol = NULL;
BOOL bResult;
puts("[IOCP 작업자 스레드 시작]");
while (1)
{
// 1) 큐에서 I/O 완료 패킷을 꺼내옴
bResult = GetQueuedCompletionStatus(
g_hIocp,
&dwTransferredSize,
(PULONG_PTR)&pSession,
(LPOVERLAPPED*)&pWol,
INFINITE
);
if (!bResult)
{
// TODO: 에러 처리
continue;
}
// 2) dwTransferredSize == 0이면 연결 종료
if (dwTransferredSize == 0)
{
CloseClient(pSession->hSocket);
delete pWol;
delete pSession;
puts("\tGQCS: 클라이언트 연결 종료");
}
else
{
// 3) 수신된 데이터 처리
SendMessageAll(pSession->buffer, dwTransferredSize);
memset(pSession->buffer, 0, sizeof(pSession->buffer));
// 4) 다시 WSARecv()로 비동기 수신 준비
WSABUF wsaBuf = { sizeof(pSession->buffer), pSession->buffer };
DWORD dwFlag = 0;
int nRes = WSARecv(
pSession->hSocket,
&wsaBuf,
1,
&dwTransferredSize,
&dwFlag,
pWol,
NULL
);
if (::WSAGetLastError() != WSA_IO_PENDING)
puts("\tGQCS: ERROR: WSARecv()");
}
}
return 0;
}
- GetQueuedCompletionStatus:
- IOCP 큐에서 I/O 완료 패킷을 기다림.
- 반환 시, dwTransferredSize, pSession, pWol 등을 얻음.
- 데이터 크기 0:
- 클라이언트 소켓이 연결을 끊었음 → 세션 객체 해제, Overlapped 구조체 해제.
- 그 외(데이터 수신):
- pSession->buffer에 데이터가 들어있으니 Broadcast나 Echo 등.
- 버퍼 초기화 후, 다시 WSARecv를 걸어 추가 메시지를 비동기로 기다림.
6. IOCP의 주요 포인트 정리
- IOCP 핸들 생성: CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
- N개의 작업자 스레드가 IOCP 큐에서 GetQueuedCompletionStatus로 대기.
- 소켓(핸들)을 IOCP에 “등록”: CreateIoCompletionPort((HANDLE)hClient, g_hIocp, (ULONG_PTR)pSession, 0);
- 세션 포인터(pSession)를 Key로 등록하면, I/O가 끝날 때 “어떤 세션인지” 알 수 있음.
- 비동기 I/O 호출: WSARecv, WSASend, WriteFile, ReadFile 등 Overlapped로 실행.
- 실제 완료가 되면 OS가 IOCP 큐에 패킷을 push.
- Work Thread: GetQueuedCompletionStatus → I/O 완료 처리 반복.
7. 결론: “IOCP 예제 한눈에 보기”
- Accept 스레드: 새 클라이언트가 접속하면 accept 후 IOCP에 소켓을 등록(CreateIoCompletionPort).
- WSARecv로 첫 recv 걸어두면, 데이터가 올 때마다 I/O 완료 패킷이 IOCP 큐에 쌓임.
- 워커 스레드(4개): GetQueuedCompletionStatus로 큐에서 꺼내고, 수신 데이터 처리 → 다시 WSARecv.
- 연결이 끊기면 dwTransferredSize == 0, 세션/Overlapped 해제.
이러한 구조로 작성하면, 동시에 수많은 클라이언트가 연결되어도 적은 스레드로 모든 I/O를 처리 가능하고, OS가 “I/O가 끝난 소켓에만” CPU 시간을 배분해주므로 컨텍스트 스위칭 오버헤드를 최소화할 수 있습니다. “확장성 높은 고성능 서버”를 짜려면, 이와 같은 IOCP 예제를 기반으로 더 복잡한 비즈니스 로직만 얹어주면 되는 것이죠.
'프로그래밍 > 소켓 프로그래밍 입문' 카테고리의 다른 글
IOCP 모델과 가상 메모리 관리: 운영체제가 어떻게 고성능 네트워크 I/O를 가능하게 할까? (2) | 2025.02.03 |
---|---|
왜 IOCP는 빠를까? 운영체제가 직접 관리하는 비동기 I/O의 힘 (0) | 2025.02.03 |
APC(Asynchronous Procedure Call) 알아보기 (0) | 2025.01.25 |
Windows 비동기 I/O의 두가지 방식, 이벤트와 콜백 (0) | 2025.01.25 |
콜백 기반 비동기 파일 I/O: 내부 동작 상세 (0) | 2025.01.24 |