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가 내부적으로 동작하는 방식은 다음과 같다.
- 애플리케이션이 WSARecv 또는 WSASend와 같은 비동기 함수를 호출한다.
- Windows 운영체제는 I/O 요청을 즉시 처리하지 않고 백그라운드로 큐에 넣는다.
- API 함수는 즉시 반환되며, 보통 'ERROR_IO_PENDING' 오류 코드를 반환한다.
- 네트워크 드라이버가 실제 I/O 작업을 처리한다.
- 작업이 완료되면 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를 효과적으로 사용할 수 있는 소켓 통신 시나리오
- 다중 클라이언트 처리가 필요한 네트워크 서버
- 대용량 데이터 전송이 필요한 애플리케이션
- 병렬 데이터 처리가 필요한 시스템
- 실시간 데이터 스트리밍 애플리케이션
'학습 > 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 |