PromleeBlog
sitemap
aboutMe

posting thumbnail
실전 채팅앱 만들기 TCP 소켓 프로그래밍 - 코딩과 함께 배우는 네트워크 12일차
Building a Chat App TCP Socket Programming - Learning Network with Coding Day 12

📅

🚀

들어가기 전에 🔗

지난 시간까지 우리는 네트워크의 여러 중요한 개념들(OSI 7계층, TCP/IP, HTTP, DNS, 각종 장비와 프로토콜 등)을 차근차근 배워왔습니다.
오늘은 이 지식들을 바탕으로, 우리 손으로 직접 간단한
채팅 애플리케이션
을 만들어보는 특별한 경험을 할 겁니다.
Python 언어와 TCP 소켓 프로그래밍을 이용하여 여러 사람이 함께 대화할 수 있는 채팅 서버와 클라이언트를 구현해 보겠습니다.

🚀

소켓 프로그래밍이란 무엇일까요? 🔗

우리가 채팅을 하거나, 웹사이트에 접속하거나, 온라인 게임을 할 때 프로그램들은 서로 데이터를 주고받아야 합니다.
이때 프로그램들이 네트워크를 통해 서로 "대화"할 수 있도록 만들어주는 창구나 통로 역할을 하는 것이 바로
소켓(Socket)
입니다.

소켓 프로그래밍은 이러한 소켓을 생성하고, 설정하고, 이를 통해 데이터를 송수신하는 프로그램을 만드는 과정을 말합니다.
마치 우리가 전화를 걸기 위해 전화기를 들고(소켓 생성), 상대방 번호를 누르고(연결 요청), 통화하는(데이터 송수신) 과정과 비슷합니다.

소켓 통신의 핵심: IP 주소와 포트 번호 🔗

채팅 프로그램도 데이터를 주고받기 위해 특정 IP 주소의 특정 포트 번호를 사용하게 됩니다.
IP 주소와 포트 번호
IP 주소와 포트 번호

🚀

TCP 채팅 서버 만들기 (Python) 🔗

전체 코드는 아래 결론 부분에서 확인할 수 있습니다.
이제 Python으로 여러 클라이언트가 접속해서 메시지를 주고받을 수 있는 간단한 TCP 채팅 서버를 만들어 보겠습니다.
먼저, 원하는 폴더에 python_tcp_server.py라는 이름으로 아래 코드를 작성해 주세요.

1. 필요한 모듈 임포트 🔗

네트워크 프로그래밍을 위한 socket 모듈과, 여러 클라이언트의 요청을 동시에 처리하기 위한 threading 모듈을 가져옵니다.
python_tcp_server.py
import socket # 소켓 프로그래밍을 위한 기본 모듈
import threading # 여러 클라이언트의 동시 처리를 위한 스레딩 모듈

2. 서버 설정 및 소켓 생성 🔗

서버가 어떤 IP 주소와 포트 번호에서 클라이언트의 접속을 기다릴지 설정하고, TCP 소켓을 생성합니다.
python_tcp_server.py
  # 서버 설정
HOST = '127.0.0.1'  # 로컬호스트 IP 주소 (자기 자신 컴퓨터에서 테스트)
PORT = 9999    # 사용할 포트 번호 (다른 서비스와 겹치지 않도록 임의로 지정)
 
  # 소켓 생성 (AF_INET: IPv4 주소 체계, SOCK_STREAM: TCP 프로토콜)
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 
  # 소켓 주소 재사용 옵션 설정 (서버 종료 후 바로 재시작 시 주소 이미 사용 중 오류 방지)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
 
  # 소켓에 IP 주소와 포트 번호 할당 (바인딩)
server_socket.bind((HOST, PORT))
 
  # 클라이언트 연결 요청을 기다리는 상태로 전환 (최대 5개 연결 동시 대기 가능)
server_socket.listen(5) 
print(f"[*] 채팅 서버가 {HOST}:{PORT} 에서 시작되었습니다. 클라이언트의 접속을 기다립니다...")

3. 클라이언트 연결 처리 및 메시지 브로드캐스팅 함수 🔗

