Overlapped I/O

1. Overlapped I/O

1.1. Overlapped I/O란

Overlapped I/O는 Windows 운영체제에서 제공하는 비동기 입출력 메커니즘이다.

하나의 스레드가 여러 I/O 작업을 동시에 진행할 수 있게 해준다.

 

이름에서 알 수 있듯이, Overlapped는 여러 I/O 작업이 시간적으로 겹쳐서 실행됨을 의미한다.

Overlapped I/O의 핵심은 I/O 요청을 시작한 후 결과를 기다리지 않고 다른 작업을 수행할 수 있다는 점이다.

 

1.2. Windows에서의 Overlapped I/O

Overlapped I/O는 Windows 운영체제의 핵심 기능 중 하나로, Win32 API에서 제공된다.

 

Windows의 I/O 모델은 크게 동기식, 비동기식(Overlapped), 그리고 비동기 완료 통지(I/O Completion Ports)로 나뉜다.

Overlapped I/O는 Windows NT 커널의 일부로 구현되어 있으며, 다양한 Win32 함수에서 OVERLAPPED 구조체를 통해 접근할 수 있다.

 

 

2. Overlapped I/O의 작동 원리

2.1. OVERLAPPED 구조체

OVERLAPPED 구조체는 Overlapped I/O 작업의 핵심이다.

typedef struct _OVERLAPPED {
    ULONG_PTR Internal;
    ULONG_PTR InternalHigh;
    union {
        struct {
            DWORD Offset;
            DWORD OffsetHigh;
        } DUMMYSTRUCTNAME;
        PVOID Pointer;
    } DUMMYUNIONNAME;
    HANDLE hEvent;
} OVERLAPPED, *LPOVERLAPPED;

 

Internal과 InternalHigh는 시스템에서 내부적으로 사용하며 개발자가 수정해서는 안 된다.

Offset과 OffsetHigh는 파일 내 위치를 지정하는 64비트 값으로, 소켓 I/O에서는 사용하지 않는다.

hEvent는 I/O 완료 시 신호를 받는 이벤트 핸들이다.

 

2.2. 내부 동작 방식

소켓 통신에서 Overlapped I/O가 내부적으로 동작하는 방식은 다음과 같다.

  1. 애플리케이션이 WSARecv 또는 WSASend와 같은 비동기 함수를 호출한다.
  2. Windows 운영체제는 I/O 요청을 즉시 처리하지 않고 백그라운드로 큐에 넣는다.
  3. API 함수는 즉시 반환되며, 보통 'ERROR_IO_PENDING' 오류 코드를 반환한다.
  4. 네트워크 드라이버가 실제 I/O 작업을 처리한다.
  5. 작업이 완료되면 OVERLAPPED 구조체의 hEvent가 신호 상태로 설정된다.

 

3. Overlapped I/O 구현

1) 소켓 생성 및 초기화

// Winsock 초기화
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
    // 오류 처리
    return;
}

// Overlapped I/O를 위한 소켓 생성
SOCKET s = WSASocket(
    AF_INET,
    SOCK_STREAM,
    IPPROTO_TCP,
    NULL,
    0,
    WSA_FLAG_OVERLAPPED
);

if (s == INVALID_SOCKET) {
    // 오류 처리
    WSACleanup();
    return;
}

 

WSA_FLAG_OVERLAPPED 플래그는 소켓이 비동기 작업을 지원하도록 설정한다.

 

2) WSAOVERLAPPED 구조체 준비

각 비동기 작업에 대해 WSAOVERLAPPED 구조체를 초기화해야 한다.

// OVERLAPPED 구조체 초기화
WSAOVERLAPPED ol = {0};
ol.hEvent = WSACreateEvent();

if (ol.hEvent == WSA_INVALID_EVENT) {
    // 오류 처리
    closesocket(s);
    WSACleanup();
    return;
}

 

이벤트 핸들(hEvent)은 I/O 작업이 완료될 때 신호를 받는 데 사용된다.

 

3) 비동기 수신(WSARecv) 구현

WSARecv 함수는 소켓에서 데이터를 비동기적으로 수신한다.

// 수신 버퍼 준비
char buffer[1024] = {0};
WSABUF wsaBuf;
wsaBuf.buf = buffer;
wsaBuf.len = sizeof(buffer);

