25년2학기/컴퓨터 네트워크

컴넷 )강의 내용(10.17)

kimchangmin02 2025. 10. 17. 20:21

. 기존 TCP 클라이언트/서버 모델 복습 및 한계

가. TCP 소켓 통신 과정

  • 서버 동작 순서: socket()bind()listen()accept()send()/recv()
  • 클라이언트 동작 순서: socket()connect()send()/recv()
  • "노란 소켓"과 "빨간 소켓" (강의 비유)
    • 노란 소켓 (Listening Socket): 서버가 최초로 생성하여 listen() 하는 대표 소켓. 클라이언트의 접속 요청을 받는 창구 역할만 합니다.
    • 빨간 소켓 (Connection Socket): 클라이언트의 접속 요청이 오면 accept() 함수가 리턴하는 새로운 소켓. 실제 데이터 통신은 이 소켓을 통해 1:1로 전담하여 이루어집니다. accept()의 리턴 값(소켓 디스크립터)을 반드시 변수에 저장해야 합니다.

나. UDP와의 차이점

  • UDP: sendto(), recvfrom() 함수를 사용. 데이터를 보낼 때마다 목적지 주소를 지정해야 합니다. 하나의 소켓으로 여러 상대와 통신합니다.
  • TCP: send(), recv() 함수를 사용. connect()accept()를 통해 이미 상대방이 1:1로 정해졌으므로, 데이터를 보낼 때 목적지 주소를 명시할 필요가 없습니다.

다. 채팅 서비스 구현을 위한 기존 모델의 한계점

  • 서버의 한계 (순차적 처리)
    • 기존 서버 구조는 accept()로 클라이언트 접속을 받은 후, 해당 클라이언트와의 통신(send/recv)이 완전히 끝나야만 다시 accept() 상태로 돌아가 다음 클라이언트를 받을 수 있습니다.
    • 문제점: 한 번에 단 한 명의 클라이언트만 접속 및 서비스가 가능하여 동시 접속이 필요한 채팅 서버로 사용할 수 없습니다.
  • 클라이언트의 한계 (동시 작업 불가)
    • 기존 클라이언트 구조는 recv() 함수를 호출하면 서버로부터 데이터가 올 때까지 프로그램이 멈춥니다 (Blocking).
    • 문제점: 데이터를 수신 대기하는 동안에는 키보드 입력(송신)을 할 수 없습니다. 채팅 프로그램이라면 내가 원할 때 언제든지 메시지를 보내고, 동시에 다른 사람의 메시지도 받을 수 있어야 하는데 이것이 불가능합니다.

2. 해결책: 멀티스레딩(Multi-threading) 프로그래밍

하나의 프로그램이 동시에 여러 작업을 수행하게 하기 위한 기술입니다.

가. 프로세스(Process)와 메모리 구조

  • 프로세스: 실행 중인 프로그램. 운영체제로부터 컴퓨팅 자원(특히 메모리)을 할당받습니다.
  • 메모리 구조: 프로세스가 할당받은 메모리는 크게 4가지 영역으로 나뉩니다.
    1. 코드(Code/Text) 섹션: 컴파일된 기계어 코드가 저장되는 공간.
    2. 데이터(Data) 섹션: **전역 변수(Global variables)**와 static 변수가 저장되는 공간.
    3. 스택(Stack) 섹션: 함수 호출 시 생성되는 **지역 변수(Local variables)**와 매개변수가 저장되는 임시 작업 공간.
    4. 힙(Heap) 섹션: 프로그램 실행 중 동적으로 메모리를 할당(malloc)할 때 사용되는 공간.

나. 스레드(Thread)의 개념

  • 스레드: "프로세스 내의 실행 흐름 단위". 하나의 프로세스는 여러 개의 스레드를 가질 수 있습니다.
  • 자원 공유: 한 프로세스에 속한 스레드들은 코드, 데이터, 힙 영역을 공유합니다.
    • 이것이 스레드 간 데이터 공유가 비교적 쉬운 이유입니다. (예: 전역 변수 접근)
  • 독립적인 자원: 각 스레드는 자신만의 스택(Stack) 영역을 가집니다.
    • 각 스레드가 독립적으로 함수를 호출하고 지역 변수를 사용해야 하기 때문입니다. 이로 인해 한 스레드에서 다른 스레드의 지역 변수에 직접 접근하는 것은 불가능합니다.
  • 장점: 새로운 프로세스를 만드는 것보다 스레드를 만드는 것이 훨씬 자원 소모가 적고 가벼워(lightweight) 시스템에 부담을 덜 줍니다.

