프로그래밍/소켓 프로그래밍 입문

멀티스레드 에코 서버 구현

데일리 백수 2025. 1. 14. 21:19

기존의 에코 서버는 한 번에 하나의 클라이언트만 처리할 수 있습니다. 이는 여러 클라이언트가 동시에 연결을 시도하면 첫 번째 클라이언트가 작업을 완료할 때까지 다른 클라이언트가 대기 상태에 놓이는 문제를 초래합니다. 멀티스레드 에코 서버는 이러한 문제를 해결하기 위해 각 클라이언트 연결마다 별도의 스레드를 생성하여 동시에 여러 클라이언트를 처리할 수 있도록 설계됩니다.

 

. 기존 에코 서버의 문제점

1.1 단일 스레드 구조

기존 서버는 단일 스레드로 동작하며, 다음과 같은 문제가 있습니다:

  1. accept 블로킹:
    • 첫 번째 클라이언트가 연결되면 accept 이후의 로직에서 해당 클라이언트만 처리.
  2. 다중 클라이언트 지원 불가:
    • 추가 클라이언트는 기존 클라이언트의 작업이 완료될 때까지 대기.

 

2. 멀티스레드 에코 서버의 필요성

2.1 동시성 제공

  • 각 클라이언트 요청에 대해 별도의 스레드를 생성하여 처리.
  • 클라이언트는 대기 없이 즉각적인 연결과 서비스를 받을 수 있음.

2.2 워커 스레드 구조

  • 메인 스레드:
    • accept를 통해 클라이언트의 연결을 수락.
    • 새로운 클라이언트 연결 시 워커 스레드 생성.
  • 워커 스레드:
    • 생성된 스레드는 해당 클라이언트와 통신(recv/send)을 담당.

3. 멀티스레드 에코 서버 구현

3.1 메인 스레드 코드

메인 스레드는 클라이언트 연결을 수락하고, 워커 스레드를 생성합니다:

 

while ((hClient = ::accept(hSocket, (SOCKADDR*)&clientaddr, &nAddrLen)) != INVALID_SOCKET)
{
    puts("새 클라이언트가 연결되었습니다.");

    // 클라이언트 처리를 위한 워커 스레드 생성
    HANDLE hThread = ::CreateThread(
        NULL,             // 보안 속성
        0,                // 기본 스택 크기 (1MB)
        ThreadFunction,   // 실행할 스레드 함수
        (LPVOID)hClient,  // 클라이언트 소켓
        0,                // 기본 생성 플래그
        &dwThreadID);     // 생성된 스레드 ID

    // 스레드 핸들 닫기
    ::CloseHandle(hThread);
}

3.2 워커 스레드 코드

워커 스레드는 클라이언트와 통신을 담당하며, 연결이 끊어질 때 소켓을 닫습니다:

 

DWORD WINAPI ThreadFunction(LPVOID pParam)
{
    SOCKET hClient = (SOCKET)pParam;
    char szBuffer[128] = { 0 };
    int nReceive = 0;

    puts("새 클라이언트가 연결되었습니다.");

    // 클라이언트와 통신
    while ((nReceive = ::recv(hClient, szBuffer, sizeof(szBuffer), 0)) > 0)
    {
        // 수신한 데이터를 그대로 반향(Echo) 전송
        ::send(hClient, szBuffer, nReceive, 0);
        puts(szBuffer);
        memset(szBuffer, 0, sizeof(szBuffer));
    }

    puts("클라이언트 연결이 끊겼습니다.");
    ::closesocket(hClient);
    return 0;
}

 

4.1 메인 스레드

  1. 클라이언트 연결 요청을 수락(accept).
  2. 연결된 클라이언트에 대해 워커 스레드를 생성.
  3. 생성된 스레드에 클라이언트 소켓을 전달.

4.2 워커 스레드

  1. 클라이언트와 데이터를 송수신(recv/send).
  2. 연결이 끊어지면 소켓 자원을 해제.

. 멀티스레드 서버의 장점

  1. 동시성 제공:
    • 여러 클라이언트를 동시에 처리 가능.
    • 한 클라이언트의 작업이 다른 클라이언트에 영향을 주지 않음.
  2. 확장성:
    • 클라이언트 수가 증가하더라도 성능 저하를 최소화.
  3. 사용자 경험 개선:
    • 빠른 응답 속도와 대기 없는 연결 제공.

 

6. 주의사항

6.1 스레드 관리

  • 워커 스레드의 생성과 종료를 효율적으로 관리해야 함.
  • 무한정 스레드를 생성하면 시스템 자원이 고갈될 수 있으므로 스레드 풀(Thread Pool) 구현을 고려.

6.2 공유 자원 관리

  • 여러 스레드가 동일한 자원을 사용할 경우 경쟁 상태(Race Condition) 발생 가능.
  • 뮤텍스(Mutex), 크리티컬 섹션(Critical Section) 등을 사용해 동기화 필요.

6.3 메모리 누수 방지

  • 스레드 종료 후 소켓 자원(closesocket)과 스레드 핸들(CloseHandle)을 반드시 해제.