// 수신에 필요한 변수들
DWORD bytesReceived = 0;
DWORD flags = 0;

// 비동기 수신 시작
int result = WSARecv(
    s,
    &wsaBuf,
    1,           // 버퍼 배열의 개수
    &bytesReceived,
    &flags,
    &ol,
    NULL         // 완료 루틴 콜백 (여기서는 사용하지 않음)
);

if (result == SOCKET_ERROR) {
    int error = WSAGetLastError();
    if (error != WSA_IO_PENDING) {
        // 진짜 오류 발생
        WSACloseEvent(ol.hEvent);
        closesocket(s);
        WSACleanup();
        return;
    }
    // WSA_IO_PENDING은 정상적인 비동기 동작 시작을 의미함
}

 

WSA_IO_PENDING 오류 코드는 실제로 오류가 아니며, I/O 작업이 백그라운드에서 진행 중임을 나타낸다.

 

4) 비동기 송신(WSASend) 구현

WSASend 함수는 소켓을 통해 데이터를 비동기적으로 전송한다.

// 송신 버퍼 준비
char sendBuffer[1024] = "Hello, Overlapped I/O!";
WSABUF wsaSendBuf;
wsaSendBuf.buf = sendBuffer;
wsaSendBuf.len = strlen(sendBuffer);

// 송신에 필요한 변수
DWORD bytesSent = 0;

// 비동기 송신 시작
int sendResult = WSASend(
    s,
    &wsaSendBuf,
    1,          // 버퍼 배열의 개수
    &bytesSent,
    0,          // 플래그 없음
    &ol,
    NULL        // 완료 루틴 콜백 (여기서는 사용하지 않음)
);

if (sendResult == SOCKET_ERROR) {
    int error = WSAGetLastError();
    if (error != WSA_IO_PENDING) {
        // 진짜 오류 발생
        WSACloseEvent(ol.hEvent);
        closesocket(s);
        WSACleanup();
        return;
    }
    // WSA_IO_PENDING은 정상적인 비동기 동작 시작을 의미함
}

 

WSASend도 WSARecv와 마찬가지로 작업이 백그라운드에서 진행 중일 때 WSA_IO_PENDING을 반환한다.

 

 

4. 완료 통지 방법

작업 완료를 통지하는 방법은 크게 두가지이다.

4.1. 이벤트 기반 완료 통지

이벤트 기반 방식은 가장 간단한 완료 통지 메커니즘이다.

- OVERLAPPED 구조체의 hEvent 필드에 이벤트 핸들을 설정한다.
- I/O 작업이 완료되면 이 이벤트가 신호 상태(signaled)로 바뀐다.
- 이벤트가 신호 상태가 되면 WSAGetOverlappedResult 함수로 작업 결과를 확인한다.

// I/O 작업이 완료될 때까지 대기
DWORD waitResult = WSAWaitForMultipleEvents(
    1,          // 이벤트 개수
    &ol.hEvent, // 이벤트 배열
    FALSE,      // 모든 이벤트가 아닌 하나라도 신호 상태가 되면 반환
    INFINITE,   // 무한정 대기
    FALSE       // 경보 가능 대기 아님
);

if (waitResult == WSA_WAIT_FAILED) {
    // 대기 오류 처리
    WSACloseEvent(ol.hEvent);
    closesocket(s);
    WSACleanup();
    return;
}

// I/O 작업 결과 확인
DWORD bytesTransferred = 0;
DWORD flags = 0;
BOOL success = WSAGetOverlappedResult(
    s,
    &ol,
    &bytesTransferred,
    FALSE,      // 이미 대기했으므로 FALSE
    &flags
);

if (!success) {
    // 작업 실패 처리
    WSACloseEvent(ol.hEvent);
    closesocket(s);
    WSACleanup();
    return;
}

// bytesTransferred로 실제 전송된 바이트 수 확인
// buffer에는 수신한 데이터가 들어있음

 

이 방식은 하나의 스레드가 여러 I/O 작업을 시작하고 WSAWaitForMultipleEvents를 통해 완료를 대기할 수 있다.

 

4.2. 완료 루틴(Completion Routine) 기반 통지

