I'm FanJae.

Unity 게임 개발 캠프 개인 프로젝트 7일차. Overlapped I/O - 요청과 완료가 분리되는 구조 (개인 공부 내용) 본문

Projects/MyToyMapleServer

Unity 게임 개발 캠프 개인 프로젝트 7일차. Overlapped I/O - 요청과 완료가 분리되는 구조 (개인 공부 내용)

FanJae 2026. 9. 21. 23:59

1. 시작에 앞서

- 이전에는 Blocking 서버의 한계와 Non-Blocking Socket에 대해 정리했다.

- Blocking 방식에서는 I/O가 완료될 때까지 Thread가 대기할 수 있었고, Non-Blocking 방식에서는 I/O를 즉시 완료할 수 없다면 WSAEWOULDBLOCK을 반환받아 Thread가 계속 실행될 수 있었다.

- 하지만 Non-Blocking 방식만 사용하면 I/O가 언제 가능한지를 애플리케이션에서 다시 확인해야 하는 문제가 남는다.

recv()
   ↓
데이터 없음
   ↓
다시 확인
   ↓
recv()

- 즉 Thread가 Blocking되지는 않지만, 작업을 언제 다시 시도해야 하는지 직접 관리해야 한다.

- 그렇다면 다음과 같은 방식으로 처리하는 것은 가능하지 않을까?

I/O 요청
   ↓
운영체제가 작업 처리
   ↓
Thread는 다른 작업 수행
   ↓
I/O 완료
   ↓
완료된 작업 처리

- Windows에서는 이와 같이 I/O를 요청하는 시점과 완료되는 시점을 분리해서 처리할 수 있도록 Overlapped I/O를 제공한다.

- 이번에는 Overlapped I/O가 어떤 방식으로 동작하는지 정리해보려고 한다.


2. Overlapped I/O란?

- Overlapped I/O는 I/O 작업을 요청하는 시점과 해당 작업이 완료되는 시점을 분리할 수 있도록 하는 Windows의 비동기 I/O 방식이다.

- 일반적인 Blocking recv()를 생각해보면 다음과 같다.

recv(socket, buffer, size, 0);

- 데이터를 바로 받을 수 없다면 해당 함수에서 기다릴 수 있다.

recv()
   ↓
데이터 대기
   ↓
데이터 수신
   ↓
return

- 반면 Overlapped I/O에서는 I/O 작업을 먼저 요청한 뒤, 실제 작업이 완료되는 동안 Thread가 다른 작업을 수행할 수 있다.

I/O 요청
   ↓
작업 진행
   ↓
Thread는 다른 작업 수행
   ↓
I/O 완료
   ↓
완료 처리

- 즉 중요한 점은 I/O를 요청한 Thread가 해당 작업의 완료까지 계속 기다릴 필요가 없다는 것이다.


3. Overlapped Socket 생성

- Winsock에서 Overlapped I/O를 사용하기 위해서는 이를 지원하도록 Socket을 생성해야 한다.

- WSASocket()을 사용할 때 WSA_FLAG_OVERLAPPED 플래그를 지정할 수 있다.

SOCKET socket = WSASocket(AF_INET,SOCK_STREAM,IPPROTO_TCP,nullptr,0,WSA_FLAG_OVERLAPPED);

- 이렇게 생성한 Socket에서는 WSARecv(), WSASend() 등을 이용해 Overlapped I/O 작업을 요청할 수 있다.

Overlapped Socket
      ↓
  WSARecv()
  WSASend()
      ↓
비동기 I/O 요청

- 여기서 주의할 점은 Overlapped Socket과 Non-Blocking Socket은 같은 개념이 아니라는 것이다.

- WSA_FLAG_OVERLAPPED는 해당 Socket에서 Overlapped I/O를 사용할 수 있도록 하는 것이며, Socket 자체의 Blocking / Non-Blocking Mode를 변경하는 것은 아니다.


4. recv()와 WSARecv()

- 일반적으로 데이터를 수신할 때 다음과 같이 recv()를 사용할 수 있다.