새로운 클라이언트가 접속할 때마다 별도의 스레드를 생성하여 각 클라이언트의 메시지를 처리하고,
받은 메시지를 다른 모든 클라이언트에게 전달
(브로드캐스팅)하는 함수를 만듭니다.
python_tcp_server.py
  # 접속한 클라이언트들을 관리하기 위한 리스트
clients = []
  # 클라이언트 리스트 접근을 동기화하기 위한 락(Lock) 객체
clients_lock = threading.Lock()
 
def broadcast(message, sender_socket):
  """
  메시지를 보낸 클라이언트를 제외한 모든 클라이언트에게 메시지를 전송합니다.
  """
  with clients_lock: # 여러 스레드가 동시에 clients 리스트에 접근하는 것을 방지
    for client_socket_in_list in list(clients): # 반복 중 리스트 변경에 안전하도록 복사본 사용
      if client_socket_in_list != sender_socket: # 메시지를 보낸 클라이언트에게는 다시 보내지 않음 (옵션)
        try:
          client_socket_in_list.send(message)
        except Exception: # 예외 발생 시 (예: 클라이언트 연결 끊김)
          # 오류 발생 시 해당 클라이언트 연결 제거
          clients.remove(client_socket_in_list)
          print(f"[-] 클라이언트({client_socket_in_list.getpeername() if client_socket_in_list.fileno() != -1 else '알수없음'}) 연결이 비정상적으로 종료되었습니다.")
 
def handle_client(client_socket, addr):
  """
  개별 클라이언트의 연결을 처리하고 메시지를 수신/브로드캐스트합니다.
  """
  print(f"[+] 새로운 클라이언트({addr})가 접속했습니다.")
  
  # 새로운 클라이언트를 리스트에 추가
  with clients_lock:
    clients.append(client_socket)
 
  try:
    while True:
      # 클라이언트로부터 메시지 수신 (최대 1024바이트)
      # recv()는 데이터가 올 때까지 기다립니다 (블로킹 함수).
      message = client_socket.recv(1024)
      if not message: # 클라이언트가 연결을 정상적으로 종료하면 빈 메시지가 수신됨
        break # 반복문 종료
      
      # 받은 메시지 화면에 출력 (서버 콘솔)
      # 실제 서비스에서는 발신자 정보 등을 포함하여 가공 후 브로드캐스트
      decoded_message = message.decode('utf-8')
      print(f"[{addr}:{addr}] {decoded_message}") 
      
      # 메시지를 보낸 클라이언트의 주소 정보와 메시지를 함께 브로드캐스트
      # 모든 클라이언트에게 [주소:포트] 메시지 형식으로 전달
      formatted_message_for_broadcast = f"[{addr}:{addr}] {decoded_message}".encode('utf-8')
      broadcast(formatted_message_for_broadcast, client_socket) # 자기 자신 제외하고 발송
 
  except ConnectionResetError: # 클라이언트가 강제로 연결을 끊었을 때 발생하는 오류
    print(f"[-] 클라이언트({addr}) 연결이 강제로 리셋되었습니다.")
  except Exception as e: # 기타 예외 처리
    print(f"[!] 오류 발생 ({addr}): {e}")
  finally:
    # 클라이언트 연결 종료 처리
    print(f"[-] 클라이언트({addr}) 접속이 종료되었습니다.")
    with clients_lock:
      if client_socket in clients:
        clients.remove(client_socket)
    client_socket.close() # 소켓 닫기

4. 메인 루프: 클라이언트 연결 수락 🔗

서버는 무한 루프를 돌면서 새로운 클라이언트의 접속을 계속 기다리고, 접속이 이루어지면 각 클라이언트를 처리할 스레드를 생성합니다.
python_tcp_server.py
  # 메인 서버 로직
try:
  while True:
    # 클라이언트의 연결 요청 수락 (연결되면 새로운 소켓과 주소 정보 반환)
    # accept()는 블로킹 함수로, 클라이언트가 접속할 때까지 기다립니다.
    client_socket, addr = server_socket.accept()
    
    # 각 클라이언트를 처리하기 위한 새로운 스레드 생성 및 시작
    # args에는 handle_client 함수에 전달할 인자들을 튜플 형태로 전달합니다.
    client_handler_thread = threading.Thread(target=handle_client, args=(client_socket, addr))
    client_handler_thread.daemon = True # 메인 프로그램 종료 시 스레드도 함께 종료되도록 설정
    client_handler_thread.start()
