Python 소켓 통신 오류 처리 방법

PythonBeginner
지금 연습하기

소개

Python 의 소켓 통신 모듈은 네트워크 애플리케이션을 구축하기 위한 강력한 도구입니다. 하지만 네트워크 연결을 다루는 것은 종종 애플리케이션의 안정성에 영향을 미칠 수 있는 다양한 문제와 잠재적인 오류를 야기합니다. 이 실습에서는 Python 소켓 프로그래밍의 기본 사항을 살펴보고 효과적인 오류 처리 기술을 구현하는 방법을 안내합니다.

이 튜토리얼을 마치면 일반적인 네트워크 통신 오류를 이해하고 연결 문제, 타임아웃 및 기타 네트워크 관련 문제를 유연하게 관리할 수 있는 탄력적인 소켓 기반 애플리케이션을 구축하는 방법을 알게 될 것입니다.

Python 소켓과 기본 통신 이해

소켓이 무엇이며 Python 에서 어떻게 작동하는지 이해하는 것부터 시작해 보겠습니다.

소켓이란 무엇인가요?

소켓은 네트워크를 통해 데이터를 송수신하기 위한 종착점 (endpoint) 입니다. 네트워크 통신이 흐르는 가상 연결 지점이라고 생각하면 됩니다. Python 의 내장 socket 모듈은 네트워크 통신을 위해 소켓을 생성, 구성 및 사용하는 도구를 제공합니다.

기본 소켓 통신 흐름

소켓 통신은 일반적으로 다음 단계를 따릅니다.

  1. 소켓 객체 생성
  2. 소켓을 주소에 바인딩 (서버용)
  3. 수신 연결 대기 (서버용)
  4. 연결 수락 (서버용) 또는 서버에 연결 (클라이언트용)
  5. 데이터 송수신
  6. 완료되면 소켓 닫기

이러한 개념을 더 잘 이해하기 위해 첫 번째 간단한 소켓 프로그램을 만들어 보겠습니다.

첫 번째 소켓 서버 만들기

먼저, 연결을 수신하고 수신한 모든 데이터를 다시 에코하는 기본 소켓 서버를 만들어 보겠습니다.

WebIDE 를 열고 /home/labex/project 디렉토리에 server.py라는 새 파일을 만들고 다음 내용을 입력합니다.

import socket

## Define server address and port
HOST = '127.0.0.1'  ## Standard loopback interface address (localhost)
PORT = 65432        ## Port to listen on (non-privileged ports are > 1023)

## Create a socket object
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print(f"Socket created successfully")

## Bind the socket to the specified address and port
server_socket.bind((HOST, PORT))
print(f"Socket bound to {HOST}:{PORT}")

## Listen for incoming connections
server_socket.listen(1)
print(f"Socket is listening for connections")

## Accept a connection
print(f"Waiting for a connection...")
connection, client_address = server_socket.accept()
print(f"Connected to client: {client_address}")

## Receive and echo data
try:
    while True:
        ## Receive data from the client
        data = connection.recv(1024)
        if not data:
            ## If no data is received, the client has disconnected
            print(f"Client disconnected")
            break

        print(f"Received: {data.decode('utf-8')}")

        ## Echo the data back to the client
        connection.sendall(data)
        print(f"Sent: {data.decode('utf-8')}")
finally:
    ## Clean up the connection
    connection.close()
    server_socket.close()
    print(f"Socket closed")

첫 번째 소켓 클라이언트 만들기

이제 서버에 연결할 클라이언트를 만들어 보겠습니다. 동일한 디렉토리에 client.py라는 새 파일을 만들고 다음 내용을 입력합니다.

import socket

## Define server address and port
HOST = '127.0.0.1'  ## The server's hostname or IP address
PORT = 65432        ## The port used by the server

## Create a socket object
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print(f"Socket created successfully")

## Connect to the server
client_socket.connect((HOST, PORT))
print(f"Connected to server at {HOST}:{PORT}")

## Send and receive data
try:
    ## Send data to the server
    message = "Hello, Server!"
    client_socket.sendall(message.encode('utf-8'))
    print(f"Sent: {message}")

    ## Receive data from the server
    data = client_socket.recv(1024)
    print(f"Received: {data.decode('utf-8')}")
finally:
    ## Clean up the connection
    client_socket.close()
    print(f"Socket closed")

소켓 프로그램 테스트

이제 소켓 프로그램을 테스트해 보겠습니다. LabEx VM 에서 두 개의 터미널 창을 엽니다.

첫 번째 터미널에서 서버를 실행합니다.

cd ~/project
python3 server.py

다음과 유사한 출력이 표시됩니다.

Socket created successfully
Socket bound to 127.0.0.1:65432
Socket is listening for connections
Waiting for a connection...