char buffer[1024];

int result = recv(
    socket,
    buffer,
    sizeof(buffer),
    0
);

- Overlapped I/O에서는 WSARecv()를 사용할 수 있다.

char buffer[1024];

WSABUF wsaBuffer;
wsaBuffer.buf = buffer;
wsaBuffer.len = sizeof(buffer);

DWORD recvBytes = 0;
DWORD flags = 0;

WSAOVERLAPPED overlapped = {};

int result = WSARecv(
    socket,
    &wsaBuffer,
    1,
    &recvBytes,
    &flags,
    &overlapped,
    nullptr
);

- 두 방식의 가장 큰 차이 중 하나는 WSARecv()에는 WSAOVERLAPPED가 함께 전달된다는 점이다.

recv()

Socket
   ↓
Buffer
WSARecv()

Socket
   ↓
Buffer
   ↓
WSAOVERLAPPED

- 이 WSAOVERLAPPED는 I/O 요청 시점과 이후 완료 시점을 연결하기 위해 사용되는 정보라고 볼 수 있다.


5. I/O가 즉시 완료되는 경우

- Overlapped I/O라고 해서 모든 작업이 반드시 나중에 완료되는 것은 아니다.

- WSARecv()를 호출한 시점에 이미 받을 데이터가 존재한다면 I/O가 즉시 완료될 수도 있다.

WSARecv()
   ↓
이미 수신할 데이터 존재
   ↓
즉시 완료
   ↓
return 0

- 이 경우 WSARecv()는 0을 반환한다.

int result = WSARecv(...);

if (result == 0)
{
    // I/O 요청이 즉시 완료됨
}

- 따라서 Overlapped I/O를 사용한다고 해서 항상 Pending 상태를 거치는 것은 아니다.


6. WSA_IO_PENDING

- 더 중요한 경우는 I/O를 즉시 완료할 수 없는 경우이다.

- 아직 수신할 데이터가 없다면 WSARecv() 호출에서 다음과 같은 결과가 발생할 수 있다.

int result = WSARecv(...);

if (result == SOCKET_ERROR)
{
    int error = WSAGetLastError();

    if (error == WSA_IO_PENDING)
    {
        // I/O 작업이 정상적으로 시작되었지만
        // 아직 완료되지 않은 상태
    }
}

- 처음 보면 SOCKET_ERROR가 반환되기 때문에 작업이 실패했다고 생각하기 쉽다.

- 하지만 WSAGetLastError()의 결과가 WSA_IO_PENDING이라면 실제 I/O 요청은 정상적으로 등록된 상태이다.

WSARecv()
   ↓
SOCKET_ERROR
   ↓
WSAGetLastError()
   ↓
WSA_IO_PENDING
   ↓
실패가 아님
   ↓
I/O 작업 진행 중

- 즉 WSA_IO_PENDING은 다음과 같이 이해할 수 있다.

I/O 요청은 성공
       +
아직 완료되지 않음

7. Request → Pending → Completion

- Overlapped I/O를 이해할 때 가장 중요한 흐름은 다음과 같다.

Request
   ↓
Pending
   ↓
Completion

- 먼저 WSARecv() 등을 이용하여 I/O 작업을 요청한다.

WSARecv()
   ↓
Recv 작업 요청

- 현재 바로 처리할 수 없다면 작업은 Pending 상태가 된다.

WSARecv()
   ↓
WSA_IO_PENDING
   ↓
Kernel에서 I/O 진행

- 이때 호출한 Thread는 WSARecv()가 끝날 때까지 기다리는 것이 아니라 다른 작업을 수행할 수 있다.

Thread
   │
   ├─ WSARecv 요청
   │
   ├─ 다른 작업
   │
   ├─ 다른 작업
   │
   └─ ...

- 이후 클라이언트에서 데이터가 도착하면 운영체제에서 해당 I/O를 완료한다.

Client
   ↓
데이터 전송
   ↓
Windows Kernel
   ↓
Recv 완료

