Network Socket opus digested
Network_Pgm3 코드 분석: System Buffer & Blocking/Non-Blocking 모드
프로젝트 전체 구조
이 프로젝트는 2단계로 구성되어, 소켓 통신의 핵심 개념을 점진적으로 설명합니다.
graph TD
subgraph "1단계: System Buffer 관찰"
A["1_StreamClient_PressEnter<br/>(TCP 송신)"] --> B["1_StreamServer_PressEnter<br/>(TCP 수신)"]
C["1_DatagramClient_PressEnter<br/>(UDP 송신)"] --> D["1_DatagramServer_PressEnter<br/>(UDP 수신)"]
end
subgraph "2단계: Blocking vs Non-Blocking"
E["2_StreamClient_Echo<br/>(TCP Echo 클라이언트)"] --> F["2_StreamServer_DoubleClient<br/>(TCP 다중 클라이언트 서버)"]
G["2_DatagramClient_Echo<br/>(UDP Echo 클라이언트)"] --> H["2_DatagramServer_DoubleSocket<br/>(UDP 다중 소켓 서버)"]
end
subgraph "GUI 확장: 4가지 I/O 모델"
I["WinDatagramClient<br/>(MFC GUI 클라이언트)"] --> J["WinDatagramServer<br/>(MFC GUI 서버)"]
end
1단계: System Buffer (시스템 버퍼) 개념 설명
핵심 교훈: “Application Buffer ≠ System Buffer”
[!IMPORTANT] 모든 1단계 코드의 핵심은 애플리케이션이
send()에 전달하는 데이터가 곧바로 네트워크로 나가는 게 아니라, OS 커널의 System Buffer에 복사된 뒤 OS가 알아서 전송한다는 것을 보여주는 것입니다.
코드에서 보여주는 방식
모든 1단계 코드에서 공통적으로 이 패턴이 등장합니다:
// Application Buffer 크기 (프로그램이 한 번에 다루는 데이터 크기)
#define APP_SND_BUF_SIZE 1024
// System Buffer 크기 (OS 커널 내부 버퍼, 의도적으로 작게 설정)
#define SYS_SND_BUF_SIZE 100
// setsockopt로 System Buffer 크기를 100바이트로 줄이려고 시도
int szBuf = SYS_SND_BUF_SIZE;
setsockopt(hSock, SOL_SOCKET, SO_SNDBUF, (char *)&szBuf, sizeof(szBuf));
// getsockopt로 실제 설정된 크기를 확인
getsockopt(hSock, SOL_SOCKET, SO_SNDBUF, (char *)&szBuf, &lenBufUnit);
// 비교 출력: "[APP/SYS] Sending Buffer Size has been set to [1024/100] Bytes"
printf("[APP/SYS] Sending Buffer Size has been set to [%d/%d] Bytes\n",
APP_SND_BUF_SIZE, szBuf);
수업에서 의도하는 실험 시나리오
| 구분 | 값 | 역할 |
|---|---|---|
APP_SND_BUF_SIZE |
1024 | 앱이 send()에 넘기는 최대 크기 |
SYS_SND_BUF_SIZE |
100 | OS 커널 송신 버퍼를 인위적으로 작게 설정 |
“Press Enter” 방식으로 송수신을 수동 제어하면서 다음을 관찰하도록 설계되어 있습니다:
- 클라이언트가 1024바이트를
send()하면 → 시스템 버퍼(100바이트)에 다 안 들어감 - 시스템 버퍼가 꽉 차면
send()가 블로킹 되거나, 일부만 전송됨 - 서버가
recv()를 늦게 호출하면 → 클라이언트 측 시스템 버퍼가 비워지지 않아send()가 더 오래 블로킹됨 nSent != nMsgSize체크로 부분 전송을 확인:
if ((nSent = send(hSock, app_wBuffer, nMsgSize, 0)) != SOCKET_ERROR) {
if (nSent != nMsgSize) {
printf("Only %d bytes from %d bytes are sent \n", nSent, nMsgSize);
}
}
[!TIP] TCP(
SOCK_STREAM)에서는 부분 전송이 발생할 수 있고, UDP(SOCK_DGRAM)에서는 메시지 단위 전송이므로 부분 전송이 발생하지 않습니다. 1단계에서 Stream/Datagram 쌍을 모두 제공하는 이유가 이것입니다.
2단계: Blocking vs Non-Blocking 모드 설명
핵심 API: ioctlsocket()
2단계 코드에서는 커맨드 라인 인자로 I/O 모드를 선택합니다:
// StreamServer_DoubleClient.c / DatagramServer_DoubleSocket.c
// 실행: 프로그램.exe <port> <max_clients> <I/O mode: 0|1>
u64 mode = atoi(argv[3]); // 0 = Blocking, 1 = Non-Blocking
// ioctlsocket으로 소켓 모드 전환
ioctlsocket(hSock[nAcceptedClient], FIONBIO, (unsigned long *)&mode);
printf("Socket-%d has been set to %s mode\n",
nAcceptedClient, mode == 1 ? "non-Blocking" : "Blocking");
mode 값 |
FIONBIO 결과 |
동작 |
|---|---|---|
0 |
Blocking (기본값) | recv()가 데이터 올 때까지 무한 대기 |
1 |
Non-Blocking | recv()가 데이터 없으면 즉시 에러 반환 (WSAEWOULDBLOCK) |
Blocking 모드의 문제점 시연: 다중 클라이언트
StreamServer_DoubleClient.c에서 서버가 여러 클라이언트의 소켓을 순차적으로 recv()합니다:
// Blocking 모드(mode=0)일 때의 문제점:
for (int nAcceptedClient = 0; nAcceptedClient < max_clients; nAcceptedClient++) {
// 클라이언트 0번의 recv()에서 블로킹되면
// → 클라이언트 1번의 데이터는 영원히 읽지 못함!
nReceived = recv(hSock[nAcceptedClient], app_rBuffer, APP_SND_BUF_SIZE, 0);
// 에코 응답
send(hSock[nAcceptedClient], app_rBuffer, nReceived, 0);
}
[!WARNING] Blocking 모드의 치명적 문제: 첫 번째 클라이언트의
recv()에서 블로킹되면, 두 번째 클라이언트의 데이터가 이미 시스템 버퍼에 도착해 있어도 서버는 읽을 수 없습니다. 이것이 Non-Blocking 모드가 필요한 이유입니다.
Non-Blocking 모드의 해결책
Non-Blocking 모드(mode=1)에서는 recv()가 데이터가 없으면 SOCKET_ERROR를 반환하고, WSAGetLastError()가 WSAEWOULDBLOCK을 돌려줍니다:
// Non-Blocking 모드일 때의 루프
int nReceived, nErrorCode;
do {
for (int nAcceptedClient = 0; nAcceptedClient < max_clients; nAcceptedClient++) {
memset(app_rBuffer, 0, sizeof(app_rBuffer));
// recv()가 즉시 반환 → 데이터 없으면 SOCKET_ERROR
nReceived = recv(hSock[nAcceptedClient], app_rBuffer, APP_SND_BUF_SIZE, 0);
if (nReceived != SOCKET_ERROR) {
// 데이터 있으면 처리 + 에코
send(hSock[nAcceptedClient], app_rBuffer, nReceived, 0);
}
// 데이터 없으면 다음 소켓으로 넘어감 (블로킹 안 됨!)
}
nErrorCode = WSAGetLastError();
} while (nReceived > 0 || nErrorCode == WSAEWOULDBLOCK);
// ↑ WSAEWOULDBLOCK이면 "데이터가 없을 뿐 에러는 아님"
// → 루프를 계속 돌면서 폴링(polling)
UDP 버전 (DatagramServer_DoubleSocket)
DatagramServer_DoubleSocket.c도 동일한 패턴이지만 2개의 소켓(hSock1, hSock2)을 다른 포트에 bind해서 같은 문제를 보여줍니다:
// 2개 소켓 모두 Non-Blocking으로 전환
ioctlsocket(hSock1, FIONBIO, (unsigned long *)&mode);
ioctlsocket(hSock2, FIONBIO, (unsigned long *)&mode);
// 폴링 루프에서 두 소켓을 번갈아 확인
do {
nReceived = recvfrom(hSock1, ...); // 즉시 반환
nReceived = recvfrom(hSock2, ...); // 즉시 반환
nErrorCode = WSAGetLastError();
} while (nReceived > 0 || nErrorCode == WSAEWOULDBLOCK);
GUI 확장: MFC 기반 4가지 I/O 모델 비교
WinDatagramServerDlg.cpp에서는 라디오 버튼으로 4가지 모드를 선택할 수 있습니다:
enum SOCKET_IO_OPTION { SOCK_BLOCKING, SOCK_NONBLOCKING, SOCK_THREAD, SOCK_ASYNCHRONOUS };
| 모드 | 구현 방식 | 설명 |
|---|---|---|
| Blocking | 시작 버튼 누르면 바로 recvfrom() 루프 진입 |
GUI가 얼어붙음 (응답 불가) |
| Non-Blocking | ioctlsocket(FIONBIO, 1) 후 “Recv” 버튼 수동 클릭 |
사용자가 직접 폴링 |
| Thread | CreateThread()로 별도 스레드에서 recvfrom() 루프 |
GUI 안 멈추고 데이터 수신 |
| Asynchronous | MFC CAsyncSocket 사용, OnReceive() 콜백 |
이벤트 기반 (가장 우아함) |
각 모드별 코드 분석
① Blocking — GUI가 멈춤:
case SOCK_BLOCKING:
OnButtonRecv(); // recvfrom() 루프에 갇힘 → UI 스레드 블로킹
break;
② Non-Blocking — 사용자가 수동 폴링:
case SOCK_NONBLOCKING:
ULONG bNBIO = 1;
ioctlsocket(m_hSock, FIONBIO, &bNBIO);
// Recv 버튼을 활성화 → 사용자가 클릭할 때마다 recvfrom() 시도
break;
③ Thread — 별도 스레드에서 블로킹 수신:
case SOCK_THREAD:
m_hThread = ::CreateThread(NULL, 0, RecvThread, (void *)this, 0, &dwThreadID);
// DoRecvThread()에서 recvfrom() 루프를 돌지만 UI 스레드와 분리됨
break;
④ Asynchronous — 이벤트 기반 콜백:
case SOCK_ASYNCHRONOUS:
// MFC CAsyncSocket이 내부적으로 WSAAsyncSelect 사용
m_asyncSock.Create(m_nPort, SOCK_DGRAM);
// 데이터 도착 시 OnReceive() 콜백이 자동 호출됨
break;
// MyAsyncSocket.cpp — OnReceive 콜백
void CMyAsyncSocket::OnReceive(int nErrorCode) {
int nReceived = ReceiveFrom(rBuffer, 512, NULL, NULL);
if (nReceived > 0) {
pDlg->MyTextOut("[MSG:%d] %s\r\n", nReceived, rBuffer);
}
}
요약: 수업 자료가 전달하는 핵심 개념 흐름
graph LR
A["1단계<br/>System Buffer 이해"] --> B["2단계<br/>Blocking의 한계"]
B --> C["Non-Blocking<br/>Polling 방식"]
C --> D["Thread 방식"]
D --> E["Asynchronous<br/>이벤트 기반"]
style A fill:#e8f5e9,stroke:#4caf50
style B fill:#ffebee,stroke:#f44336
style C fill:#fff3e0,stroke:#ff9800
style D fill:#e3f2fd,stroke:#2196f3
style E fill:#f3e5f5,stroke:#9c27b0
| 단계 | 파일 | 핵심 API | 설명하는 개념 |
|---|---|---|---|
| 1 | *_PressEnter (4개) |
setsockopt(SO_SNDBUF) |
시스템 버퍼 크기와 send()/recv() 관계 |
| 2 | *_DoubleClient/Socket (2개) |
ioctlsocket(FIONBIO) |
Blocking이 다중 소켓 처리에서 문제되는 이유 |
| 2 | *_Echo (2개) |
send()→recv() 쌍 |
Echo 프로토콜을 통한 요청-응답 패턴 |
| GUI | WinDatagramServer/Client |
CAsyncSocket, CreateThread |
4가지 I/O 모델의 GUI 레벨 비교 |