C++/Win32

[Win32] IOCP 서버

powergirl 2025. 12. 27. 18:59

IOCP 는 Windows 전용 고성능 비동기 I/O 모델로, 정식 명칭은 I/O Completion Port.

 

IOCP 객체 (Completion Port)
HANDLE hIOCP = CreateIoCompletionPort(...);

작업 완료 알림을 넣어두는 큐

완료된 recv/send 결과가 쌓임

 

 

소켓을 IOCP에 등록
CreateIoCompletionPort(
    (HANDLE)clientSocket,
    hIOCP,
    (ULONG_PTR)clientContext,
    0
);

 

 

recv 요청
WSARecv(sock, &buf, 1, NULL, &flags, &ov, NULL);

 

 

IOCP 서버의 스레드
while (true)
{
    GetQueuedCompletionStatus(
        hIOCP,
        &bytes,
        &key,
        &overlapped,
        INFINITE
    );

    // 여기로 "작업 끝났음"이 들어옴
}

 

 

 

 


IOCP 서버 최소 동작 코드

 

Clientcontext - 핵심 구조체
struct ClientContext
{
    SOCKET socket;
    OVERLAPPED overlapped;
    WSABUF wsaBuf;
    char buffer[1024];
};

socket : 클라이언트 소켓

OVERLAPPED : 비동기 작업 식별자

WSABUF : recv 대상 버퍼

buffer : 실제 데이터 공간

 

 

전연 IOCP 핸들
HANDLE g_hIOCP;

 

 

Worker Thread
DWORD WINAPI WorkerThread(LPVOID)
{
    while (true)
    {
        DWORD bytesTransferred;
        ULONG_PTR completionKey;
        OVERLAPPED* pOverlapped;

        BOOL ret = GetQueuedCompletionStatus(
            g_hIOCP,
            &bytesTransferred,
            &completionKey,
            &pOverlapped,
            INFINITE
        );

        ClientContext* ctx = (ClientContext*)completionKey;

        if (bytesTransferred == 0)
        {
            closesocket(ctx->socket);
            delete ctx;
            continue;
        }

        ctx->buffer[bytesTransferred] = '\0';
        printf("Recv: %s\n", ctx->buffer);

        // 다시 recv 요청
        ZeroMemory(&ctx->overlapped, sizeof(OVERLAPPED));

        DWORD flags = 0;
        WSARecv(
            ctx->socket,
            &ctx->wsaBuf,
            1,
            NULL,
            &flags,
            &ctx->overlapped,
            NULL
        );
    }
}

 

 

서버 초기화 & 메인
int main()
{
    WSADATA wsa;
    WSAStartup(MAKEWORD(2, 2), &wsa);

    SOCKET listenSock = socket(AF_INET, SOCK_STREAM, 0);

    SOCKADDR_IN addr{};
    addr.sin_family = AF_INET;
    addr.sin_port = htons(9000);
    addr.sin_addr.s_addr = INADDR_ANY;

    bind(listenSock, (SOCKADDR*)&addr, sizeof(addr));
    listen(listenSock, SOMAXCONN);

    // IOCP 생성
    g_hIOCP = CreateIoCompletionPort(
        INVALID_HANDLE_VALUE,
        NULL,
        0,
        0
    );

    // Worker Thread 1개 (최소)
    CreateThread(NULL, 0, WorkerThread, NULL, 0, NULL);

    printf("IOCP Server Start\n");

    while (true)
    {
        SOCKET clientSock = accept(listenSock, NULL, NULL);

        ClientContext* ctx = new ClientContext;
        ZeroMemory(ctx, sizeof(ClientContext));

        ctx->socket = clientSock;
        ctx->wsaBuf.buf = ctx->buffer;
        ctx->wsaBuf.len = sizeof(ctx->buffer);

        // 소켓을 IOCP에 연결
        CreateIoCompletionPort(
            (HANDLE)clientSock,
            g_hIOCP,
            (ULONG_PTR)ctx,
            0
        );

        // 최초 recv 요청
        DWORD flags = 0;
        WSARecv(
            clientSock,
            &ctx->wsaBuf,
            1,
            NULL,
            &flags,
            &ctx->overlapped,
            NULL
        );
    }

    return 0;
}

 

 

 


IOCP에서 길이 + 데이터 프레임 처리

 

전송 규칙
[4바이트 길이][데이터 본문]

 

 

ClientContext 확장
struct ClientContext
{
    SOCKET socket;
    OVERLAPPED overlapped;
    WSABUF wsaBuf;

    char buffer[4096];

    int recvBytes;      // 지금까지 받은 바이트
    int expectedBytes;  // 이번에 받아야 할 총 바이트
};

IOCP에서는 “상태”를 Context에 저장한다

 

 

최초 recv 요청 (길이 4바이트)
ctx->recvBytes = 0;
ctx->expectedBytes = sizeof(int);

ctx->wsaBuf.buf = ctx->buffer;
ctx->wsaBuf.len = ctx->expectedBytes;

WSARecv(
    ctx->socket,
    &ctx->wsaBuf,
    1,
    NULL,
    0,
    &ctx->overlapped,
    NULL
);

 

 

WorkerThread에서 프레임 처리
DWORD WINAPI WorkerThread(LPVOID)
{
    while (true)
    {
        DWORD bytesTransferred;
        ULONG_PTR key;
        OVERLAPPED* ov;

        GetQueuedCompletionStatus(
            g_hIOCP,
            &bytesTransferred,
            &key,
            &ov,
            INFINITE
        );

        ClientContext* ctx = (ClientContext*)key;

        if (bytesTransferred == 0)
        {
            closesocket(ctx->socket);
            delete ctx;
            continue;
        }

        ctx->recvBytes += bytesTransferred;

        if (ctx->recvBytes < ctx->expectedBytes)
        {
            // 아직 부족 → 계속 recv
            ctx->wsaBuf.buf = ctx->buffer + ctx->recvBytes;
            ctx->wsaBuf.len = ctx->expectedBytes - ctx->recvBytes;

            WSARecv(
                ctx->socket,
                &ctx->wsaBuf,
                1,
                NULL,
                0,
                &ctx->overlapped,
                NULL
            );
            continue;
        }

        // 여기 오면 "이번 프레임 완성"

 

길이 프레임인지 / 데이터 프레임인지 구분
        if (ctx->expectedBytes == sizeof(int))
        {
            int bodySize;
            memcpy(&bodySize, ctx->buffer, sizeof(int));
            bodySize = ntohl(bodySize);

            ctx->recvBytes = 0;
            ctx->expectedBytes = bodySize;

            ctx->wsaBuf.buf = ctx->buffer;
            ctx->wsaBuf.len = bodySize;

            WSARecv(
                ctx->socket,
                &ctx->wsaBuf,
                1,
                NULL,
                0,
                &ctx->overlapped,
                NULL
            );
        }
        else
        {
            // 데이터 프레임 완성
            ctx->buffer[ctx->expectedBytes] = '\0';
            printf("Recv Msg: %s\n", ctx->buffer);

            // 다시 길이부터 받기
            ctx->recvBytes = 0;
            ctx->expectedBytes = sizeof(int);

            ctx->wsaBuf.buf = ctx->buffer;
            ctx->wsaBuf.len = sizeof(int);

            WSARecv(
                ctx->socket,
                &ctx->wsaBuf,
                1,
                NULL,
                0,
                &ctx->overlapped,
                NULL
            );
        }
    }
}