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” 방식으로 송수신을 수동 제어하면서 다음을 관찰하도록 설계되어 있습니다:

  1. 클라이언트가 1024바이트를 send() 하면 → 시스템 버퍼(100바이트)에 다 안 들어감
  2. 시스템 버퍼가 꽉 차면 send()가 블로킹 되거나, 일부만 전송됨
  3. 서버가 recv()를 늦게 호출하면 → 클라이언트 측 시스템 버퍼가 비워지지 않아 send()가 더 오래 블로킹됨
  4. 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 레벨 비교