서버를 계속 실행하고 두 번째 터미널을 열어 클라이언트를 실행합니다.

cd ~/project
python3 client.py

다음과 유사한 출력이 표시됩니다.

Socket created successfully
Connected to server at 127.0.0.1:65432
Sent: Hello, Server!
Received: Hello, Server!
Socket closed

그리고 서버 터미널에서는 다음을 볼 수 있습니다.

Connected to client: ('127.0.0.1', XXXXX)
Received: Hello, Server!
Sent: Hello, Server!
Client disconnected
Socket closed

축하합니다! Python 에서 첫 번째 소켓 기반 클라이언트 - 서버 애플리케이션을 만들고 테스트했습니다. 이는 소켓 통신이 어떻게 작동하는지, 그리고 다음 단계에서 오류 처리를 구현하는 방법을 이해하기 위한 기반을 제공합니다.

일반적인 소켓 오류 및 기본 오류 처리

이전 단계에서는 간단한 소켓 서버와 클라이언트를 만들었지만, 소켓 통신 중에 오류가 발생할 경우 어떻게 되는지는 다루지 않았습니다. 네트워크 통신은 본질적으로 신뢰할 수 없으며, 연결 실패에서 예기치 않은 연결 해제에 이르기까지 다양한 문제가 발생할 수 있습니다.

일반적인 소켓 오류

소켓 프로그래밍을 할 때 몇 가지 일반적인 오류가 발생할 수 있습니다.

  1. Connection refused (연결 거부): 클라이언트가 실행 중이지 않거나 지정된 포트에서 수신 대기하지 않는 서버에 연결하려고 할 때 발생합니다.
  2. Connection timeout (연결 시간 초과): 연결 시도가 완료되는 데 너무 오래 걸릴 때 발생합니다.
  3. Address already in use (주소 이미 사용 중): 이미 사용 중인 주소와 포트에 소켓을 바인딩하려고 할 때 발생합니다.
  4. Connection reset (연결 재설정): 피어 (peer) 에 의해 연결이 예기치 않게 닫힐 때 발생합니다.
  5. Network unreachable (네트워크에 연결할 수 없음): 네트워크 인터페이스가 대상 네트워크에 연결할 수 없을 때 발생합니다.

try-except 를 사용한 기본 오류 처리

Python 의 예외 처리 메커니즘은 소켓 통신에서 오류를 관리하는 강력한 방법을 제공합니다. 기본 오류 처리를 포함하도록 클라이언트 및 서버 프로그램을 업데이트해 보겠습니다.

오류 처리가 향상된 소켓 서버

server.py 파일을 다음 코드로 업데이트합니다.

import socket
import sys

## Define server address and port
HOST = '127.0.0.1'
PORT = 65432

## Create a socket object
try:
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    print(f"Socket created successfully")

    ## Allow reuse of address
    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    ## Bind the socket to the specified address and port
    server_socket.bind((HOST, PORT))
    print(f"Socket bound to {HOST}:{PORT}")

    ## Listen for incoming connections
    server_socket.listen(1)
    print(f"Socket is listening for connections")

    ## Accept a connection
    print(f"Waiting for a connection...")
    connection, client_address = server_socket.accept()
    print(f"Connected to client: {client_address}")

    ## Receive and echo data
    try:
        while True:
            ## Receive data from the client
            data = connection.recv(1024)
            if not data:
                ## If no data is received, the client has disconnected
                print(f"Client disconnected")
                break

            print(f"Received: {data.decode('utf-8')}")

            ## Echo the data back to the client
            connection.sendall(data)
            print(f"Sent: {data.decode('utf-8')}")
    except socket.error as e:
        print(f"Socket error occurred: {e}")
    finally:
        ## Clean up the connection
        connection.close()
        print(f"Connection closed")

except socket.error as e:
    print(f"Socket error occurred: {e}")
except KeyboardInterrupt:
    print(f"\nServer shutting down...")
finally:
    ## Clean up the server socket
    if 'server_socket' in locals():
        server_socket.close()
        print(f"Server socket closed")
    sys.exit(0)

오류 처리가 향상된 소켓 클라이언트

client.py 파일을 다음 코드로 업데이트합니다.

import socket
import sys
import time

## Define server address and port
HOST = '127.0.0.1'
PORT = 65432

