| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
- Data Structure
- BOJ
- System Programming
- git
- docker
- Toy Project
- team project
- Unity
- 독서
- c#
- C++
- PS
- Network Programming
- multi-thread
- Photon
- Online Judge
- design pattern
- Today
- Total
I'm FanJae.
Unity 게임 개발 캠프 개인 프로젝트 6일차. Non-Blocking Socket과 Polling의 한계 (개인 공부 내용) 본문
Unity 게임 개발 캠프 개인 프로젝트 6일차. Non-Blocking Socket과 Polling의 한계 (개인 공부 내용)
FanJae 2026. 9. 19. 21:581. 시작에 앞서
- 이전에는 Blocking 방식으로 여러 클라이언트를 처리할 때 발생할 수 있는 문제에 대해 정리했다.
- Blocking Socket에서는 recv()와 같은 I/O 함수를 호출했을 때 작업을 바로 완료할 수 없다면 해당 Thread가 대기하게 된다.
Thread
recv()
│
│ 데이터 대기
│
▼
return
- 따라서 여러 클라이언트를 동시에 처리하기 위해 Client마다 Thread를 하나씩 할당하는 방법을 생각할 수 있었다.
Client A ─ Thread A
Client B ─ Thread B
Client C ─ Thread C
- 하지만 연결 수가 증가하면 Thread 수도 같이 증가하고, 많은 Thread가 실제 연산이 아닌 네트워크 I/O를 기다리기 위해 존재하게 된다.
- 그렇다면 이런 의문이 생긴다. 데이터를 기다리지 않고, recv()가 바로 반환하도록 만들 수 있지 않을까?
- 여기서 등장하는 방식이 Non-Blocking Socket이다.
2. Non-Blocking Socket
- Blocking Socket에서는 읽을 데이터가 없으면 recv()가 데이터를 기다릴 수 있다.
- 반면 Non-Blocking Socket에서는 현재 작업을 바로 완료할 수 없다면 기다리지 않고 즉시 반환한다.
- Windows에서는 이 경우 일반적으로 WSAEWOULDBLOCK 오류를 반환한다.
- 예를 들어 다음과 같이 사용할 수 있다.
int result = recv(socket,buffer,sizeof(buffer),0);
if (result == SOCKET_ERROR)
{
int error = WSAGetLastError();
if (error == WSAEWOULDBLOCK)
{
// 현재 읽을 수 있는 데이터가 없음
}
}
- 흐름으로 보면 다음과 같다.
recv()
│
▼
현재 데이터가 있는가?
│
├─ YES → 데이터 반환
│
└─ NO → WSAEWOULDBLOCK
- 따라서 Thread는 데이터가 도착할 때까지 recv() 안에서 멈춰 있지 않는다.
3. Blocking 방식과 비교
- Blocking 방식에서는 다음과 같은 상황이 발생할 수 있다.
Client A
recv()
│
│
│ 데이터 대기
│
▼
return
- 그동안 해당 Thread는 다른 작업을 할 수 없다.
- 반면 Non-Blocking 방식에서는 데이터가 없으면 아래와 같이 즉시 반환되어 Thread가 다른 작업 수행이 가능하다.
recv()
│
▼
데이터 없음
│
▼
즉시 반환
- 겉으로 보면 Blocking 서버에서 발생했던 문제가 해결된 것처럼 보인다.
- 하지만 새로운 문제가 생긴다.
4. 그렇다면 언제 다시 recv()를 호출해야 할까?
- 현재 데이터가 없어서 WSAEWOULDBLOCK이 반환되었다고 가정해보자.
recv(...);
↓
// WSAEWOULDBLOCK
- recv()는 바로 반환되었지만 결국 서버는 언젠가 다시 recv()를 호출해야 한다.
- 그렇다면 언제 다시 확인해야 할까?
- 가장 단순한 방법은 반복해서 호출하는 것이다.
while (true)
{
int result = recv(socket,buffer,sizeof(buffer),0);
if (result > 0)
{
ProcessPacket(buffer);
break;
}
if (WSAGetLastError() == WSAEWOULDBLOCK)
{
continue;
}
}
- 동작 흐름은 다음과 같다.
recv()
↓
데이터 없음
recv()
↓
데이터 없음
recv()
↓
데이터 없음
recv()
↓
데이터 도착
- 이러한 방식은 프로그램이 직접 상태를 계속 확인한다는 점에서 Polling이라고 볼 수 있다.
- Non-Blocking 작업이 WSAEWOULDBLOCK을 반환한 경우, 이후 언제 다시 작업을 시도해야 하는지를 판단하기 위한 별도의 메커니즘이 필요하다고 설명한다.
5. 단순 Polling의 문제
- Non-Blocking 방식의 장점은 Thread가 I/O에서 멈추지 않는다는 것이다.
- 하지만 다음과 같이 계속 recv()를 호출한다면 문제가 생긴다.
while (true)
{
recv(SocketA);
recv(SocketB);
recv(SocketC);
}
- 실제로 데이터가 없어도 계속 Socket을 확인한다.
- 예를 들어 세 개의 Socket이 있다고 해보자.
Socket A → 데이터 없음
Socket B → 데이터 없음
Socket C → 데이터 없음
- 서버는 계속 반복한다.
A 확인 → 없음
B 확인 → 없음
C 확인 → 없음
A 확인 → 없음
B 확인 → 없음
C 확인 → 없음
A 확인 → 없음
...
- Blocking 방식에서는 Thread가 아무 작업도 하지 않고 기다렸다면, 단순한 Non-Blocking Polling에서는 반대로 할 일이 없어도 CPU를 사용하면서 계속 상태를 확인할 수 있다.
- 즉, Thread가 너무 자주 확인하게 된다는 문제가 생긴다.
6. 여러 Socket을 직접 순회한다면?
- 클라이언트가 여러 명이라면 다음과 같은 방식도 생각할 수 있다.
for (Socket& socket : sockets)
{
int result = recv(socket,buffer,sizeof(buffer),0);
if (result > 0)
{
ProcessPacket(buffer);
}
}
- 이 구조에서는 하나의 Thread가 여러 Socket을 관리할 수 있다.
Thread
│
├─ Socket A 확인
├─ Socket B 확인
├─ Socket C 확인
├─ Socket D 확인
└─ ...
- Client마다 Thread를 하나씩 만들 필요는 없어졌다.
- 하지만 연결 수가 많아지면 매번 모든 Socket을 순회해서 확인해야 한다.
100개의 Socket
→ 100개 확인
1,000개의 Socket
→ 1,000개 확인
10,000개의 Socket
→ 10,000개 확인
- 실제로 데이터가 들어온 Socket이 극히 일부라도 애플리케이션이 전체 Socket을 반복적으로 검사한다면 불필요한 확인 작업이 증가한다.
- 이에 대해서 이와 다음과 같은 고려 사항이 생긴다. '데이터가 들어온 Socket만 알려줄 수는 없을까?'
7. Socket의 상태를 한 번에 확인하기
- 이 문제를 해결하기 위한 방법 중 하나가 select()이다.
- Windows의 select() 함수는 하나 이상의 Socket에 대해 읽기 가능, 쓰기 가능, 예외 상태 등을 확인할 수 있다. 호출이 반환되면 조건을 만족한 Socket 집합을 확인할 수 있다.
- 개념적으로 보면 다음과 같다.
Socket A ─┐
Socket B ─┤
Socket C ─┼─ select()
Socket D ─┤
Socket E ─┘
│
▼
현재 준비된 Socket 확인
- 예를 들어 소켓에 대해서 각각 아래와 같다고 가정한다.
Socket A → 읽을 데이터 없음
Socket B → 읽을 데이터 있음
Socket C → 읽을 데이터 없음
Socket D → 읽을 데이터 있음
select()
↓
// B, D가 Read 가능
- 그러면, select()에서는 이와 같이 확인 가능하다.
- 따라서 애플리케이션에서 무작정 모든 Socket에 recv()를 반복 호출하는 것보다는 효율적인 방식으로 상태를 확인할 수 있다.
8. select()를 이용한 구조
대략적인 구조는 다음과 같다.
fd_set readSet;
FD_ZERO(&readSet);
for (SOCKET socket : sockets)
{
FD_SET(socket, &readSet);
}
int result = select(
0,
&readSet,
nullptr,
nullptr,
nullptr);
- select()가 반환되면 각 Socket이 읽기 가능한지 확인한다.
for (SOCKET socket : sockets)
{
if (FD_ISSET(socket, &readSet))
{
recv(socket,buffer,sizeof(buffer),0);
}
}
- 흐름은 다음과 같다.
Socket 집합 등록
↓
select()
↓
준비된 Socket 확인
↓
해당 Socket만 처리
- select()는 fd_set에 등록된 여러 Socket을 대상으로 읽기 가능성, 쓰기 가능성, 오류 상태 등을 확인하며, 반환 후에는 조건을 만족하는 Socket만 집합에 남는다.
9. 그렇다면 select()로 충분한가?
- 단순 Polling보다는 개선되었지만 select() 역시 생각해볼 부분이 있다.
- 우선 select()는 애플리케이션이 관심 있는 Socket 집합을 전달하고, 호출이 반환된 뒤 다시 어떤 Socket이 준비되었는지를 확인하는 방식이다.
- 즉 기본적인 구조는 여전히 준비된 Socket에 대해서 애플리케이션이 확인 후 처리하는 형태다.
- 따라서 많은 연결을 처리하는 서버에서는 애플리케이션이 계속 Socket의 상태를 확인하는 구조가 아니라, 'I/O 작업 자체를 미리 요청하고 작업이 끝났을 때 완료 사실을 전달받을 수는 없을까?' 라는 방향으로 생각할 수 있다.
10. 상태 확인에서 완료 통지로
- 지금까지의 형태를 보면, 각각 다음과 같다.
Blocking
recv()
↓
데이터가 올 때까지 기다림
↓
처리
Non-Blocking
recv()
↓
데이터 없음
↓
바로 반환
↓
나중에 다시 확인
select()
여러 Socket 감시
↓
준비된 Socket 확인
↓
recv()
- 여기에서 한 단계 더 생각해볼 수 있다.
recv 작업을 미리 요청
↓
운영체제가 I/O 처리
↓
I/O 완료
↓
완료 사실을 전달
↓
완료된 작업 처리
- 즉, 준비가 되었는지 계속 확인하는 것이 아니라 작업이 끝나면 알려달라고 요청하는 방식이다.
- 이것이 Windows에서 사용하는 Overlapped I/O와 이후 IOCP를 이해하는 중요한 출발점이 된다.
11. Overlapped I/O로 넘어가기
- Windows에서는 Winsock에 Overlapped I/O를 사용할 수 있다.
- Overlapped I/O 작업이 Socket의 Blocking / Non-Blocking 모드와 별개의 개념이라고 명시한다.
- 즉 Overlapped 속성을 사용한다고 해서 Socket 자체가 Non-Blocking 모드로 변경되는 것은 아니며, Overlapped I/O 작업은 별도의 비동기 I/O 모델로 동작한다.
- Overlapped I/O에서는 대략 다음과 같은 형태가 된다.
WSARecv 요청
↓
I/O Pending
↓
Thread는 다른 작업 수행
↓
수신 완료
↓
완료 처리
- 이 과정에서 WSAOVERLAPPED 구조체는 I/O 작업을 시작한 시점과 이후 작업 완료 시점을 연결하는 정보를 제공한다.
- 즉 이전 방식과의 가장 큰 차이는 다음과 같이 생각할 수 있다.
Non-Blocking
"지금 데이터 있어?"
"아직 없어."
"지금은?"
"아직 없어."
"지금은?"
Overlapped I/O
"데이터 수신 작업을 요청할게."
↓
"완료되면 알려줄게."
12. 현재 프로젝트와 연결
- 현재 서버 프로젝트에서는 이후 IOCP 기반으로 여러 Session의 네트워크 I/O를 처리하려고 한다.
- Blocking 방식이라면 다음과 같은 구조를 생각할 수 있었다.
Session A ─ Thread A ─ recv()
Session B ─ Thread B ─ recv()
Session C ─ Thread C ─ recv()
- Non-Blocking Socket에서는 하나의 Thread가 여러 Session을 순회하며 상태를 확인할 수도 있다.
Thread
│
├─ Session A 확인
├─ Session B 확인
├─ Session C 확인
└─ ...
- 하지만 IOCP에서는 방향이 달라진다.
Session A ─ I/O 요청 ─┐
Session B ─ I/O 요청 ─┤
Session C ─ I/O 요청 ─┤
Session D ─ I/O 요청 ─┘
│
▼
Windows Kernel
│
I/O 완료
│
▼
IOCP
│
▼
Worker Thread
- 즉 Thread가 모든 Session을 계속 확인하는 것이 아니라 미리 등록한 I/O 중 완료된 작업을 중심으로 처리하는 구조를 만들 수 있다.
- Winsock 문서에서도 여러 연결을 처리하는 서버에서는 Overlapped I/O 또는 IOCP와 같은 비동기 I/O 모델을 사용할 수 있으며, 최신 서버 코드에서는 이러한 비동기 I/O 방식을 권장하고 있다.
13. 정리
- Blocking I/O에서는 작업을 완료할 수 없으면 Thread가 대기한다.
- 이를 해결하기 위해 Non-Blocking Socket을 사용하면 recv()와 같은 호출이 현재 처리할 수 없는 경우 WSAEWOULDBLOCK을 반환하고 즉시 제어권을 돌려준다.
- 하지만 Non-Blocking으로 변경했다고 모든 문제가 해결되는 것은 아니다.
Blocking
↓
Thread가 I/O를 기다림
Non-Blocking
↓
Thread가 직접 완료 가능 여부를 확인해야 함
Polling
↓
불필요한 반복 확인 가능
select()
↓
여러 Socket의 Ready 상태를 묶어서 확인
Overlapped I/O
↓
I/O 작업 요청과 완료 처리를 분리
- 결국 중요한 것은 Thread가 기다리지 않는 것 뿐만 아니라 I/O가 언제 처리 가능한지를 어떻게 효율적으로 알아낼 것인가에 있다.
- 그리고 Windows에서는 이 문제를 비동기 I/O인 Overlapped I/O를 통해 처리할 수 있다.
- 다음에는 OVERLAPPED와 WSARecv()를 기준으로 Windows의 Overlapped I/O가 실제로 어떤 방식으로 동작하는지 정리해보려고 한다.
14. 참고
- Microsoft Learn, Nonblocking Input/Output
- https://learn.microsoft.com/en-us/windows/win32/winsock/nonblocking-i-o-2
Nonblocking Input/Output - Win32 apps
If a socket is in nonblocking mode, any I/O operation must either complete immediately or return the error code WSAEWOULDBLOCK indicating that the operation cannot be finished right away.
learn.microsoft.com
- Microsoft Learn, select function (winsock2.h)
- https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-select
select function (winsock2.h) - Win32 apps
The select function determines the status of one or more sockets, waiting if necessary, to perform synchronous I/O.
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, 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, Getting Started With Winsock
- https://learn.microsoft.com/en-us/windows/win32/winsock/getting-started-with-winsock
Getting started with Winsock - Win32 apps
Use the links and resources in this topic as a step-by-step guide to getting started with Windows Sockets programming.
learn.microsoft.com
'Projects > MyToyMapleServer' 카테고리의 다른 글
| Unity 게임 개발 캠프 개인 프로젝트 7일차. 캐릭터 선택 이후 게임 서버 입장 흐름 구현 (0) | 2026.09.20 |
|---|---|
| Unity 게임 개발 캠프 개인 프로젝트 6일차. LoginServer와 GameServer 인증 연동 및 네트워크 송수신 안정성 보완 작업 (0) | 2026.09.19 |
| Unity 게임 개발 캠프 개인 프로젝트 5일차. LoginServer · GameServer 인증 구조 구성 (0) | 2026.09.19 |
| Unity 게임 개발 캠프 개인 프로젝트 4일차. IOCP 기반 비동기 네트워크 처리 구조 구현 (0) | 2026.09.17 |
| Unity 게임 개발 캠프 개인 프로젝트 4일차. Blocking 서버의 한계 (개인 공부 내용) (0) | 2026.09.17 |