블로킹 소켓의 의미와 장단점
블로킹 동작
- 블로킹 소켓에서
accept, recv, send는 준비될 때까지 호출 스레드를 멈춥니다.
- 코드가 직관적이라 입문/단일 연결 테스트에는 이해하기 쉽습니다.
왜 서버에서 문제가 되는가
| 상황 | 결과 |
|---|
단일 스레드가 recv에서 대기 | 다른 클라이언트 처리 지연 |
| 느린 클라이언트/유휴 연결 증가 | 처리량 급감 |
| 동접 증가 | 스레드 고갈 또는 대기 시간 폭증 |
게임 서버 관점
- MMO/FPS 서버는 "누가 언제 보낼지 모르는" 이벤트 기반 트래픽입니다.
- 블로킹 기반 단순 루프는 동접이 커질수록 구조적으로 불리합니다.
논블로킹 소켓으로 전환
설정
u_long on = 1;
ioctlsocket(sock, FIONBIO, &on);
동작 변화
| 함수 | 블로킹 소켓 | 논블로킹 소켓 |
|---|
accept | 접속 올 때까지 대기 | 즉시 반환 |
recv | 데이터 올 때까지 대기 | 즉시 반환 |
send | 버퍼 여유 생길 때까지 대기 가능 | 즉시 반환(준비 안 되면 실패 코드) |
핵심 변화
- "대기"가 "즉시 상태 확인"으로 바뀝니다.
- 대신 호출 결과를 더 세밀하게 분기 처리해야 합니다.
WSAEWOULDBLOCK 정확히 이해하기
의미
- 논블로킹 모드에서 준비되지 않은 I/O를 호출했을 때 나오는 상태 코드입니다.
- "진짜 장애"가 아니라 "지금은 아직 안 됨, 나중에 다시 시도"에 가깝습니다.
예시 분기
int n = recv(s, buf, sizeof(buf), 0);
if (n > 0) {
} else if (n == 0) {
} else {
int err = WSAGetLastError();
if (err == WSAEWOULDBLOCK) {
} else {
}
}
관련 코드 주의
accept, recv, send, non-blocking connect 모두 유사 패턴이 필요합니다.
- 상태 코드와 실제 에러 코드를 섞어 처리하면 장애 원인 추적이 어려워집니다.
논블로킹만으로는 부족한 이유
Busy Polling 문제
- 논블로킹 소켓을
while(true)로 계속 확인하면 CPU를 과도하게 소모합니다.
- 연결 수가 많을수록 "아무 일도 없는데 돌기만 하는" 루프 비용이 커집니다.
해결 방향
| 단계 | 아이디어 |
|---|
| 1 | 논블로킹으로 대기 블로킹 제거 |
| 2 | 준비된 소켓만 깨워주는 I/O multiplexing 도입 |
| 3 | 대규모 동접에서는 IOCP 같은 완료 기반 모델 사용 |
다음 Part와 연결
select, WSAEventSelect, Overlapped, IOCP는 모두 "불필요한 폴링을 줄이기 위한 진화"입니다.
블로킹/논블로킹 선택 가이드
| 환경 | 권장 |
|---|
| 단일 연결 디버깅/학습 | 블로킹도 충분 |
| 소수 연결 툴/테스트 | 블로킹 + 스레드 분리 가능 |
| 동접이 큰 게임 서버 | 논블로킹 + I/O 모델 필수 |
실무 원칙
- "논블로킹으로 바꿨다"가 끝이 아니라, 이벤트/완료 기반 구조까지 같이 설계해야 합니다.
- 상태 코드를 정상 흐름으로 처리하는 코드 스타일(분기 표준화)이 중요합니다.
강의 시 유의사항
강조 포인트
- 블로킹은 나쁜 기술이 아니라 "적용 범위가 제한된 기술"임을 설명하세요.
WSAEWOULDBLOCK을 에러가 아닌 "미준비 상태"로 분명히 구분시키세요.
- 논블로킹의 목적은 속도보다 확장성/응답성 확보입니다.
자주 하는 오해
| 오해 | 바로잡기 |
|---|
| 논블로킹이면 자동으로 고성능 | 폴링 구조면 CPU만 더 쓸 수 있음 |
WSAEWOULDBLOCK은 장애 코드 | 논블로킹에서는 정상적인 상태 코드 |
| 블로킹은 절대 사용하면 안 됨 | 디버깅/소규모/단순 시나리오에선 유용 |
체크 질문 (스스로 답해보기)
- 블로킹에서 논블로킹으로 바꿨을 때 코드 구조가 어떻게 달라지는가?
recv에서 SOCKET_ERROR가 나왔을 때 어떤 순서로 분기해야 하는가?
- 왜 논블로킹 다음 단계로 I/O 모델이 필수적으로 등장하는가?