전체 코드는 아래 결론 부분에서 확인할 수 있습니다.
python_tcp_server.py
라는 이름으로 아래 코드를 작성해 주세요.socket
모듈과, 여러 클라이언트의 요청을 동시에 처리하기 위한 threading
모듈을 가져옵니다.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} 에서 시작되었습니다. 클라이언트의 접속을 기다립니다...")
socket.SOCK_DGRAM
) # 접속한 클라이언트들을 관리하기 위한 리스트
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() # 소켓 닫기
clients
리스트를 수정하려고 할 때 발생할 수 있는 문제를 막기 위한 장치(락)입니다.sender_socket
를 인자로 받아 구분)broadcast()
함수를 통해 다른 클라이언트들에게 전달합니다. # 메인 서버 로직
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("[*] 모든 연결이 종료되었습니다.")
client_socket
)과 클라이언트의 주소 정보(addr
)를 반환합니다.handle_client
함수를 실행할 새로운 스레드를 만듭니다.
이렇게 해야 여러 클라이언트가 동시에 접속해도 서버가 멈추지 않고 각 클라이언트의 요청을 독립적으로 처리할 수 있습니다.socket
모듈과 threading
모듈이 필요합니다. 접속할 서버의 IP 주소와 포트 번호도 설정합니다.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()
receive_messages()
함수: 서버로부터 오는 메시지를 계속 받아서 화면에 출력합니다. 사용자가 메시지를 입력하는 동안에도 서버 메시지를 받아야 하므로, 이 함수는 별도의 스레드에서 동작합니다.client_socket.connect()
: 서버에 연결을 시도합니다.input()
: 사용자로부터 콘솔 입력을 받습니다.client_socket.send()
: 입력받은 메시지를 서버로 전송합니다.python_tcp_server.py
(또는 원하는 이름으로 저장) 파일을 파이썬으로 실행합니다.
터미널에 "채팅 서버가 ... 시작되었습니다..." 메시지가 뜨면 성공입니다.python_tcp_client.py
파일을 실행합니다.
새 터미널 창을 열어서 실행해도 되고, 다른 컴퓨터에서 실행해도 됩니다 (이 경우 SERVER_HOST
를 서버 컴퓨터의 실제 IP 주소로 변경해야 합니다).python_tcp_client.py
파일을 여러 개 실행하여 다수의 클라이언트를 서버에 접속시킬 수 있습니다.exit
를 입력하면 해당 클라이언트가 종료됩니다.
서버는 Ctrl+C를 눌러 종료할 수 있습니다.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("[*] 모든 연결이 종료되었습니다.")
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()
다음 시간에는 우리가 만든 애플리케이션에 HTTPS를 적용하여 보안을 강화하는 방법에 대해 알아보겠습니다.