except KeyboardInterrupt: # Ctrl+C 입력 시 서버 종료
  print("\n[*] 채팅 서버를 종료합니다.")
finally:
  # 모든 클라이언트 소켓 닫기 (선택적, 스레드가 각자 닫지만 안전을 위해)
  with clients_lock:
    for client_sock in clients:
      client_sock.close()
  server_socket.close() # 서버 소켓 닫기
  print("[*] 모든 연결이 종료되었습니다.")

🚀

TCP 채팅 클라이언트 만들기 (Python) 🔗

이제 위에서 만든 채팅 서버에 접속할 수 있는 간단한 클라이언트 프로그램을 만들어 보겠습니다.

1. 필요한 모듈 임포트 및 서버 정보 설정 🔗

서버와 마찬가지로 socket 모듈과 threading 모듈이 필요합니다. 접속할 서버의 IP 주소와 포트 번호도 설정합니다.
python_tcp_client.py
import socket # 소켓 프로그래밍 모듈
import threading # 메시지 수신을 위한 스레딩 모듈
 
  # 접속할 서버 정보
SERVER_HOST = '127.0.0.1' # 서버 IP 주소 (서버와 같은 컴퓨터에서 실행 시)
SERVER_PORT = 9999    # 서버 포트 번호

2. 서버에 연결 및 메시지 수신 함수 🔗

클라이언트가 서버에 접속하고, 서버로부터 오는 메시지를 지속적으로 받아서 화면에 출력하는 함수를 만듭니다. 이 함수는 별도의 스레드에서 실행될 것입니다.
python_tcp_client.py
def receive_messages(client_socket):
  """
  서버로부터 메시지를 계속 수신하여 화면에 출력합니다.
  """
  try:
    while True:
      message = client_socket.recv(1024) # 서버로부터 메시지 수신 (최대 1024 바이트)
      if not message: # 서버가 연결을 끊거나, 문제가 발생한 경우
        print("\n[!] 서버와의 연결이 끊어졌습니다. 엔터를 눌러 종료하세요.")
        break # 수신 스레드 종료
      print(message.decode('utf-8')) # 수신 메시지 화면에 출력
  except ConnectionAbortedError: # 사용자가 강제 종료 (예: 소켓 닫힘)
    print("\n[!] 연결이 중단되었습니다.")
  except ConnectionResetError: # 서버가 강제로 연결을 끊은 경우
    print("\n[!] 서버에 의해 연결이 강제로 종료되었습니다. 엔터를 눌러 종료하세요.")
  except Exception as e: # 기타 예외
    print(f"\n[!] 메시지 수신 중 오류 발생: {e}")
  finally:
    # 소켓이 이미 닫히지 않았다면 닫아줍니다. (메인 스레드에서 닫을 수도 있음)
    if client_socket.fileno() != -1: 
      client_socket.close()
 
  # 1. 클라이언트 소켓 생성
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 
try:
  # 2. 서버에 연결 시도
  client_socket.connect((SERVER_HOST, SERVER_PORT))
  print(f"[*] 채팅 서버({SERVER_HOST}:{SERVER_PORT})에 연결되었습니다. 메시지를 입력하세요 (종료: exit).")
 
  # 3. 서버로부터 메시지를 수신할 스레드 생성 및 시작
  receive_thread = threading.Thread(target=receive_messages, args=(client_socket,))
  receive_thread.daemon = True # 메인 프로그램 종료 시 스레드도 함께 종료
  receive_thread.start()
 
  # 4. 사용자로부터 메시지를 입력받아 서버로 전송
  while True:
    message_to_send = input() # 사용자 입력 대기 (이 부분이 블로킹될 수 있음)
    if message_to_send.lower() == "exit": # 'exit' 입력 시 종료
      print("[*] 채팅을 종료합니다.")
      break
    
    # 소켓이 아직 유효한지 (수신 스레드에서 닫히지 않았는지) 확인 후 전송
    if client_socket.fileno() == -1:
      print("[!] 이미 연결이 종료되었습니다.")
      break
      
    try:
      client_socket.send(message_to_send.encode('utf-8')) # 메시지 인코딩하여 서버로 전송
    except BrokenPipeError: # 서버와의 연결이 이미 끊어진 상태에서 보내려고 할 때
      print("[!] 메시지를 보낼 수 없습니다. 서버와의 연결이 끊어졌습니다.")
      break
    except Exception as e:
      print(f"[!] 메시지 전송 중 오류 발생: {e}")
      break
 
 
