네트워크 프로그래밍 핵심 개념과 Python 소켓 실습

1. 네트워크 인프라스트럭처 이해

1.1 네트워크 장비의 역할

컴퓨터 간 데이터 교환을 위해서는 다양한 네트워크 장비들이 필요합니다. 각 장비의 특성과 동작 원리를 살펴보겠습니다.

스위치 (L2 Switch)

여러 대의 컴퓨터를 연결하여 로컬 네트워크를 구성하는 핵심 장비입니다. 각 호스트에는 고유한 IP 주소가 할당되어야 통신이 가능합니다.

호스트 구성 예시:
- 워크스테이션 A: 192.168.10.1
- 워크스테이션 B: 192.168.10.2
- 워크스테이션 C: 192.168.10.3
- 워크스테이션 D: 192.168.10.4

ARP 프로토콜 동작 방식:

IP 주소만 알고 MAC 주소를 모르는 상황에서 통신을 시작하면, 스위치는 브로드캐스트 방식으로 모든 포트에 데이터를 전송합니다. 대상 호스트만 응답하고 나머지는 패킷을 버립니다.

이 과정에서 각 호스트는 ARP 테이블을 유지하여 IP-MAC 매핑 정보를 캐싱합니다. 이후 통신에서는 유니캐스트로 직접 전송이 가능합니다.

ARP 테이블 예시:
네트워크 주소        물리적 주소
192.168.10.1    a4-5f-3c-2b-11-9e
192.168.10.3    a4-5f-3c-2b-11-c0

스위치 내부에도 MAC 주소 테이블이 존재하여 포트와 연결된 호스트의 물리적 주소를 관리합니다.

라우터 (Router)

서로 다른 네트워크 세그먼트 간 통신을 가능하게 하는 장비입니다. 브로드캐스트 도메인을 분리하여 네트워크 성능을 향상시킵니다.

네트워크 구성 예시:
A동 사무실:
  - PC1: 192.168.10.1  게이트웨이: 192.168.10.254
  - PC2: 192.168.10.2  게이트웨이: 192.168.10.254

B동 사무실:
  - PC1: 192.168.20.1  게이트웨이: 192.168.20.254
  - PC2: 192.168.20.2  게이트웨이: 192.168.20.254

라우터 인터페이스 설정:
  eth0: 192.168.10.254 (192.168.10.0/24 네트워크)
  eth1: 192.168.20.254 (192.168.20.0/24 네트워크)

게이트웨이는 외부 네트워크로 나가는 관문 역할을 합니다. 라우팅 테이블을 참조하여 패킷을 적절한 인터페이스로 전달합니다.

L3 스위치

스위칭과 라우팅 기능을 통합한 장비로, VLAN을 활용하여 논리적으로 네트워크를 분리합니다.

VLAN 기반 구성:
VLAN 10 (A동):
  - 포트 1-3 할당
  - IP: 192.168.10.254
  
VLAN 20 (B동):
  - 포트 4-6 할당
  - IP: 192.168.20.254

1.2 핵심 네트워크 개념

IP 주소와 서브넷 마스크

IPv4 주소는 32비트로 구성되며, 점으로 구분된 10진수 표기법을 사용합니다.

이진 표현: 11000000.10101000.00000001.11001000
10진 표현: 192.168.1.200

서브넷 마스크는 네트워크 부분과 호스트 부분을 구분합니다:

예시 1:
IP:        192.168.1.200    11000000.10101000.00000001.11001000
서브넷:    255.255.255.0    11111111.11111111.11111111.00000000
네트워크:  192.168.1.0/24 (앞 24비트)

예시 2:
IP:        172.16.50.100    10101100.00010000.00110010.01100100
서브넷:    255.255.240.0    11111111.11111111.11110000.00000000
네트워크:  172.16.48.0/20 (앞 20비트)

같은 네트워크에 속한 호스트끼리는 직접 통신이 가능하고, 다른 네트워크와는 라우터를 거쳐야 합니다.

DHCP (Dynamic Host Configuration Protocol)

IP 주소를 수동으로 설정하는 대신, 네트워크 장비의 DHCP 서버에서 자동으로 할당받을 수 있습니다. IP 주소, 서브넷 마스크, 게이트웨이, DNS 정보가 함께 제공됩니다.

사설 IP와 공인 IP

사설 IP 대역 (RFC 1918):

  • 10.0.0.0 ~ 10.255.255.255
  • 172.16.0.0 ~ 172.31.255.255
  • 192.168.0.0 ~ 192.168.255.255

인터넷 통신을 위해서는 ISP에서 할당한 공인 IP가 필요합니다. NAT 기술을 통해 여러 사설 IP가 하나의 공인 IP를 공유하기도 합니다.

