다중 클라이언트 및 오류 처리
실제 애플리케이션에서 서버는 일반적으로 여러 클라이언트를 동시에 처리하고 다양한 오류 조건을 적절하게 관리해야 합니다. 이러한 점을 염두에 두고 구현을 개선해 보겠습니다.
동시 클라이언트 처리 이해
여러 클라이언트를 동시적으로 처리하는 방법에는 여러 가지가 있습니다.
- 스레딩 (Threading): 각 클라이언트 연결에 대해 새 스레드를 생성합니다.
- 프로세스 기반 (Process-based): 각 클라이언트에 대해 새 프로세스를 생성합니다.
- 비동기 I/O (Asynchronous I/O): 이벤트 루프와 함께 논블로킹 I/O 를 사용합니다.
이 랩에서는 이해하고 구현하기 비교적 쉬운 스레딩 기반 접근 방식을 구현합니다.
다중 클라이언트를 위한 서버 개선
스레드를 사용하여 여러 클라이언트를 처리하도록 서버를 수정해 보겠습니다.
- WebIDE 에서
server.py 파일을 열고 코드를 다음으로 바꿉니다.
import socket
import threading
def handle_client(client_socket, client_address):
"""Handle communication with a single client"""
try:
print(f"[NEW CONNECTION] {client_address} connected.")
while True:
## Receive client data
try:
data = client_socket.recv(1024)
if not data:
break ## Client disconnected
message = data.decode()
print(f"[{client_address}] {message}")
## Send response
response = f"Message '{message}' received successfully"
client_socket.send(response.encode())
except ConnectionResetError:
print(f"[{client_address}] Connection reset by client")
break
except Exception as e:
print(f"[ERROR] {e}")
finally:
## Clean up when client disconnects
client_socket.close()
print(f"[DISCONNECTED] {client_address} disconnected")
def start_server():
"""Start the server and listen for connections"""
## Server configuration
host = '127.0.0.1'
port = 12345
## Create socket
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
## Set socket option to reuse address
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
## Bind to host and port
server_socket.bind((host, port))
## Listen for connections
server_socket.listen(5)
print(f"[STARTING] Server is listening on {host}:{port}")
while True:
## Accept client connection
client_socket, client_address = server_socket.accept()
## Create a new thread to handle the client
client_thread = threading.Thread(
target=handle_client,
args=(client_socket, client_address)
)
client_thread.daemon = True ## Thread will close when main program exits
client_thread.start()
## Display active connections
print(f"[ACTIVE CONNECTIONS] {threading.active_count() - 1}")
except KeyboardInterrupt:
print("\n[SHUTTING DOWN] Server is shutting down...")
except Exception as e:
print(f"[ERROR] {e}")
finally:
server_socket.close()
print("[CLOSED] Server socket closed")
if __name__ == "__main__":
start_server()
- 파일을 저장합니다.
오류 처리를 통한 클라이언트 개선
또한 더 나은 오류 처리를 통해 클라이언트를 개선해 보겠습니다.
- WebIDE 에서
client.py 파일을 열고 코드를 다음으로 바꿉니다.
import socket
import sys
import time
def start_client():
"""Start a client that connects to the server"""
## Server information
host = '127.0.0.1'
port = 12345
## Create socket
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
## Set a timeout for connection attempts (5 seconds)
client_socket.settimeout(5)
try:
## Connect to server
print(f"[CONNECTING] Connecting to server at {host}:{port}...")
client_socket.connect((host, port))
## Reset timeout to none for regular communication
client_socket.settimeout(None)
print("[CONNECTED] Connected to server")
## Communication loop
while True:
## Get user input
message = input("\nEnter message (or 'quit' to exit): ")
if message.lower() == 'quit':
print("[CLOSING] Closing connection by request...")
break
try:
## Send message
client_socket.send(message.encode())
## Wait for response
response = client_socket.recv(1024)
print(f"[RESPONSE] {response.decode()}")
except ConnectionResetError:
print("[ERROR] Connection was reset by the server")
break
except ConnectionAbortedError:
print("[ERROR] Connection was aborted")
break
except Exception as e:
print(f"[ERROR] {e}")
break
except socket.timeout:
print("[TIMEOUT] Connection attempt timed out. Is the server running?")
except ConnectionRefusedError:
print("[REFUSED] Connection refused. Make sure the server is running.")
except KeyboardInterrupt:
print("\n[INTERRUPT] Client shutting down...")
except Exception as e:
print(f"[ERROR] {e}")
finally:
## Close socket
try:
client_socket.close()
print("[DISCONNECTED] Disconnected from server")
except:
pass
if __name__ == "__main__":
start_client()
- 파일을 저장합니다.
개선된 코드 이해
서버 개선 사항:
- 여러 클라이언트를 동시적으로 처리하기 위해
threading 모듈을 추가했습니다.
- 각 클라이언트 연결은 이제 별도의 스레드에서 처리됩니다.
- 보다 구체적인 예외 처리를 통해 오류 처리를 개선했습니다.
- 활성 클라이언트 연결 수를 표시합니다.
- 스레드는 "데몬 (daemon)"으로 설정되어 주 프로그램이 종료되면 자동으로 닫힙니다.
클라이언트 개선 사항:
- 서버를 사용할 수 없는 경우 멈추는 것을 방지하기 위해 연결 시간 초과를 추가했습니다.
- 다양한 네트워크 오류에 대한 특정 예외 처리를 통해 오류 처리를 개선했습니다.
- 더 명확한 서식으로 더 설명적인 상태 메시지를 추가했습니다.
다중 클라이언트 서버 테스트
개선된 애플리케이션을 테스트해 보겠습니다.
- 서버를 시작합니다.
python3 ~/project/socket_lab/server.py
다음과 같은 출력을 볼 수 있습니다.
[STARTING] Server is listening on 127.0.0.1:12345
- 새 터미널에서 클라이언트를 시작합니다.
python3 ~/project/socket_lab/client.py
- 세 번째 터미널에서 다른 클라이언트를 시작합니다.
python3 ~/project/socket_lab/client.py
- 서버 터미널에서 두 연결을 모두 볼 수 있습니다.
[NEW CONNECTION] ('127.0.0.1', 59124) connected.
[ACTIVE CONNECTIONS] 1
[NEW CONNECTION] ('127.0.0.1', 59126) connected.
[ACTIVE CONNECTIONS] 2
- 두 클라이언트에서 메시지를 보내고 서버가 이를 수신하는 것을 관찰합니다.
[('127.0.0.1', 59124)] Hello from client 1
[('127.0.0.1', 59126)] Hello from client 2
- 완료되면 각 클라이언트에서 'quit'을 입력하여 연결을 끊거나 서버 터미널에서 Ctrl+C 를 눌러 서버를 종료합니다.
서버가 실행되지 않는 경우 처리
서버가 실행되지 않는 경우 어떻게 되는지 테스트해 보겠습니다.
-
서버가 중지되었는지 확인합니다 (실행 중인 경우 Ctrl+C 를 누릅니다).
-
클라이언트를 실행해 봅니다.
python3 ~/project/socket_lab/client.py
다음과 같은 출력을 볼 수 있습니다.
[CONNECTING] Connecting to server at 127.0.0.1:12345...
[REFUSED] Connection refused. Make sure the server is running.
[DISCONNECTED] Disconnected from server
이제 클라이언트는 오류를 적절하게 처리하여 사용자에게 서버가 실행되지 않을 수 있음을 알립니다.
이러한 개선 사항을 통해 여러 클라이언트와 다양한 오류 조건을 처리할 수 있는 강력한 클라이언트 - 서버 시스템을 만들었습니다. 이는 더 복잡한 네트워크 애플리케이션을 개발하기 위한 견고한 기반입니다.