except ConnectionRefusedError:
  print(f"[!] 서버({SERVER_HOST}:{SERVER_PORT})에 연결할 수 없습니다. 서버가 실행 중인지 확인하세요.")
except Exception as e:
  print(f"[!] 프로그램 실행 중 오류 발생: {e}")
finally:
  # 프로그램 종료 시 소켓이 아직 열려있다면 닫습니다.
  if client_socket.fileno() != -1:
    client_socket.close() 

🚀

채팅 애플리케이션 실행 및 테스트 🔗

  1. 서버 실행
    먼저 위에서 작성한 python_tcp_server.py (또는 원하는 이름으로 저장) 파일을 파이썬으로 실행합니다.
    터미널에 "채팅 서버가 ... 시작되었습니다..." 메시지가 뜨면 성공입니다.
  2. 클라이언트 실행
    이제 python_tcp_client.py 파일을 실행합니다.
    새 터미널 창을 열어서 실행해도 되고, 다른 컴퓨터에서 실행해도 됩니다 (이 경우 SERVER_HOST를 서버 컴퓨터의 실제 IP 주소로 변경해야 합니다).
  3. 여러 클라이언트 접속
    python_tcp_client.py 파일을 여러 개 실행하여 다수의 클라이언트를 서버에 접속시킬 수 있습니다.
  4. 채팅 시작
    각 클라이언트 창에서 메시지를 입력하고 엔터를 치면, 서버를 통해 다른 모든 클라이언트에게 메시지가 전달되는 것을 확인할 수 있습니다.
    서버 콘솔에는 어떤 클라이언트가 어떤 메시지를 보냈는지 로그가 남습니다.
  5. 종료
    클라이언트 창에서 exit를 입력하면 해당 클라이언트가 종료됩니다.
    서버는 Ctrl+C를 눌러 종료할 수 있습니다.
채팅 애플리케이션 실행
채팅 애플리케이션 실행

🚀

결론 🔗

전체 서버 코드 보기 (python_tcp_server.py)
import socket # 소켓 프로그래밍을 위한 기본 모듈
import threading # 여러 클라이언트의 동시 처리를 위한 스레딩 모듈
 
  # 서버 설정
HOST = '127.0.0.1'  # 로컬호스트 IP 주소 (자기 자신 컴퓨터에서 테스트)
PORT = 9999    # 사용할 포트 번호 (다른 서비스와 겹치지 않도록 임의로 지정)
 
  # 소켓 생성 (AF_INET: IPv4 주소 체계, SOCK_STREAM: TCP 프로토콜)
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 
  # 소켓 주소 재사용 옵션 설정 (서버 종료 후 바로 재시작 시 주소 이미 사용 중 오류 방지)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
 
  # 소켓에 IP 주소와 포트 번호 할당 (바인딩)
server_socket.bind((HOST, PORT))
 
  # 클라이언트 연결 요청을 기다리는 상태로 전환 (최대 5개 연결 동시 대기 가능)
server_socket.listen(5) 
print(f"[*] 채팅 서버가 {HOST}:{PORT} 에서 시작되었습니다. 클라이언트의 접속을 기다립니다...")
 
  # 접속한 클라이언트들을 관리하기 위한 리스트
clients = []
  # 클라이언트 리스트 접근을 동기화하기 위한 락(Lock) 객체
clients_lock = threading.Lock()
 