포트와 서비스 식별

단일 서버에서 여러 애플리케이션을 운영할 때 포트 번호로 구분합니다.

예시:
- 웹 서비스 A: 203.0.113.45:8080
- 웹 서비스 B: 203.0.113.45:9090
- API 서버: 203.0.113.45:5000

권장 사용자 정의 포트 범위: 5000-65535 (Well-known 포트 제외)

DNS와 도메인 이름

사람이 기억하기 쉬운 도메인을 IP 주소로 변환하는 시스템입니다.

도메인 → IP 매핑:
api.example.com  →  203.0.113.45
db.internal.net  →  10.0.0.15

DNS 조회 과정:

  1. 브라우저 캐시 확인
  2. 운영체제 캐시 및 hosts 파일 확인
  3. 로컬 DNS 서버 (resolver) 조회
  4. 필요시 루트 → TLD → 권한 네임서버 순차 조회

2. Python 소켓 프로그래밍

Python의 socket 모듈을 활용한 네트워크 애플리케이션 개발 방법입니다.

2.1 기본 TCP 통신 예제

에코 서버:

import socket

def run_echo_server(host='0.0.0.0', port=9001):
    listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    listener.bind((host, port))
    listener.listen(5)
    
    print(f"서버 시작: {host}:{port}")
    
    while True:
        conn, client_addr = listener.accept()
        print(f"연결 수락: {client_addr}")
        
        try:
            payload = conn.recv(4096)
            if payload:
                text = payload.decode('utf-8')
                print(f"수신: {text}")
                conn.sendall(f"에코: {text}".encode('utf-8'))
        finally:
            conn.close()

if __name__ == '__main__':
    run_echo_server()

에코 클라이언트:

import socket

def run_echo_client(server_ip, server_port):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    try:
        sock.connect((server_ip, server_port))
        message = "안녕하세요, 서버!"
        sock.sendall(message.encode('utf-8'))
        
        response = sock.recv(4096)
        print(f"서버 응답: {response.decode('utf-8')}")
    finally:
        sock.close()

if __name__ == '__main__':
    run_echo_client('127.0.0.1', 9001)

2.2 대화형 서비스 구현

간단한 문의 응대 시스템:

import socket

def inquiry_server():
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.bind(('127.0.0.1', 9002))
    server.listen(5)
    
    print("문의 시스템 대기 중...")
    
    while True:
        session, addr = server.accept()
        
        # 초기 환영 메시지
        session.sendall("고객 지원 시스템에 오신 것을 환영합니다.\n문의사항을 입력하세요 (종료: exit):\n".encode('utf-8'))
        
        while True:
            query = session.recv(2048).decode('utf-8').strip()
            if not query or query.lower() == 'exit':
                break
            
            # 간단한 키워드 매칭 응답
            if '가격' in query:
                reply = "요금 정보는 홈페이지에서 확인 가능합니다."
            elif '배송' in query:
                reply = "배송은 2-3일 소요됩니다."
            else:
                reply = "담당자에게 전달하겠습니다. 잠시만 기다려주세요."
            
            session.sendall((reply + "\n").encode('utf-8'))
        
        session.close()
        print(f"세션 종료: {addr}")

if __name__ == '__main__':
    inquiry_server()

대화형 클라이언트:

import socket

def inquiry_client():
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client.connect(('127.0.0.1', 9002))
    
    # 초기 메시지 수신
    print(client.recv(2048).decode('utf-8'), end='')
    
    while True:
        user_input = input("> ")
        if user_input.lower() == 'exit':
            break
        
        client.sendall(user_input.encode('utf-8'))
        response = client.recv(2048).decode('utf-8')
        print(response, end='')
    
    client.close()

if __name__ == '__main__':
    inquiry_client()

2.3 파일 전송 시스템

파일 수신 서버:

import socket
import os

def file_receiver(save_dir='./received', port=9003):
    os.makedirs(save_dir, exist_ok=True)
    
    receiver = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    receiver.bind(('127.0.0.1', port))
    receiver.listen(3)
    
    print(f"파일 수신 서버: {port}")
    
    conn, addr = receiver.accept()
    
    # 메타데이터 수신 (파일명:크기)
    meta = conn.recv(256).decode('utf-8')
    filename, filesize = meta.split(':')
    filesize = int(filesize)
    
    filepath = os.path.join(save_dir, filename)
    received = 0
    
    with open(filepath, 'wb') as f:
        while received < filesize:
            chunk = conn.recv(min(8192, filesize - received))
            if not chunk:
                break
            f.write(chunk)
            received += len(chunk)
            print(f"진행률: {received}/{filesize} bytes")
    
    print(f"저장 완료: {filepath}")
    conn.close()
    receiver.close()

