. 기존 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가지 영역으로 나뉩니다.
- 코드(Code/Text) 섹션: 컴파일된 기계어 코드가 저장되는 공간.
- 데이터(Data) 섹션: **전역 변수(Global variables)**와 static 변수가 저장되는 공간.
- 스택(Stack) 섹션: 함수 호출 시 생성되는 **지역 변수(Local variables)**와 매개변수가 저장되는 임시 작업 공간.
- 힙(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로 설정해도 동작에 문제가 없습니다.
- 세 번째 파라미터: LPTHREAD_START_ROUTINE (함수 포인터)
4. 멀티스레딩을 이용한 채팅 프로그램 구현 전략
가. 채팅 클라이언트 구현
- 목표: 송신(키보드 입력)과 수신(서버 메시지) 작업을 동시에 처리한다.
- 구현 방안:
- 메인 스레드 (송신 전담):
- socket(), connect()로 서버에 접속합니다.
- 접속 성공 후, CreateThread()를 호출하여 수신 전담 스레드를 생성합니다.
- 무한 루프를 돌며 gets_s() (키보드 입력 대기) 함수를 실행합니다.
- 사용자가 메시지를 입력하고 엔터를 치면 send() 함수로 서버에 전송합니다.
- 새로운 스레드 (수신 전담):
- 메인 스레드로부터 소켓 디스크립터를 인자로 전달받습니다.
- 무한 루프를 돌며 recv() 함수를 실행하여 서버로부터 메시지가 오기를 대기합니다.
- 메시지가 도착하면 printf() 등으로 화면에 출력합니다.
- 메인 스레드 (송신 전담):
나. 채팅 서버 구현
- 목표: 여러 클라이언트의 동시 접속을 처리하고, 한 클라이언트가 보낸 메시지를 모든 클라이언트에게 전달(Broadcast)한다.
- 구현 방안:
- 메인 스레드 (접속 처리 전담):
- socket(), bind(), listen()으로 서버를 준비합니다.
- 무한 루프를 돌며 accept() 함수만 계속 호출하여 클라이언트의 접속을 기다립니다.
- 새로운 클라이언트가 접속하면, CreateThread()를 호출하여 해당 클라이언트와의 통신을 전담할 새로운 스레드를 생성하고, 메인 스레드는 즉시 다시 accept() 상태로 돌아갑니다.
- 새로운 스레드 (클라이언트 1:1 통신 전담):
- 메인 스레드로부터 특정 클라이언트와 연결된 빨간 소켓에 대한 정보(소켓 디스크립터 또는 인덱스)를 전달받습니다.
- 무한 루프를 돌며 recv()를 통해 담당 클라이언트로부터 메시지가 오기를 기다립니다.
- 메시지가 도착하면, 모든 클라이언트에게 이 메시지를 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. 서버: 무엇을 동시에 하고 싶은가?
- 동시에 하고 싶은 두 가지 일:
- 새로운 클라이언트의 접속 받기 (accept())
- 이미 접속한 클라이언트와 대화하기 (recv())
2. 클라이언트: 무엇을 동시에 하고 싶은가?
- 동시에 하고 싶은 두 가지 일:
- 서버로부터 메시지 받기 (recv())
- 키보드로 내 메시지 입력해서 보내기 (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 |