def broadcast(message, sender_socket):
  """
  메시지를 보낸 클라이언트를 제외한 모든 클라이언트에게 메시지를 전송합니다.
  """
  with clients_lock: # 여러 스레드가 동시에 clients 리스트에 접근하는 것을 방지
    for client_socket_in_list in list(clients): # 반복 중 리스트 변경에 안전하도록 복사본 사용
      if client_socket_in_list != sender_socket: # 메시지를 보낸 클라이언트에게는 다시 보내지 않음 (옵션)
        try:
          client_socket_in_list.send(message)
        except Exception: # 예외 발생 시 (예: 클라이언트 연결 끊김)
          # 오류 발생 시 해당 클라이언트 연결 제거
          clients.remove(client_socket_in_list)
          print(f"[-] 클라이언트({client_socket_in_list.getpeername() if client_socket_in_list.fileno() != -1 else '알수없음'}) 연결이 비정상적으로 종료되었습니다.")
 
 
def handle_client(client_socket, addr):
  """
  개별 클라이언트의 연결을 처리하고 메시지를 수신/브로드캐스트합니다.
  """
  print(f"[+] 새로운 클라이언트({addr})가 접속했습니다.")
  
  # 새로운 클라이언트를 리스트에 추가
  with clients_lock:
    clients.append(client_socket)
 
  try:
    while True:
      # 클라이언트로부터 메시지 수신 (최대 1024바이트)
      # recv()는 데이터가 올 때까지 기다립니다 (블로킹 함수).
      message = client_socket.recv(1024)
      if not message: # 클라이언트가 연결을 정상적으로 종료하면 빈 메시지가 수신됨
        break # 반복문 종료
      
      # 받은 메시지 화면에 출력 (서버 콘솔)
      # 실제 서비스에서는 발신자 정보 등을 포함하여 가공 후 브로드캐스트
      decoded_message = message.decode('utf-8')
      print(f"[{addr}:{addr}] {decoded_message}") 
      
      # 메시지를 보낸 클라이언트의 주소 정보와 메시지를 함께 브로드캐스트
      # 모든 클라이언트에게 [주소:포트] 메시지 형식으로 전달
      formatted_message_for_broadcast = f"[{addr}:{addr}] {decoded_message}".encode('utf-8')
      broadcast(formatted_message_for_broadcast, client_socket) # 자기 자신 제외하고 발송
 
  except ConnectionResetError: # 클라이언트가 강제로 연결을 끊었을 때 발생하는 오류
    print(f"[-] 클라이언트({addr}) 연결이 강제로 리셋되었습니다.")
  except Exception as e: # 기타 예외 처리
    print(f"[!] 오류 발생 ({addr}): {e}")
  finally:
    # 클라이언트 연결 종료 처리
    print(f"[-] 클라이언트({addr}) 접속이 종료되었습니다.")
    with clients_lock:
      if client_socket in clients:
        clients.remove(client_socket)
    client_socket.close() # 소켓 닫기
 
  # 메인 서버 로직
try:
  while True:
    # 클라이언트의 연결 요청 수락 (연결되면 새로운 소켓과 주소 정보 반환)
    # accept()는 블로킹 함수로, 클라이언트가 접속할 때까지 기다립니다.
    client_socket, addr = server_socket.accept()
    
    # 각 클라이언트를 처리하기 위한 새로운 스레드 생성 및 시작
    # args에는 handle_client 함수에 전달할 인자들을 튜플 형태로 전달합니다.
    client_handler_thread = threading.Thread(target=handle_client, args=(client_socket, addr))
    client_handler_thread.daemon = True # 메인 프로그램 종료 시 스레드도 함께 종료되도록 설정
    client_handler_thread.start()
except KeyboardInterrupt: # Ctrl+C 입력 시 서버 종료
  print("\n[*] 채팅 서버를 종료합니다.")
finally:
  # 모든 클라이언트 소켓 닫기 (선택적, 스레드가 각자 닫지만 안전을 위해)
  with clients_lock:
    for client_sock in clients:
      client_sock.close()
  server_socket.close() # 서버 소켓 닫기
  print("[*] 모든 연결이 종료되었습니다.")
전체 클라이언트 코드 보기 (python_tcp_client.py)
import socket # 소켓 프로그래밍 모듈
import threading # 메시지 수신을 위한 스레딩 모듈
 
  # 접속할 서버 정보