if __name__ == '__main__':
    file_receiver()

파일 전송 클라이언트:

import socket
import os

def file_sender(filepath, server='127.0.0.1', port=9003):
    if not os.path.exists(filepath):
        print("파일이 존재하지 않습니다.")
        return
    
    filename = os.path.basename(filepath)
    filesize = os.path.getsize(filepath)
    
    sender = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sender.connect((server, port))
    
    # 메타데이터 전송
    sender.sendall(f"{filename}:{filesize}".encode('utf-8'))
    
    # 파일 내용 전송
    sent = 0
    with open(filepath, 'rb') as f:
        while sent < filesize:
            data = f.read(8192)
            sender.sendall(data)
            sent += len(data)
            print(f"전송 중: {sent}/{filesize} bytes")
    
    print("전송 완료")
    sender.close()

if __name__ == '__main__':
    file_sender('./sample.pdf')

3. 애플리케이션 아키텍처 패턴

3.1 C/S (Client/Server) 구조

전용 클라이언트 프로그램을 설치하여 서버와 통신하는 방식입니다. 데스크톱 애플리케이션, 모바일 네이티브 앱 등이 해당됩니다.

특징:

  • 풍부한 UI/UX 제공 가능
  • 로컬 리소스 직접 활용
  • 설치 및 업데이트 필요

3.2 B/S (Browser/Server) 구조

웹 브라우저를 클라이언트로 사용하여 서버와 통신하는 방식입니다. 별도 설치 없이 URL로 접근 가능합니다.

특징:

  • 플랫폼 독립적
  • 즉시 배포 및 업데이트
  • 브라우저 호환성 고려 필요

4. 실습 과제: 인증 기반 접속 시스템

사용자 인증 후 접속하는 보안 채널을 구현합니다.

인증 서버:

import socket
import json
import hashlib

class AuthServer:
    def __init__(self, port=9004):
        self.port = port
        self.credentials = self._load_users()
    
    def _load_users(self):
        # 실제 환경에서는 데이터베이스 사용 권장
        return {
            'admin': '5f4dcc3b5aa765d61d8327deb882cf99',  # 'password'의 MD5
            'user01': '0acf4539a14b3aa27deeb4cbdf6e989f'   # 'hello123'의 MD5
        }
    
    def verify(self, userid, password):
        hashed = hashlib.md5(password.encode()).hexdigest()
        return self.credentials.get(userid) == hashed
    
    def start(self):
        srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        srv.bind(('127.0.0.1', self.port))
        srv.listen(5)
        
        print(f"인증 서버 실행 중: {self.port}")
        
        while True:
            conn, addr = srv.accept()
            self._handle_session(conn)
    
    def _handle_session(self, conn):
        conn.sendall(json.dumps({
            'type': 'info',
            'message': '인증 시스템 v1.0\n아이디와 비밀번호를 |로 구분하여 입력하세요'
        }).encode('utf-8'))
        
        while True:
            data = conn.recv(1024).decode('utf-8').strip()
            if '|' not in data:
                conn.sendall(json.dumps({
                    'type': 'error',
                    'message': '형식: 아이디|비밀번호'
                }).encode('utf-8'))
                continue
            
            uid, pwd = data.split('|', 1)
            
            if self.verify(uid, pwd):
                conn.sendall(json.dumps({
                    'type': 'success',
                    'message': f'환영합니다, {uid}님!',
                    'token': hashlib.sha256(uid.encode()).hexdigest()[:16]
                }).encode('utf-8'))
                break
            else:
                conn.sendall(json.dumps({
                    'type': 'fail',
                    'message': '인증 실패. 다시 시도하세요.'
                }).encode('utf-8'))
        
        conn.close()

if __name__ == '__main__':
    AuthServer().start()

인증 클라이언트:

import socket
import json

def secure_client():
    cli = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    cli.connect(('127.0.0.1', 9004))
    
    # 서버 정보 수신
    info = json.loads(cli.recv(2048).decode('utf-8'))
    print(info['message'])
    
    while True:
        uid = input("아이디: ")
        pwd = input("비밀번호: ")
        
        cli.sendall(f"{uid}|{pwd}".encode('utf-8'))
        result = json.loads(cli.recv(2048).decode('utf-8'))
        
        print(f"[{result['type'].upper()}] {result['message']}")
        
        if result['type'] == 'success':
            print(f"발급된 토큰: {result.get('token')}")
            break
    
    cli.close()

if __name__ == '__main__':
    secure_client()

태그: socket TCP/IP network-programming python Client-Server

9월 23일 07:31에 게시됨