## Create a socket object
try:
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    print(f"Socket created successfully")

    ## Set a timeout for connection attempts
    client_socket.settimeout(5)
    print(f"Socket timeout set to 5 seconds")

    ## Connect to the server
    try:
        print(f"Attempting to connect to server at {HOST}:{PORT}...")
        client_socket.connect((HOST, PORT))
        print(f"Connected to server at {HOST}:{PORT}")

        ## Send and receive data
        try:
            ## Send data to the server
            message = "Hello, Server!"
            client_socket.sendall(message.encode('utf-8'))
            print(f"Sent: {message}")

            ## Receive data from the server
            data = client_socket.recv(1024)
            print(f"Received: {data.decode('utf-8')}")

        except socket.error as e:
            print(f"Error during data exchange: {e}")

    except socket.timeout:
        print(f"Connection attempt timed out")
    except ConnectionRefusedError:
        print(f"Connection refused. Make sure the server is running.")
    except socket.error as e:
        print(f"Connection error: {e}")

except socket.error as e:
    print(f"Socket creation error: {e}")
except KeyboardInterrupt:
    print(f"\nClient shutting down...")
finally:
    ## Clean up the connection
    if 'client_socket' in locals():
        client_socket.close()
        print(f"Socket closed")
    sys.exit(0)

오류 처리 테스트

이제 오류 처리를 테스트해 보겠습니다. 일반적인 오류인 실행 중이지 않은 서버에 연결하려고 시도하는 것을 시연해 보겠습니다.

  1. 먼저 서버가 실행 중이지 않은지 확인합니다 (실행 중인 경우 닫습니다).

  2. 클라이언트를 실행합니다.

    cd ~/project
    python3 client.py
    

    다음과 유사한 출력이 표시됩니다.

    Socket created successfully
    Socket timeout set to 5 seconds
    Attempting to connect to server at 127.0.0.1:65432...
    Connection refused. Make sure the server is running.
    Socket closed
    
  3. 이제 한 터미널에서 서버를 시작합니다.

    cd ~/project
    python3 server.py
    
  4. 다른 터미널에서 클라이언트를 실행합니다.

    cd ~/project
    python3 client.py
    

    연결이 성공해야 하며 클라이언트와 서버 모두에서 예상되는 출력이 표시됩니다.

오류 처리 코드 이해

추가한 주요 오류 처리 구성 요소를 살펴보겠습니다.

  1. Outer try-except block (외부 try-except 블록): 소켓 생성 및 일반적인 오류를 처리합니다.
  2. Connection try-except block (연결 try-except 블록): 특히 연결 관련 오류를 처리합니다.
  3. Data exchange try-except block (데이터 교환 try-except 블록): 데이터 송수신 중 오류를 처리합니다.
  4. finally block (finally 블록): 오류 발생 여부에 관계없이 리소스가 적절하게 정리되도록 합니다.
  5. socket.settimeout(): connect() 와 같은 작업에 대한 시간 초과 기간을 설정하여 무한 대기를 방지합니다.
  6. socket.setsockopt(): 서버가 닫힌 직후 주소를 재사용할 수 있도록 SO_REUSEADDR 과 같은 소켓 옵션을 설정합니다.

이러한 개선 사항은 오류를 적절하게 처리하고 리소스가 올바르게 정리되도록 하여 소켓 프로그램을 더욱 강력하게 만듭니다.

고급 오류 처리 기술

이제 기본적인 오류 처리를 이해했으므로 소켓 애플리케이션을 더욱 강력하게 만드는 몇 가지 고급 기술을 살펴보겠습니다. 이 단계에서는 다음을 구현합니다.

  1. 연결 실패에 대한 재시도 메커니즘
  2. 예기치 않은 연결 해제에 대한 정상적인 처리
  3. 더 나은 오류 추적을 위한 로깅 통합

재시도 메커니즘을 갖춘 클라이언트 만들기

실패할 경우 자동으로 연결을 재시도하는 향상된 클라이언트를 만들어 보겠습니다. /home/labex/project 디렉토리에 retry_client.py라는 새 파일을 만듭니다.

import socket
import sys
import time

## Define server address and port
HOST = '127.0.0.1'
PORT = 65432

## Configure retry parameters
MAX_RETRIES = 3
RETRY_DELAY = 2  ## seconds

def connect_with_retry(host, port, max_retries, retry_delay):
    """Attempt to connect to a server with retry mechanism"""
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client_socket.settimeout(5)  ## Set timeout for connection attempts

    print(f"Socket created successfully")
    print(f"Socket timeout set to 5 seconds")

    attempt = 0
    while attempt < max_retries:
        attempt += 1
        try:
            print(f"Connection attempt {attempt}/{max_retries}...")
            client_socket.connect((host, port))
            print(f"Connected to server at {host}:{port}")
            return client_socket
        except socket.timeout:
            print(f"Connection attempt timed out")
        except ConnectionRefusedError:
            print(f"Connection refused. Make sure the server is running.")
        except socket.error as e:
            print(f"Connection error: {e}")

        if attempt < max_retries:
            print(f"Retrying in {retry_delay} seconds...")
            time.sleep(retry_delay)

    ## If we get here, all connection attempts failed
    print(f"Failed to connect after {max_retries} attempts")
    client_socket.close()
    return None