완료 루틴은 I/O 작업이 완료되면 호출되는 콜백 함수이다.

- WSARecv나 WSASend 함수 호출 시 마지막 매개변수로 콜백 함수(완료 루틴)를 등록한다.
- I/O 작업이 완료되면 시스템이 자동으로 등록된 콜백 함수를 호출한다.
- 콜백 함수는 작업 결과(오류 코드, 전송된 바이트 수 등)를 매개변수로 받습니다.

// 완료 루틴 정의
void CALLBACK CompletionRoutine(
    DWORD dwError,
    DWORD cbTransferred,
    LPWSAOVERLAPPED lpOverlapped,
    DWORD dwFlags
) {
    // dwError - 오류 코드
    // cbTransferred - 전송된 바이트 수
    // lpOverlapped - OVERLAPPED 구조체 포인터

    // 여기서 완료된 I/O 처리
    if (dwError == 0 && cbTransferred > 0) {
        // 데이터 처리
        char* buffer = ((WSABUF*)(lpOverlapped->Internal))->buf;
        printf("Received data: %.*s\n", cbTransferred, buffer);
    }

    // 리소스 정리
    WSACloseEvent(lpOverlapped->hEvent);
    // 필요한 경우 메모리 해제 등의 작업 수행
}

// WSABUF 및 버퍼 설정
char buffer[1024] = {0};
WSABUF wsaBuf;
wsaBuf.buf = buffer;
wsaBuf.len = sizeof(buffer);

// OVERLAPPED 구조체에 WSABUF 저장 (사용자 정의 용도)
WSAOVERLAPPED ol = {0};
ol.hEvent = WSACreateEvent();
ol.Internal = (ULONG_PTR)&wsaBuf;  // 버퍼 참조를 위한 트릭

// 완료 루틴과 함께 비동기 수신 시작
DWORD flags = 0;
int result = WSARecv(
    s,
    &wsaBuf,
    1,
    NULL,            // bytesReceived는 콜백에서 제공되므로 NULL
    &flags,
    &ol,
    CompletionRoutine // 완료 루틴 함수 포인터
);

// 경보 가능 대기 상태로 전환
while (true) {
    SleepEx(INFINITE, TRUE);  // 알림 가능한 대기 상태
    // 완료 루틴이 실행되면 이 지점으로 돌아옴
    break;  // 또는 다른 로직 수행
}

 

 

5. 결론

1) 작업 요청 단계 (WSASend/WSARecv 호출)

- 비동기 I/O 작업을 요청

- 함수가 SOCKET_ERROR를 반환하고 WSAGetLastError()가 WSA_IO_PENDING을 반환하면, "작업이 백그라운드 에서 진행 중"이라는 의미(작업이 가능하여 시작되었음을 나타냄)


2) 완료 통지 단계 (이벤트 신호 또는 콜백 호출)

- 이벤트 기반: 작업이 완전히 끝나면 hEvent가 신호 상태로 전환

- 완료 루틴 기반: 작업이 완전히 끝나면 콜백 함수 호출

 

3) 결과 확인 단계 (WSAGetOverlappedResult 호출)

- 이벤트 기반 방식에서는 이벤트가 신호 상태가 된 후 WSAGetOverlappedResult를 호출하여 실제 작업 결과(전송된 바이트 수, 오류 여부 등)를 확인

- 완료 루틴 기반에서는 콜백 함수의 매개변수로 이 정보를 직접 전송

 

 

5.1. 적절한 사용 시나리오

Overlapped I/O를 효과적으로 사용할 수 있는 소켓 통신 시나리오

  1. 다중 클라이언트 처리가 필요한 네트워크 서버
  2. 대용량 데이터 전송이 필요한 애플리케이션
  3. 병렬 데이터 처리가 필요한 시스템
  4. 실시간 데이터 스트리밍 애플리케이션

'학습 > C++ 소켓 프로그래밍' 카테고리의 다른 글

I/O 모델 비교  (0) 2025.03.31
IOCP  (0) 2025.03.30
Non-Blocking 소켓  (0) 2024.11.09
소켓 옵션  (0) 2024.11.08
UDP 소켓 프로그래밍  (0) 2024.10.31