- 서버에서는 이후 이 완료된 I/O 결과를 받아 후속 처리를 수행하게 된다.

- 따라서 Overlapped I/O에서는 I/O 함수의 호출이 끝났다는 것과 실제 I/O 작업이 완료되었다는 것을 구분해야 한다.


8. WSAOVERLAPPED가 필요한 이유

- 서버에서는 하나의 I/O만 처리하는 것이 아니라 여러 작업을 동시에 요청할 수 있다.

Recv A 요청

Recv B 요청

Send C 요청

Recv D 요청

- 각각의 작업은 서로 다른 시점에 완료될 수 있다.

Recv A ───────────── 완료

Recv B ───── 완료

Send C ───────────────── 완료

Recv D ───────── 완료

- 따라서 완료 결과를 받았을 때는 어떤 I/O 요청에 대한 완료인지 구분할 수 있어야 한다.

- 이때 I/O 요청과 함께 전달한 WSAOVERLAPPED를 이용할 수 있다.

WSARecv
   │
   ├─ Buffer
   │
   └─ WSAOVERLAPPED
           │
           ↓
       I/O 진행
           │
           ↓
       I/O 완료
           │
           ↓
   기존 작업과 연결

- 즉 WSAOVERLAPPED는 단순히 WSARecv()의 인자로 전달하는 구조체가 아니라 I/O 요청과 완료 사이를 연결하기 위한 정보라고 볼 수 있다.


9. WSAOVERLAPPED의 수명 관리

- Overlapped I/O에서는 I/O 요청과 완료 시점이 분리되어 있기 때문에 객체의 수명 역시 중요하다.

- 예를 들어 다음과 같이 함수 내부에서 WSAOVERLAPPED를 생성했다고 가정해보자.

void Recv()
{
    WSAOVERLAPPED overlapped = {};
    WSARecv(socket,&buffer,1,nullptr,&flags,&overlapped,nullptr);
}

- I/O가 즉시 완료되지 않았다면 다음과 같은 상황이 발생할 수 있다.

Recv()
   ↓
WSARecv 요청
   ↓
WSA_IO_PENDING
   ↓
Recv() 종료
   ↓
overlapped 소멸
   ↓
하지만 I/O는 아직 진행 중

- 즉 애플리케이션에서는 WSAOVERLAPPED를 제거했지만 운영체제에서는 여전히 해당 I/O 작업을 처리하고 있을 수 있다.

- 따라서 Pending 중인 I/O가 완료될 때까지 해당 I/O에서 사용하는 Overlapped 정보가 유효해야 한다.


10. Buffer의 수명 관리

- 같은 이유로 I/O에 사용되는 Buffer 역시 완료 시점까지 유지되어야 한다.

void Recv()
{
    char buffer[1024];

    WSABUF wsaBuffer;
    wsaBuffer.buf = buffer;
    wsaBuffer.len = sizeof(buffer);

    WSARecv(...);
}

- 만약 WSARecv()가 Pending 상태가 되었다면 다음과 같은 문제가 발생할 수 있다.

WSARecv 요청
   ↓
Pending
   ↓
Recv() 함수 종료
   ↓
buffer 소멸
   ↓
I/O는 아직 진행 중

- 운영체제는 여전히 해당 Buffer를 사용하는 I/O 작업을 수행하고 있기 때문에 Buffer를 먼저 제거하면 안 된다.

- 즉 Overlapped I/O에서는 다음 데이터들의 수명을 함께 고려해야 한다.

I/O 요청
   │
   ├─ WSAOVERLAPPED
   │
   └─ Buffer

- 따라서 실제 서버에서는 I/O 작업과 관련된 데이터를 별도의 Context에 묶어 관리할 수 있다.

struct RecvContext
{
    WSAOVERLAPPED overlapped = {};

    WSABUF wsaBuffer = {};

    char buffer[1024] = {};
};

- 이렇게 구성하면 하나의 I/O 작업에 필요한 정보와 수명을 같이 관리할 수 있다.