try:
    ## Attempt to connect with retry
    client_socket = connect_with_retry(HOST, PORT, MAX_RETRIES, RETRY_DELAY)

    ## Proceed if connection was successful
    if client_socket:
        try:
            ## Send data to the server
            message = "Hello, Server with Retry!"
            client_socket.sendall(message.encode('utf-8'))
            print(f"Sent: {message}")

            ## Receive data from the server
            data = client_socket.recv(1024)
            print(f"Received: {data.decode('utf-8')}")

        except socket.error as e:
            print(f"Error during data exchange: {e}")
        finally:
            ## Clean up the connection
            client_socket.close()
            print(f"Socket closed")

except KeyboardInterrupt:
    print(f"\nClient shutting down...")
    if 'client_socket' in locals() and client_socket:
        client_socket.close()
        print(f"Socket closed")
    sys.exit(0)

여러 클라이언트와 연결 해제를 처리하는 서버 만들기

여러 클라이언트를 처리하고 연결 해제를 정상적으로 처리할 수 있는 향상된 서버를 만들어 보겠습니다. 동일한 디렉토리에 robust_server.py라는 새 파일을 만듭니다.

import socket
import sys
import time

## Define server address and port
HOST = '127.0.0.1'
PORT = 65432

def handle_client(client_socket, client_address):
    """Handle a client connection"""
    print(f"Handling connection from {client_address}")

    try:
        ## Set a timeout for receiving data
        client_socket.settimeout(30)  ## 30 seconds timeout for inactivity

        ## Receive and echo data
        while True:
            try:
                ## Receive data from the client
                data = client_socket.recv(1024)
                if not data:
                    ## If no data is received, the client has disconnected
                    print(f"Client {client_address} disconnected gracefully")
                    break

                print(f"Received from {client_address}: {data.decode('utf-8')}")

                ## Echo the data back to the client
                client_socket.sendall(data)
                print(f"Sent to {client_address}: {data.decode('utf-8')}")

            except socket.timeout:
                print(f"Connection with {client_address} timed out due to inactivity")
                break
            except ConnectionResetError:
                print(f"Connection with {client_address} was reset by the client")
                break
            except socket.error as e:
                print(f"Error with client {client_address}: {e}")
                break
    finally:
        ## Clean up the connection
        client_socket.close()
        print(f"Connection with {client_address} closed")

try:
    ## Create a socket object
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    print(f"Socket created successfully")

    ## Allow reuse of address
    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    ## Bind the socket to the specified address and port
    server_socket.bind((HOST, PORT))
    print(f"Socket bound to {HOST}:{PORT}")

    ## Listen for incoming connections
    server_socket.listen(5)  ## Allow up to 5 pending connections
    print(f"Socket is listening for connections")

    ## Set timeout for accept operation
    server_socket.settimeout(60)  ## 60 seconds timeout for accept

    ## Accept connections and handle them
    while True:
        try:
            print(f"Waiting for a connection...")
            client_socket, client_address = server_socket.accept()
            print(f"Connected to client: {client_address}")

            ## Handle this client
            handle_client(client_socket, client_address)

        except socket.timeout:
            print(f"No connections received in the last 60 seconds, still waiting...")
        except socket.error as e:
            print(f"Error accepting connection: {e}")
            ## Small delay to prevent CPU hogging in case of persistent errors
            time.sleep(1)

except socket.error as e:
    print(f"Socket error occurred: {e}")
except KeyboardInterrupt:
    print(f"\nServer shutting down...")
finally:
    ## Clean up the server socket
    if 'server_socket' in locals():
        server_socket.close()
        print(f"Server socket closed")
    sys.exit(0)

더 나은 오류 추적을 위한 로깅 통합

적절한 로깅 기능을 갖춘 서버를 만들어 보겠습니다. 동일한 디렉토리에 logging_server.py라는 새 파일을 만듭니다.

import socket
import sys
import time
import logging

## Configure logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler("server_log.txt"),
        logging.StreamHandler()
    ]
)

## Define server address and port
HOST = '127.0.0.1'
PORT = 65432

def handle_client(client_socket, client_address):
    """Handle a client connection with logging"""
    logging.info(f"Handling connection from {client_address}")

    try:
        ## Set a timeout for receiving data
        client_socket.settimeout(30)  ## 30 seconds timeout for inactivity

        ## Receive and echo data
        while True:
            try:
                ## Receive data from the client
                data = client_socket.recv(1024)
                if not data:
                    ## If no data is received, the client has disconnected
                    logging.info(f"Client {client_address} disconnected gracefully")
                    break

                logging.info(f"Received from {client_address}: {data.decode('utf-8')}")

                ## Echo the data back to the client
                client_socket.sendall(data)
                logging.info(f"Sent to {client_address}: {data.decode('utf-8')}")

            except socket.timeout:
                logging.warning(f"Connection with {client_address} timed out due to inactivity")
                break
            except ConnectionResetError:
                logging.error(f"Connection with {client_address} was reset by the client")
                break
            except socket.error as e:
                logging.error(f"Error with client {client_address}: {e}")
                break
    finally:
        ## Clean up the connection
        client_socket.close()
        logging.info(f"Connection with {client_address} closed")

