네트워크 프로그래밍을 처음 시작하면 가장 먼저 배우는 함수가 있다. 바로 socket(), bind(), listen(), 그리고 accept(), connect(), closesocket() 이다. 이는 버클리 소켓(Berkeley Sockets) 기반의 표준 API로, 유닉스와 리눅스, 윈도우 등 대부분의 OS에서 호환된다.
하지만 C++ 기반의 윈도우 게임 서버나 대규모 고성능 서버를 개발하다 보면, 기존 표준 함수 대신 AcceptEx, ConnectEx, DisconnectEx라는 생소한 함수들과 마주하게 된다.
왜 윈도우에는 뒤에 Ex(Extended)가 붙은 확장 함수들이 따로 존재할까? 기존의 표준 소켓 함수들은 이제 도태된 것일까?
이번 포스팅에서는 일반 소켓 함수와 Ex 확장 함수의 차이점, 고성능 윈도우 서버에서 Ex 함수를 꼭 써야 하는 이유, 그리고 윈도우와 리눅스의 근본적인 I/O 철학 차이까지 깊이 있게 파헤쳐보고자 한다.
1) 들어가며: 왜 윈도우에는 'Ex'가 붙은 소켓 함수가 있을까?
결론부터 말하자면, 윈도우 소켓(Winsock)에 Ex 함수가 존재하는 이유는 마이크로소프트가 윈도우 환경에서 비동기 I/O(Overlapped I/O)와 IOCP(I/O Completion Port)의 성능을 100% 끌어내기 위해 만든 윈도우 전용 특화 API이기 때문이다.
표준 accept나 connect 함수는 본질적으로 동기형(Synchronous) 함수이다. 논블로킹(Non-blocking) 모드로 우회해서 사용할 수는 있지만, 윈도우 커널이 제공하는 완벽한 비동기 메커니즘과 맞물려 돌아가기에는 아쉬운 점이 많았다.
수만 명의 동시 접속자를 처리해야 하는 MMORPG 서버나 대규모 실시간 통신 서버에서 발생하는 병목 현상(소켓 생성 오버헤드, 컨텍스트 스위칭 등)을 커널 단에서 해결하기 위해 탄생한 것이 바로 AcceptEx, ConnectEx, DisconnectEx이다.
2) 일반 함수 vs Ex 확장 함수 완벽 비교
각 함수가 어떤 극적인 차이를 만들어내는지 표를 통해 비교해보자.
① 서버의 수락: accept vs AcceptEx
| 비교 항목 | 일반 accept 함수 | AcceptEx 확장 함수 |
| 동작 방식 | 동기 (블로킹 / 논블로킹) | 완전 비동기 (Overlapped I/O) |
| 소켓 생성 주체 | 연결이 발생하면 OS가 새롭게 생성 | 개발자가 미리 할당해 둔 빈 소켓을 사용 |
| 데이터 수신 | 연결 후 별도의 recv 호출 필요 | 연결 수락과 동시에 첫 번째 데이터를 수신 가능 |
| 함수 호출 | 라이브러리 직접 호출 | 동적 링킹 (WSAIoctl로 함수 포인터 획득 필요) |
② 클라이언트의 연결: connect vs ConnectEx
| 비교 항목 | 일반 connect 함수 | ConnectEx 확장 함수 |
| 동작 방식 | 동기형 (완료될 때까지 블로킹) | 완전 비동기 (IOCP와 연동) |
| 데이터 송신 | 연결 완료 후 별도의 send 호출 필요 | 연결 성공과 동시에 첫 번째 데이터를 즉시 송신 가능 |
③ 소켓의 종료: closesocket vs DisconnectEx
| 비교 항목 | closesocket / shutdown | DisconnectEx 확장 함수 |
| 동작 방식 | 소켓 리소스 파괴 (Kernel 반환) | 소켓을 파괴하지 않고 초기화(Reset) |
| 재사용성 | 불가능 (재접속 시 다시 socket() 호출) | 재사용 가능 (Socket Pooling의 핵심) |
3) 고성능 윈도우 서버의 심장, AcceptEx와 확장 OVERLAPPED
IOCP 서버를 개발할 때 가장 난관이 되는 부분이 바로 AcceptEx의 구조이다. 기존 accept가 낚싯대를 던지고 물고기가 물 때까지 기다리는 것이라면, AcceptEx는 바다에 수백 개의 통발을 미리 던져놓고(비동기 호출), 알림이 오면 건져 올리는 방식이다.
AcceptEx 사용의 핵심 포인트
1. 함수 포인터 획득: AcceptEx는 ws2_32.dll에 직접 노출되어 있지 않다. 따라서 프로그램 시작 시 리슨 소켓을 통해 WSAIoctl 함수로 LPFN_ACCEPTEX (함수 포인터)를 얻어와야 한다.
2. 미리 던져두는 빈 소켓: accept와 달리, 개발자가 미리 WSASocket으로 빈 소켓을 생성하여 AcceptEx의 인자로 넘겨주어야 한다.
비동기 작업의 핵심: 확장 OVERLAPPED 패턴
IOCP에서 알림(GetQueuedCompletionStatus)이 오면, 이 작업이 Accept인지 Recv인지 Send인지 어떻게 구분할까? 윈도우 C++ 서버에서는 구조체의 첫 번째 멤버 주소가 구조체 자체의 주소와 동일하다는 특징을 활용한 '확장 OVERLAPPED 패턴'을 사용한다.
// 비동기 작업 종류 정의
enum class IO_TYPE { ACCEPT, RECV, SEND, DISCONNECT };
// ★ 핵심: 확장 OVERLAPPED 구조체 정의
struct EXTENDED_OVERLAPPED {
WSAOVERLAPPED overlapped; // 무조건 첫 번째 멤버여야 한다. (OS가 인식하는 영역)
// 여기서부터 사용자 정의 영역
IO_TYPE ioType; // 개발자가 식별할 작업 종류
WSABUF wsaBuf; // 버퍼 정보
char buffer[1024]; // 실제 데이터
void Clear() {
ZeroMemory(&overlapped, sizeof(WSAOVERLAPPED));
}
};
작업을 요청할 때 위 구조체를 만들어 OS에 넘기고, IOCP 알림이 오면 반환된 OVERLAPPED 포인터를 (EXTENDED_OVERLAPPED*)로 캐스팅하여 ioType을 확인한다. 이것이 윈도우 비동기 서버 엔진의 핵심 동작 원리이다.
4) DisconnectEx와 소켓 풀링(Socket Pooling)의 시너지
C++ 등 성능에 민감한 언어에서는 객체의 생성과 소멸(new / delete)이 잦아질수록 성능이 급격히 저하된다. 운영체제 커널의 리소스를 사용하는 소켓(socket(), closesocket())은 오버헤드가 훨씬 더 크다.
이 문제를 해결하기 위해 소켓 풀링(Socket Pooling) 기법을 사용한다. 수천 개의 빈 소켓을 미리 만들어두고 꺼내 쓰는 방식이다. 이때 DisconnectEx가 빛을 발한다.
클라이언트의 접속이 끊어졌을 때 소켓을 파괴(closesocket)하는 대신, DisconnectEx를 호출하여 소켓 내부 상태만 깨끗하게 초기화한다. 그리고 이 소켓을 다시 AcceptEx 통발에 묶어서 재활용(TF_REUSE_SOCKET 플래그)한다. 이를 통해 서버 실행 중 소켓 생성/소멸로 인한 커널 오버헤드를 0(Zero)으로 만들 수 있다.
5) [부록] 윈도우와 리눅스의 서로 다른 철학 (Proactor vs Reactor)
여기서 한 가지 중요한 의문이 생길 수 있다. "그렇다면 일반 accept, connect 함수는 성능이 구려서 최신 상용 서버에서는 안 쓰이는 도태된 함수일까?"
정답은 "절대 아니다" 이다. 리눅스(Linux) 생태계에서는 여전히 accept가 초고성능 상용 서버의 핵심으로 활발하게 사용되고 있다. Nginx, Redis, Node.js 모두 accept를 기반으로 작동한다.
이러한 차이는 윈도우와 리눅스의 비동기 I/O 아키텍처 철학이 다르기 때문이다.
윈도우의 철학: Proactor 패턴 (완료 통지)
- 방식: "OS야, 이 빈 소켓 던져줄 테니까 알아서 연결 맺고(작업 수행), 모든 작업이 완료되면 나한테 IOCP로 알려줘."
- 특징: OS가 주도적으로 I/O를 처리한다. 따라서 처음부터 완벽하게 비동기로 설계된 AcceptEx 같은 특수 함수가 필요하다.
리눅스의 철학: Reactor 패턴 (준비 상태 통지)
- 방식: "OS야, 리슨 소켓에 새로운 클라이언트가 와서 데이터를 읽을 준비가 되면 나한테 epoll로 신호만 줘. 내가 직접 논블로킹 accept로 데이터를 꺼내갈게."
- 특징: OS는 '상태'만 알려주고, 실제 작업(I/O)은 애플리케이션 계층에서 표준 함수(accept, recv)를 논블로킹 모드로 호출하여 수행한다. 따라서 굳이 AcceptEx 같은 별도의 OS 종속적인 확장 함수를 만들 필요가 없었다. (최근에는 리눅스도 io_uring을 도입하며 Proactor 방식을 지원하기 시작했다.)
6) 마무리
정리하자면 다음과 같다.
1. 리눅스 생태계에서는 epoll과 논블로킹 accept의 조합이 현재 전 세계 백엔드 서버 트렌드의 표준이다.
2. 윈도우 환경에서 극한의 성능을 내는 게임 서버 등을 개발할 때는, OS 구조상 IOCP와 시너지를 내는 AcceptEx, DisconnectEx의 사용이 사실상 필수이다.
현대의 백엔드 개발은 Java의 Netty, C++의 Boost.Asio, Rust의 Tokio, Go 언어 등 훌륭한 네트워크 프레임워크들이 이 OS 간의 차이(epoll vs IOCP)를 내부적으로 추상화하여 알아서 최적화해 준다.
'Windows Socket' 카테고리의 다른 글
| Windows 논블로킹/블로킹 소켓 제어 ioctlsocket 함수 (0) | 2025.03.02 |
|---|---|
| 블로킹vs논블로킹 모델 / 동기vs비동기형 모델 (2) (0) | 2022.11.04 |
| 블로킹vs논블로킹 모델 / 동기vs비동기형 모델 (1) (0) | 2022.07.01 |
| Windows TCP서버 소켓 통신- Select서버 (0) | 2022.02.17 |
| 다중 접속 서버 모델 (0) | 2021.11.16 |




