11. 요청 순서와 완료 순서

- 비동기 I/O에서는 여러 작업을 동시에 요청할 수 있다.

요청 A B C

- 하지만 완료 통지가 반드시 요청한 순서대로 발생한다고 가정해서는 안 된다.

요청 A B C
↓
완료 통지 B A C

- 따라서 완료 결과를 처리할 때는 단순히 요청 순서를 기준으로 판단하는 것이 아니라 각 I/O 작업이 가지고 있는 Context를 기준으로 어떤 작업이 완료된 것인지 확인할 필요가 있다.

- 이 점 역시 이후 IocpContext와 같은 구조가 필요한 이유와 연결된다.


12. 완료된 I/O를 어떻게 확인할까?

- Overlapped I/O를 요청했다면 결국 해당 작업이 완료되었다는 사실을 확인해야 한다.

- Windows에서는 여러 방법을 사용할 수 있다.

Event Object

Completion Routine

WSAGetOverlappedResult()

IOCP

- 예를 들어 Event Object를 사용한다면 다음과 같은 흐름을 생각할 수 있다.

I/O 요청
   ↓
Pending
   ↓
I/O 진행
   ↓
I/O 완료
   ↓
Event Signal
   ↓
완료 처리

- 하지만 게임 서버처럼 많은 Socket에서 여러 I/O 작업이 동시에 발생한다면 각각의 완료를 어떻게 효율적으로 처리할 것인지도 고려해야 한다.

- 여기서 다음 문제가 생긴다. 여러 Socket에서 발생하는 수많은 Overlapped I/O의 완료를 어떻게 효율적으로 모아서 처리할 것인가?

 

 

- Windows에서는 이를 위한 방식 중 하나로 I/O Completion Port(IOCP)를 제공한다.


13. Overlapped I/O와 IOCP

- Overlapped I/O와 IOCP는 같은 개념은 아니다.

- Overlapped I/O는 I/O 요청과 완료를 분리하는 비동기 I/O 방식이다.

I/O 요청
   ↓
Pending
   ↓
I/O 완료

- 반면 IOCP는 여러 Overlapped I/O의 완료 결과를 효율적으로 전달하고 처리하기 위한 메커니즘이라고 볼 수 있다.

Socket A ─ Overlapped I/O ─┐
Socket B ─ Overlapped I/O ─┤
Socket C ─ Overlapped I/O ─┤
Socket D ─ Overlapped I/O ─┘
                           ↓
                       I/O 완료
                           ↓
                          IOCP
                           ↓
                     Worker Thread

- 따라서 두 개념의 역할을 나누면 다음과 같다.

Overlapped I/O : I/O 요청과 완료를 분리
IOCP : 여러 I/O의 완료를 효율적으로 전달하고 처리

- 즉 IOCP를 이해하기 위해서는 먼저 Overlapped I/O의

Request
   ↓
Pending
   ↓
Completion

- 구조를 이해할 필요가 있다.


14. 현재 프로젝트의 IocpContext와 연결

- 현재 프로젝트에서는 Overlapped I/O와 관련된 정보를 관리하기 위해 IocpContext를 사용하고 있다.

- Overlapped I/O에서는 I/O 요청 시점과 완료 시점이 서로 다르기 때문에 완료 시점에 I/O 완료만으로 부족하다.

 

- 실제로는 다음과 같은 정보를 확인할 필요가 있다.

어떤 I/O가 완료되었는가?

Recv인가?

Send인가?

Accept인가?

어떤 Session에서 요청한 작업인가?

어떤 Buffer를 사용하고 있는가?

- 따라서 개념적으로 다음과 같은 Context를 생각할 수 있다.

struct IocpContext
{
    OVERLAPPED overlapped;

    // I/O 종류

    // Session 정보

    // Buffer 정보

    // 완료 처리에 필요한 정보
};

- 즉 IocpContext는 단순히 OVERLAPPED를 감싸기 위한 구조가 아니다.