try:
    ## Create a socket object
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    logging.info(f"Socket created successfully")

    ## Allow reuse of address
    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    ## Bind the socket to the specified address and port
    server_socket.bind((HOST, PORT))
    logging.info(f"Socket bound to {HOST}:{PORT}")

    ## Listen for incoming connections
    server_socket.listen(5)  ## Allow up to 5 pending connections
    logging.info(f"Socket is listening for connections")

    ## Set timeout for accept operation
    server_socket.settimeout(60)  ## 60 seconds timeout for accept

    ## Accept connections and handle them
    while True:
        try:
            logging.info(f"Waiting for a connection...")
            client_socket, client_address = server_socket.accept()
            logging.info(f"Connected to client: {client_address}")

            ## Handle this client
            handle_client(client_socket, client_address)

        except socket.timeout:
            logging.info(f"No connections received in the last 60 seconds, still waiting...")
        except socket.error as e:
            logging.error(f"Error accepting connection: {e}")
            ## Small delay to prevent CPU hogging in case of persistent errors
            time.sleep(1)

except socket.error as e:
    logging.critical(f"Socket error occurred: {e}")
except KeyboardInterrupt:
    logging.info(f"Server shutting down...")
finally:
    ## Clean up the server socket
    if 'server_socket' in locals():
        server_socket.close()
        logging.info(f"Server socket closed")
    sys.exit(0)

고급 오류 처리 테스트

고급 오류 처리 구현을 테스트해 보겠습니다.

  1. 서버 없이 재시도 클라이언트를 실행하여 재시도 메커니즘을 테스트합니다.

    cd ~/project
    python3 retry_client.py
    

    클라이언트가 여러 번 연결을 시도하는 것을 볼 수 있습니다.

    Socket created successfully
    Socket timeout set to 5 seconds
    Connection attempt 1/3...
    Connection refused. Make sure the server is running.
    Retrying in 2 seconds...
    Connection attempt 2/3...
    Connection refused. Make sure the server is running.
    Retrying in 2 seconds...
    Connection attempt 3/3...
    Connection refused. Make sure the server is running.
    Failed to connect after 3 attempts
    
  2. 강력한 서버를 시작하고 재시도 클라이언트로 연결을 시도합니다.

    ## Terminal 1
    cd ~/project
    python3 robust_server.py
    
    ## Terminal 2
    cd ~/project
    python3 retry_client.py
    

    성공적인 연결과 데이터 교환을 볼 수 있습니다.

  3. 로깅 서버를 테스트하여 로그가 기록되는 방식을 확인합니다.

    ## Terminal 1
    cd ~/project
    python3 logging_server.py
    
    ## Terminal 2
    cd ~/project
    python3 client.py
    

    교환 후 로그 파일을 확인할 수 있습니다.

    cat ~/project/server_log.txt
    

    연결 및 데이터 교환에 대한 자세한 로그를 볼 수 있습니다.

주요 고급 오류 처리 기술

이러한 예제에서는 몇 가지 고급 오류 처리 기술을 구현했습니다.

  1. Retry mechanisms (재시도 메커니즘): 실패한 작업을 설정된 횟수만큼 시도 간 지연을 두고 자동으로 재시도합니다.
  2. Timeout settings (시간 초과 설정): 무한 대기를 방지하기 위해 소켓 작업에 시간 초과를 설정합니다.
  3. Detailed error handling (자세한 오류 처리): 특정 소켓 예외를 포착하고 적절하게 처리합니다.
  4. Structured logging (구조화된 로깅): Python 의 로깅 모듈을 사용하여 오류 및 작업에 대한 자세한 정보를 기록합니다.
  5. Resource cleanup (리소스 정리): 오류 조건에서도 모든 리소스가 제대로 닫히도록 합니다.

이러한 기술은 광범위한 오류 조건을 정상적으로 처리할 수 있는 보다 강력한 소켓 애플리케이션을 만드는 데 도움이 됩니다.

완전한 오류 복구 소켓 애플리케이션 만들기

이 마지막 단계에서는 지금까지 배운 모든 것을 결합하여 완전하고 오류 복구 기능이 있는 소켓 애플리케이션을 만들 것입니다. 모든 수준에서 적절한 오류 처리를 갖춘 간단한 채팅 시스템을 구축할 것입니다.

