| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | ||||
| 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| 11 | 12 | 13 | 14 | 15 | 16 | 17 |
| 18 | 19 | 20 | 21 | 22 | 23 | 24 |
| 25 | 26 | 27 | 28 | 29 | 30 | 31 |
- team project
- docker
- c#
- BOJ
- Toy Project
- multi-thread
- Data Structure
- git
- 독서
- Online Judge
- Photon
- Unity
- C++
- Network Programming
- System Programming
- design pattern
- PS
- Today
- Total
I'm FanJae.
Unity 게임 개발 캠프 개인 프로젝트 7일차. Overlapped I/O - 요청과 완료가 분리되는 구조 (개인 공부 내용) 본문
Unity 게임 개발 캠프 개인 프로젝트 7일차. Overlapped I/O - 요청과 완료가 분리되는 구조 (개인 공부 내용)
FanJae 2026. 9. 21. 23:591. 시작에 앞서
- 이전에는 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
'Projects > MyToyMapleServer' 카테고리의 다른 글
| Unity 게임 개발 캠프 개인 프로젝트 9일차. MySQL 연동과 로그인 서버 DB 조회 구조 추가 (0) | 2026.09.22 |
|---|---|
| Unity 게임 개발 캠프 개인 프로젝트 8일차. 인증 키 생성과 인증 티켓 유효 시간 관리 (0) | 2026.09.22 |
| Unity 게임 개발 캠프 개인 프로젝트 7일차. 캐릭터 선택 이후 게임 서버 입장 흐름 구현 (0) | 2026.09.20 |
| Unity 게임 개발 캠프 개인 프로젝트 6일차. LoginServer와 GameServer 인증 연동 및 네트워크 송수신 안정성 보완 작업 (0) | 2026.09.19 |
| Unity 게임 개발 캠프 개인 프로젝트 6일차. Non-Blocking Socket과 Polling의 한계 (개인 공부 내용) (0) | 2026.09.19 |