- I/O 요청 시점의 정보를 완료 시점까지 유지하고 이후 어떤 처리를 수행해야 하는지를 연결하기 위한 Context라고 볼 수 있다.

I/O 요청
   ↓
IocpContext 생성 및 유지
   ↓
I/O Pending
   ↓
I/O 완료
   ↓
IocpContext 확인
   ↓
Recv / Send / Accept 구분
   ↓
해당 작업 처리

- 이후 IOCP에서 완료된 I/O를 가져왔을 때 해당 Context를 이용하여 어떤 작업인지 구분하고 후속 처리를 수행할 수 있다.


15. 정리

- Overlapped I/O는 I/O 작업을 요청하는 시점과 실제 작업이 완료되는 시점을 분리해서 처리할 수 있도록 하는 Windows의 비동기 I/O 방식이다.

- WSARecv() 등의 Overlapped I/O를 요청했을 때 작업이 바로 완료될 수도 있지만, 즉시 처리할 수 없는 경우 WSA_IO_PENDING 상태가 될 수 있다.

Request
   ↓
Pending
   ↓
Completion

- WSA_IO_PENDING은 I/O 작업의 실패를 의미하는 것이 아니라 I/O 작업이 정상적으로 시작되었지만 아직 완료되지 않았다는 의미이다.

- I/O 요청과 완료 시점이 분리되기 때문에 WSAOVERLAPPED와 Buffer 등의 데이터 역시 I/O가 완료될 때까지 유효하게 유지할 필요가 있다.

- 또한 여러 I/O 작업을 동시에 요청할 수 있기 때문에 완료 시점에는 어떤 I/O가 완료된 것인지 구분할 수 있어야 한다.

- 따라서 실제 서버에서는 OVERLAPPED뿐만 아니라 I/O 종류, Buffer, Session 등의 정보를 하나의 Context에 함께 관리하는 구조를 사용할 수 있다.

- 현재 프로젝트에서 사용하는 IocpContext 역시 이러한 관점에서 I/O 요청 시점의 정보를 완료 처리 시점까지 연결하기 위한 객체라고 볼 수 있다.

- 그리고 여러 Overlapped I/O의 완료 결과를 효율적으로 처리하기 위해 Windows에서는 I/O Completion Port(IOCP)를 사용할 수 있다.

- 다음에는 IOCP가 어떤 구조로 동작하며 Completion Port, Completion Queue, Worker Thread가 각각 어떤 역할을 담당하는지 정리해보려고 한다.


16. 정리

 

- Microsoft Learn - Overlapped Input/Output

- https://learn.microsoft.com/en-us/windows/win32/winsock/overlapped-i-o-2

 

Overlapped Input/Output - Win32 apps

Windows Sockets 2 introduces overlapped I/O and requires that all transport providers support this capability.

learn.microsoft.com

 

- Microsoft Learn - Overlapped I/O and Event Objects

- https://learn.microsoft.com/en-us/windows/win32/winsock/overlapped-i-o-and-event-objects-2

 

Overlapped I/O and Event Objects - Win32 apps

Windows Sockets 2 supports overlapped I/O and all transport providers support this capability.

learn.microsoft.com

- Microsoft Learn - WSARecv function

- https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsarecv

 

WSARecv function (winsock2.h) - Win32 apps

Receives data from a connected socket or a bound connectionless socket. (WSARecv)

learn.microsoft.com

- Microsoft Learn - WSAOVERLAPPED structure

- https://learn.microsoft.com/en-us/windows/win32/api/winsock2/ns-winsock2-wsaoverlapped

 

WSAOVERLAPPED (winsock2.h) - Win32 apps

Provides a communication medium between the initiation of an overlapped I/O operation and its subsequent completion.

learn.microsoft.com

- Microsoft Learn - I/O Completion Ports

- https://learn.microsoft.com/en-us/windows/win32/fileio/i-o-completion-ports

 

I/O Completion Ports - Win32 apps

I/O completion ports provide an efficient threading model for processing multiple asynchronous I/O requests on a multiprocessor system.

learn.microsoft.com

 

Comments