채팅 애플리케이션 아키텍처

채팅 애플리케이션은 다음과 같이 구성됩니다.

  1. 여러 클라이언트를 처리할 수 있는 서버
  2. 메시지를 보내고 받을 수 있는 클라이언트
  3. 전반적인 강력한 오류 처리
  4. 적절한 리소스 관리
  5. 진단을 위한 로깅

채팅 서버 만들기

/home/labex/project 디렉토리에 chat_server.py라는 새 파일을 만듭니다.

import socket
import sys
import threading
import logging
import time

## Configure logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler("chat_server_log.txt"),
        logging.StreamHandler()
    ]
)

## Define server address and port
HOST = '127.0.0.1'
PORT = 65432

## Store active client connections
clients = {}
clients_lock = threading.Lock()

def broadcast(message, sender_address=None):
    """Send a message to all connected clients except the sender"""
    with clients_lock:
        for client_address, client_socket in list(clients.items()):
            ## Don't send the message back to the sender
            if client_address != sender_address:
                try:
                    client_socket.sendall(message)
                except socket.error:
                    ## If sending fails, the client will be removed in the client handler
                    pass

def handle_client(client_socket, client_address):
    """Handle a client connection"""
    client_id = f"{client_address[0]}:{client_address[1]}"
    logging.info(f"New client connected: {client_id}")

    ## Register the new client
    with clients_lock:
        clients[client_address] = client_socket

    ## Notify all clients about the new connection
    broadcast(f"SERVER: Client {client_id} has joined the chat.".encode('utf-8'))

    try:
        ## Set a timeout for receiving data
        client_socket.settimeout(300)  ## 5 minutes timeout for inactivity

        ## Handle client messages
        while True:
            try:
                ## Receive data from the client
                data = client_socket.recv(1024)
                if not data:
                    ## If no data is received, the client has disconnected
                    break

                message = data.decode('utf-8')
                logging.info(f"Message from {client_id}: {message}")

                ## Broadcast the message to all other clients
                broadcast_message = f"{client_id}: {message}".encode('utf-8')
                broadcast(broadcast_message, client_address)

            except socket.timeout:
                logging.warning(f"Client {client_id} timed out due to inactivity")
                client_socket.sendall("SERVER: You have been disconnected due to inactivity.".encode('utf-8'))
                break
            except ConnectionResetError:
                logging.error(f"Connection with client {client_id} was reset")
                break
            except socket.error as e:
                logging.error(f"Error with client {client_id}: {e}")
                break
    finally:
        ## Remove client from active clients
        with clients_lock:
            if client_address in clients:
                del clients[client_address]

        ## Close the client socket
        client_socket.close()
        logging.info(f"Connection with client {client_id} closed")

        ## Notify all clients about the disconnection
        broadcast(f"SERVER: Client {client_id} has left the chat.".encode('utf-8'))

def main():
    """Main server function"""
    try:
        ## Create a socket object
        server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        logging.info("Socket created successfully")

        ## Allow reuse of address
        server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

        ## Bind the socket to the specified address and port
        server_socket.bind((HOST, PORT))
        logging.info(f"Socket bound to {HOST}:{PORT}")

        ## Listen for incoming connections
        server_socket.listen(5)  ## Allow up to 5 pending connections
        logging.info("Socket is listening for connections")

        ## Accept connections and handle them
        while True:
            try:
                ## Accept a new client connection
                client_socket, client_address = server_socket.accept()

                ## Start a new thread to handle the client
                client_thread = threading.Thread(
                    target=handle_client,
                    args=(client_socket, client_address)
                )
                client_thread.daemon = True
                client_thread.start()

            except socket.error as e:
                logging.error(f"Error accepting connection: {e}")
                time.sleep(1)  ## Small delay to prevent CPU hogging

    except socket.error as e:
        logging.critical(f"Socket error occurred: {e}")
    except KeyboardInterrupt:
        logging.info("Server shutting down...")
    finally:
        ## Clean up and close all client connections
        with clients_lock:
            for client_socket in clients.values():
                try:
                    client_socket.close()
                except:
                    pass
            clients.clear()

        ## Close the server socket
        if 'server_socket' in locals():
            server_socket.close()
            logging.info("Server socket closed")

        logging.info("Server shutdown complete")

if __name__ == "__main__":
    main()

채팅 클라이언트 만들기

동일한 디렉토리에 chat_client.py라는 새 파일을 만듭니다.

import socket
import sys
import threading
import logging
import time

## Configure logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler("chat_client_log.txt"),
        logging.StreamHandler(sys.stdout)
    ]
)

## Define server address and port
HOST = '127.0.0.1'
PORT = 65432

## Flag to indicate if the client is running
running = True