SERVER_HOST = '127.0.0.1' # 서버 IP 주소 (서버와 같은 컴퓨터에서 실행 시)
SERVER_PORT = 9999    # 서버 포트 번호
 
def receive_messages(client_socket):
  """
  서버로부터 메시지를 계속 수신하여 화면에 출력합니다.
  """
  try:
    while True:
      message = client_socket.recv(1024) # 서버로부터 메시지 수신 (최대 1024 바이트)
      if not message: # 서버가 연결을 끊거나, 문제가 발생한 경우
        print("\n[!] 서버와의 연결이 끊어졌습니다. 엔터를 눌러 종료하세요.")
        break # 수신 스레드 종료
      print(message.decode('utf-8')) # 수신 메시지 화면에 출력
  except ConnectionAbortedError: # 사용자가 강제 종료 (예: 소켓 닫힘)
    print("\n[!] 연결이 중단되었습니다.")
  except ConnectionResetError: # 서버가 강제로 연결을 끊은 경우
    print("\n[!] 서버에 의해 연결이 강제로 종료되었습니다. 엔터를 눌러 종료하세요.")
  except Exception as e: # 기타 예외
    print(f"\n[!] 메시지 수신 중 오류 발생: {e}")
  finally:
    # 소켓이 이미 닫히지 않았다면 닫아줍니다. (메인 스레드에서 닫을 수도 있음)
    if client_socket.fileno() != -1: 
      client_socket.close()
 
 
  # 1. 클라이언트 소켓 생성
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 
try:
  # 2. 서버에 연결 시도
  client_socket.connect((SERVER_HOST, SERVER_PORT))
  print(f"[*] 채팅 서버({SERVER_HOST}:{SERVER_PORT})에 연결되었습니다. 메시지를 입력하세요 (종료: exit).")
 
  # 3. 서버로부터 메시지를 수신할 스레드 생성 및 시작
  receive_thread = threading.Thread(target=receive_messages, args=(client_socket,))
  receive_thread.daemon = True # 메인 프로그램 종료 시 스레드도 함께 종료
  receive_thread.start()
 
  # 4. 사용자로부터 메시지를 입력받아 서버로 전송
  while True:
    message_to_send = input() # 사용자 입력 대기 (이 부분이 블로킹될 수 있음)
    if message_to_send.lower() == "exit": # 'exit' 입력 시 종료
      print("[*] 채팅을 종료합니다.")
      break
    
    # 소켓이 아직 유효한지 (수신 스레드에서 닫히지 않았는지) 확인 후 전송
    if client_socket.fileno() == -1:
      print("[!] 이미 연결이 종료되었습니다.")
      break
      
    try:
      client_socket.send(message_to_send.encode('utf-8')) # 메시지 인코딩하여 서버로 전송
    except BrokenPipeError: # 서버와의 연결이 이미 끊어진 상태에서 보내려고 할 때
      print("[!] 메시지를 보낼 수 없습니다. 서버와의 연결이 끊어졌습니다.")
      break
    except Exception as e:
      print(f"[!] 메시지 전송 중 오류 발생: {e}")
      break
 
 
except ConnectionRefusedError:
  print(f"[!] 서버({SERVER_HOST}:{SERVER_PORT})에 연결할 수 없습니다. 서버가 실행 중인지 확인하세요.")
except Exception as e:
  print(f"[!] 프로그램 실행 중 오류 발생: {e}")
finally:
  # 프로그램 종료 시 소켓이 아직 열려있다면 닫습니다.
  if client_socket.fileno() != -1:
    client_socket.close() 
오늘은 Python과 TCP 소켓 프로그래밍을 이용하여 직접 간단한 멀티 클라이언트 채팅 애플리케이션을 만들어보았습니다.
서버가 어떻게 클라이언트의 연결을 받고, 스레드를 이용해 여러 클라이언트와 동시에 통신하며 메시지를 주고받는지 그 과정을 코드로 직접 경험해 보았습니다.

다음 시간에는 우리가 만든 애플리케이션에 HTTPS를 적용하여 보안을 강화하는 방법에 대해 알아보겠습니다.

참고 🔗