3. Windows에서의 스레드 생성: CreateThread API

  • 목적: 운영체제(Windows)에게 새로운 스레드를 만들어달라고 요청하는 함수.
  • 헤더 파일: windows.h를 반드시 #include 해야 합니다.
  • 함수 원형: HANDLE CreateThread(...)
  • 주요 파라미터 (6개 중 2개가 핵심)
    • 세 번째 파라미터: LPTHREAD_START_ROUTINE (함수 포인터)
      • 새로 생성될 스레드가 실행할 코드가 담긴 **함수의 이름(주소)**을 전달합니다. 스레드를 만들기 전에 이 함수를 미리 작성해 두어야 합니다.
    • 네 번째 파라미터: LPVOID (함수의 인자)
      • 세 번째 파라미터로 지정한 함수에게 전달할 **단 하나의 인자(Argument)**입니다.
      • 여러 정보를 넘기고 싶을 때는 보통 구조체(struct)를 만들어 그 주소를 넘기지만, 강의에서는 편의상 소켓 디스크립터나 배열 인덱스 같은 정수 값을 강제 형 변환하여 전달하는 방법을 사용했습니다.
    • 나머지 파라미터들은 대부분 0 또는 NULL로 설정해도 동작에 문제가 없습니다.

4. 멀티스레딩을 이용한 채팅 프로그램 구현 전략

가. 채팅 클라이언트 구현

  • 목표: 송신(키보드 입력)과 수신(서버 메시지) 작업을 동시에 처리한다.
  • 구현 방안:
    • 메인 스레드 (송신 전담):
      1. socket(), connect()로 서버에 접속합니다.
      2. 접속 성공 후, CreateThread()를 호출하여 수신 전담 스레드를 생성합니다.
      3. 무한 루프를 돌며 gets_s() (키보드 입력 대기) 함수를 실행합니다.
      4. 사용자가 메시지를 입력하고 엔터를 치면 send() 함수로 서버에 전송합니다.
    • 새로운 스레드 (수신 전담):
      1. 메인 스레드로부터 소켓 디스크립터를 인자로 전달받습니다.
      2. 무한 루프를 돌며 recv() 함수를 실행하여 서버로부터 메시지가 오기를 대기합니다.
      3. 메시지가 도착하면 printf() 등으로 화면에 출력합니다.

나. 채팅 서버 구현

  • 목표: 여러 클라이언트의 동시 접속을 처리하고, 한 클라이언트가 보낸 메시지를 모든 클라이언트에게 전달(Broadcast)한다.
  • 구현 방안:
    • 메인 스레드 (접속 처리 전담):
      1. socket(), bind(), listen()으로 서버를 준비합니다.
      2. 무한 루프를 돌며 accept() 함수만 계속 호출하여 클라이언트의 접속을 기다립니다.
      3. 새로운 클라이언트가 접속하면, CreateThread()를 호출하여 해당 클라이언트와의 통신을 전담할 새로운 스레드를 생성하고, 메인 스레드는 즉시 다시 accept() 상태로 돌아갑니다.
    • 새로운 스레드 (클라이언트 1:1 통신 전담):
      1. 메인 스레드로부터 특정 클라이언트와 연결된 빨간 소켓에 대한 정보(소켓 디스크립터 또는 인덱스)를 전달받습니다.
      2. 무한 루프를 돌며 recv()를 통해 담당 클라이언트로부터 메시지가 오기를 기다립니다.
      3. 메시지가 도착하면, 모든 클라이언트에게 이 메시지를 send()로 뿌려줍니다(브로드캐스트).