def receive_messages(client_socket):
    """Receive and display messages from the server"""
    global running

    while running:
        try:
            ## Receive data from the server
            data = client_socket.recv(1024)
            if not data:
                logging.warning("Server has closed the connection")
                running = False
                break

            ## Display the received message
            message = data.decode('utf-8')
            print(f"\n{message}")
            print("Your message: ", end='', flush=True)

        except socket.timeout:
            ## Socket timeout - just continue and check if we're still running
            continue
        except ConnectionResetError:
            logging.error("Connection was reset by the server")
            running = False
            break
        except socket.error as e:
            logging.error(f"Socket error: {e}")
            running = False
            break

    logging.info("Message receiver stopped")

def connect_to_server(host, port, max_retries=3, retry_delay=2):
    """Connect to the chat server with retry mechanism"""
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client_socket.settimeout(5)  ## Set timeout for connection attempts

    logging.info("Socket created successfully")

    attempt = 0
    while attempt < max_retries:
        attempt += 1
        try:
            logging.info(f"Connection attempt {attempt}/{max_retries}...")
            client_socket.connect((host, port))
            logging.info(f"Connected to server at {host}:{port}")
            return client_socket
        except socket.timeout:
            logging.warning("Connection attempt timed out")
        except ConnectionRefusedError:
            logging.warning("Connection refused. Make sure the server is running.")
        except socket.error as e:
            logging.error(f"Connection error: {e}")

        if attempt < max_retries:
            logging.info(f"Retrying in {retry_delay} seconds...")
            time.sleep(retry_delay)

    ## If we get here, all connection attempts failed
    logging.error(f"Failed to connect after {max_retries} attempts")
    client_socket.close()
    return None

def main():
    """Main client function"""
    global running

    try:
        ## Connect to the server
        client_socket = connect_to_server(HOST, PORT)
        if not client_socket:
            logging.error("Could not connect to server. Exiting.")
            return

        ## Set a longer timeout for normal operation
        client_socket.settimeout(1)  ## 1 second timeout for receiving

        ## Start a thread to receive messages
        receive_thread = threading.Thread(target=receive_messages, args=(client_socket,))
        receive_thread.daemon = True
        receive_thread.start()

        ## Print welcome message
        print("\nWelcome to the Chat Client!")
        print("Type your messages and press Enter to send.")
        print("Type 'exit' to quit the chat.")

        ## Send messages
        while running:
            try:
                message = input("Your message: ")

                ## Check if the user wants to exit
                if message.lower() == 'exit':
                    logging.info("User requested to exit")
                    running = False
                    break

                ## Send the message to the server
                client_socket.sendall(message.encode('utf-8'))

            except EOFError:
                ## Handle EOF (Ctrl+D)
                logging.info("EOF received, exiting")
                running = False
                break
            except KeyboardInterrupt:
                ## Handle Ctrl+C
                logging.info("Keyboard interrupt received, exiting")
                running = False
                break
            except socket.error as e:
                logging.error(f"Error sending message: {e}")
                running = False
                break

    except Exception as e:
        logging.error(f"Unexpected error: {e}")
    finally:
        ## Clean up
        running = False

        if 'client_socket' in locals() and client_socket:
            try:
                client_socket.close()
                logging.info("Socket closed")
            except:
                pass

        logging.info("Client shutdown complete")
        print("\nDisconnected from the chat server. Goodbye!")

if __name__ == "__main__":
    main()

채팅 애플리케이션 테스트

이제 채팅 애플리케이션을 테스트해 보겠습니다.

  1. 먼저 채팅 서버를 시작합니다.

    cd ~/project
    python3 chat_server.py
    
  2. 두 번째 터미널에서 채팅 클라이언트를 시작합니다.

    cd ~/project
    python3 chat_client.py
    
  3. 세 번째 터미널에서 다른 채팅 클라이언트를 시작합니다.

    cd ~/project
    python3 chat_client.py
    
  4. 두 클라이언트 모두에서 메시지를 보내고 연결된 모든 클라이언트에 어떻게 브로드캐스트되는지 확인합니다.

  5. 클라이언트 중 하나를 종료하고 (Ctrl+C 사용 또는 'exit' 입력) 서버가 연결 해제를 처리하는 방식을 확인합니다.

  6. 클라이언트 중 하나를 다시 시작하여 재연결 프로세스를 확인합니다.

구현된 주요 기능

