Múltiples Clientes y Manejo de Errores
En las aplicaciones del mundo real, un servidor normalmente necesita manejar múltiples clientes simultáneamente y gestionar con elegancia diversas condiciones de error. Mejoremos nuestra implementación teniendo en cuenta estas consideraciones.
Entendiendo el Manejo Concurrente de Clientes
Hay varias formas de manejar múltiples clientes concurrentemente:
- Threading (Hilos): Crea un nuevo hilo para cada conexión de cliente
- Process-based (Basado en procesos): Genera un nuevo proceso para cada cliente
- Asynchronous I/O (E/S asíncrona): Usa E/S no bloqueante con un bucle de eventos
Para este laboratorio, implementaremos un enfoque basado en hilos, que es relativamente simple de entender e implementar.
Mejorando el Servidor para Múltiples Clientes
Modifiquemos nuestro servidor para manejar múltiples clientes usando hilos:
- Abre el archivo
server.py en el WebIDE y reemplaza el código con:
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()
- Guarda el archivo.
Mejorando el Cliente con Manejo de Errores
Mejoremos también nuestro cliente con un mejor manejo de errores:
- Abre el archivo
client.py en el WebIDE y reemplaza el código con:
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()
- Guarda el archivo.
Entendiendo el Código Mejorado
Mejoras del Servidor:
- Hemos agregado el módulo
threading para manejar múltiples clientes concurrentemente.
- Cada conexión de cliente ahora se maneja en un hilo separado.
- Hemos mejorado el manejo de errores con capturas de excepciones más específicas.
- Mostramos el número de conexiones de cliente activas.
- Los hilos se establecen como "daemon" (demonio), lo que significa que se cerrarán automáticamente cuando el programa principal se cierre.
Mejoras del Cliente:
- Hemos agregado un tiempo de espera de conexión para evitar que se cuelgue si el servidor no está disponible.
- Hemos mejorado el manejo de errores con capturas de excepciones específicas para diferentes errores de red.
- Hemos agregado mensajes de estado más descriptivos con un formato claro.
Probando el Servidor Multi-Cliente
Probemos nuestras aplicaciones mejoradas:
- Inicia el servidor:
python3 ~/project/socket_lab/server.py
Deberías ver:
[STARTING] Server is listening on 127.0.0.1:12345
- En una nueva terminal, inicia un cliente:
python3 ~/project/socket_lab/client.py
- Inicia otro cliente en una tercera terminal:
python3 ~/project/socket_lab/client.py
- En la terminal del servidor, deberías ver ambas conexiones:
[NEW CONNECTION] ('127.0.0.1', 59124) connected.
[ACTIVE CONNECTIONS] 1
[NEW CONNECTION] ('127.0.0.1', 59126) connected.
[ACTIVE CONNECTIONS] 2
- Envía mensajes desde ambos clientes y observa cómo el servidor los recibe:
[('127.0.0.1', 59124)] Hello from client 1
[('127.0.0.1', 59126)] Hello from client 2
- Cuando hayas terminado, escribe 'quit' en cada cliente para desconectarte, o presiona Ctrl+C en la terminal del servidor para apagar el servidor.
Manejo de Servidor No en Ejecución
Probemos también qué sucede cuando el servidor no está en ejecución:
-
Asegúrate de que el servidor esté detenido (presiona Ctrl+C si se está ejecutando)
-
Intenta ejecutar un cliente:
python3 ~/project/socket_lab/client.py
Deberías ver:
[CONNECTING] Connecting to server at 127.0.0.1:12345...
[REFUSED] Connection refused. Make sure the server is running.
[DISCONNECTED] Disconnected from server
El cliente ahora maneja el error con elegancia, informando al usuario que es posible que el servidor no se esté ejecutando.
Con estas mejoras, hemos creado un sistema cliente-servidor robusto que puede manejar múltiples clientes y diversas condiciones de error. Esta es una base sólida para desarrollar aplicaciones en red más complejas.