다. 서버 구현의 핵심 과제와 해결책

  • 과제: 한 스레드(A 클라이언트 담당)가 다른 모든 스레드가 관리하는 소켓에 접근하여 메시지를 보내야 합니다. 하지만 스레드별로 스택이 분리되어 있어 다른 스레드의 지역 변수(소켓 디스크립터)에 접근할 수 없습니다.
  • 해결책: 전역 변수(Global Variable)를 사용합니다.
    • 모든 "빨간 소켓"의 디스크립터를 저장할 수 있는 전역 변수 배열을 만듭니다.
    • 메인 스레드가 accept()로 새로운 소켓을 만들 때마다, 이 소켓의 디스크립터를 지역 변수가 아닌 전역 변수 배열의 빈자리에 저장합니다.
    • 데이터(Data) 섹션에 위치한 전역 변수는 모든 스레드가 공유하므로, 어떤 스레드든 이 배열에 접근하여 모든 클라이언트의 소켓 정보를 알아내고 메시지를 보낼 수 있습니다.

 

 

 

 

 

 

 

connect()

  • 핵심 역할: 지정된 서버의 IP 주소와 포트 번호로 연결을 요청합니다.

 

 

 

bind()

  • 핵심 역할: 생성된 소켓에 고유한 IP 주소와 포트 번호를 할당(연결)합니다.

 

 

 

 

 

 

socket()

  • 핵심 역할: 통신을 위한 끝점(Endpoint)인 소켓을 생성합니다.

 

 

 

 

 

 

 

 

 

  • listen(): "가게 오픈합니다! 지금부터 손님 줄 서세요!" 라고 선언하고, 손님들이 기다릴 **대기 공간(줄)**을 만드는 것.
  • accept(): "다음 손님 들어오세요!" 라고 외치며, 대기 줄의 맨 앞 손님 한 명을 받아 전담 직원을 붙여주는 행위.

 

 

 

 

 

 

 

두 함수의 역할과 결정적인 차이점

1. listen(소켓, 대기인원수) 함수

  • 역할: "상태 설정"
  • 무엇을 하는가?: bind()까지 마친 소켓을 "이제부터 외부 접속을 받을 수 있는 상태"로 만들어 줍니다. 이 함수가 호출되어야 비로소 서버 소켓이 귀를 열게 됩니다.
  • 블로킹(멈춤) 여부: 멈추지 않습니다. listen()은 "자, 이제 준비 끝!"이라고 상태만 설정하고 바로 다음 코드로 넘어갑니다.
  • 호출 횟수: 서버 프로그램이 시작될 때 단 한 번만 호출됩니다.

 

 

 

 

 

 

 

 

 

 

 

 

2. accept(리스닝소켓) 함수

  • 역할: "실제 연결 처리"
  • 무엇을 하는가?: listen()이 만들어 놓은 대기열을 감시하다가, 줄 서 있는 클라이언트가 있으면 맨 앞의 요청 하나를 꺼내 연결을 수락합니다.
  • 핵심 기능: 연결이 수락되면, 그 클라이언트와 1:1 통신을 전담할 아주 새로운 소켓("빨간 소켓")을 만들어서 반환(return)합니다. 원래의 리스닝 소켓("노란 소켓")은 이 통신에 관여하지 않고, 계속해서 다른 손님을 받을 준비만 합니다.
  • 블로킹(멈춤) 여부: 멈춥니다 (블로킹 함수). 대기열에 클라이언트 요청이 들어올 때까지 프로그램 실행을 멈추고 하염없이 기다립니다.
  • 호출 횟수: 새로운 클라이언트가 접속할 때마다 반복적으로 호출됩니다. 보통 while 루프 안에서 계속 실행됩니다.

 

 

 

 

1. 서버: 무엇을 동시에 하고 싶은가?

  • 동시에 하고 싶은 두 가지 일:
    1. 새로운 클라이언트의 접속 받기 (accept())
    2. 이미 접속한 클라이언트와 대화하기 (recv())

2. 클라이언트: 무엇을 동시에 하고 싶은가?

  • 동시에 하고 싶은 두 가지 일:
    1. 서버로부터 메시지 받기 (recv())
    2. 키보드로 내 메시지 입력해서 보내기 (gets_s() + send())

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

'25년2학기 > 컴퓨터 네트워크' 카테고리의 다른 글

컴넷)시험대비1 (10.19)  (0) 2025.10.19
컴넷) 시험문제 예상(10.17)  (0) 2025.10.17
컴넷)과제 (10.17)  (0) 2025.10.17
컴넷)예상문제 (10.15)  (0) 2025.10.15
컴넷) (10.15)  (0) 2025.10.15