완전한 채팅 애플리케이션은 몇 가지 중요한 오류 처리 및 강력성 기능을 구현합니다.

  1. Connection retry mechanism (연결 재시도 메커니즘): 클라이언트는 초기 연결에 실패할 경우 서버에 다시 연결을 시도합니다.
  2. Proper thread management (적절한 스레드 관리): 서버는 스레드를 사용하여 여러 클라이언트를 동시에 처리합니다.
  3. Timeout handling (시간 초과 처리): 서버와 클라이언트 모두 무한 대기를 방지하기 위해 시간 초과를 구현합니다.
  4. Resource cleanup (리소스 정리): 모든 리소스 (소켓, 스레드) 는 오류 조건에서도 적절하게 정리됩니다.
  5. Comprehensive error handling (포괄적인 오류 처리): 특정 오류 유형이 포착되어 적절하게 처리됩니다.
  6. Logging (로깅): 서버와 클라이언트 모두 진단을 위해 로깅을 구현합니다.
  7. User-friendly messages (사용자 친화적인 메시지): 명확한 메시지는 연결 상태에 대해 사용자에게 알립니다.
  8. Graceful shutdown (정상적인 종료): 애플리케이션은 요청 시 정상적으로 종료될 수 있습니다.

소켓 오류 처리를 위한 모범 사례

구현을 기반으로 Python 에서 소켓 오류 처리를 위한 몇 가지 모범 사례는 다음과 같습니다.

  1. 오류를 포착하고 처리하기 위해 소켓 작업 주변에 **Always use try-except blocks (항상 try-except 블록 사용)**를 사용합니다.
  2. 무한 대기를 방지하기 위해 모든 소켓 작업에 **Implement timeouts (시간 초과 구현)**를 사용합니다.
  3. 다양한 유형의 오류를 적절하게 처리하려면 **Use specific exception types (특정 예외 유형 사용)**를 사용합니다.
  4. 적절한 리소스 정리를 위해 **Always close sockets (항상 소켓 닫기)**를 finally 블록에서 사용합니다.
  5. 연결과 같은 중요한 작업에 대해 **Implement retry mechanisms (재시도 메커니즘 구현)**를 사용합니다.
  6. 진단을 위해 오류 및 작업을 기록하려면 **Use logging (로깅 사용)**을 사용합니다.
  7. 여러 클라이언트로 작업할 때 **Handle thread synchronization (스레드 동기화 처리)**를 적절하게 수행합니다.
  8. 문제가 발생할 경우 사용자에게 **Provide meaningful error messages (의미 있는 오류 메시지 제공)**를 제공합니다.
  9. 클라이언트와 서버 모두에 대해 Implement graceful shutdown (정상적인 종료 구현) 절차를 구현합니다.
  10. 오류 처리가 올바르게 작동하는지 확인하려면 **Test error scenarios (오류 시나리오 테스트)**를 수행합니다.

이러한 모범 사례를 따르면 Python 에서 강력하고 안정적인 소켓 기반 애플리케이션을 구축하는 데 도움이 됩니다.

요약

이 Lab 에서는 Python 소켓 통신에서 강력한 오류 처리를 구현하는 방법을 배웠습니다. 소켓 프로그래밍의 기본 사항부터 시작하여 일반적인 소켓 오류를 식별하고 적절한 오류 처리 기술을 구현하는 과정을 거쳤습니다.

이 Lab 의 주요 학습 내용은 다음과 같습니다.

  1. Understanding Socket Basics (소켓 기본 사항 이해): 소켓을 만들고, 연결을 설정하고, 데이터를 교환하는 것을 포함하여 Python 에서 소켓 통신이 작동하는 방식을 배웠습니다.

  2. Identifying Common Errors (일반적인 오류 식별): 연결 거부, 시간 초과 및 예기치 않은 연결 해제와 같은 일반적인 소켓 관련 오류를 살펴보았습니다.

  3. Implementing Basic Error Handling (기본 오류 처리 구현): try-except 블록을 사용하여 소켓 오류를 정상적으로 포착하고 처리하는 방법을 배웠습니다.

  4. Advanced Error Handling Techniques (고급 오류 처리 기술): 재시도 메커니즘, 시간 초과 처리 및 적절한 리소스 정리를 구현했습니다.

  5. Integrating Logging (로깅 통합): 더 나은 진단을 위해 Python 의 로깅 모듈을 사용하여 작업 및 오류를 기록하는 방법을 배웠습니다.

  6. Building Complete Applications (완전한 애플리케이션 구축): 실제 시나리오에서 포괄적인 오류 처리를 보여주는 완전한 채팅 애플리케이션을 만들었습니다.

자신의 Python 소켓 프로그래밍 프로젝트에서 이러한 기술을 적용하면 다양한 오류 조건을 정상적으로 처리할 수 있는 보다 강력하고 안정적인 네트워크 애플리케이션을 만들 수 있습니다.

적절한 오류 처리는 오류를 포착하는 것뿐만 아니라 의미 있는 피드백을 제공하고, 복구 메커니즘을 구현하며, 네트워크 관련 문제에도 불구하고 애플리케이션이 안정적이고 안전하게 유지되도록 하는 것임을 기억하십시오.