Multiple Clients and Error Handling
In real-world applications, a server typically needs to handle multiple clients simultaneously and gracefully manage various error conditions. Let's improve our implementation with these considerations in mind.
Understanding Concurrent Client Handling
There are several ways to handle multiple clients concurrently:
- Threading: Create a new thread for each client connection
- Process-based: Spawn a new process for each client
- Asynchronous I/O: Use non-blocking I/O with an event loop
For this lab, we'll implement a threading-based approach, which is relatively simple to understand and implement.
Enhancing the Server for Multiple Clients
Let's modify our server to handle multiple clients using threads:
- Open the
server.py
file in the WebIDE and replace the code with:
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()
- Save the file.
Improving the Client with Error Handling
Let's also enhance our client with better error handling:
- Open the
client.py
file in the WebIDE and replace the code with:
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()
- Save the file.
Understanding the Enhanced Code
Server Enhancements:
- We've added the
threading
module to handle multiple clients concurrently
- Each client connection is now handled in a separate thread
- We've improved error handling with more specific exception catches
- We display the number of active client connections
- Threads are set as "daemon", which means they'll automatically close when the main program exits
Client Enhancements:
- We've added a connection timeout to prevent hanging if the server isn't available
- We've improved error handling with specific exception catches for different network errors
- We've added more descriptive status messages with clear formatting
Testing the Multi-Client Server
Let's test our improved applications:
- Start the server:
python3 ~/project/socket_lab/server.py
You should see:
[STARTING] Server is listening on 127.0.0.1:12345
- In a new terminal, start a client:
python3 ~/project/socket_lab/client.py
- Start another client in a third terminal:
python3 ~/project/socket_lab/client.py
- In the server terminal, you should see both connections:
[NEW CONNECTION] ('127.0.0.1', 59124) connected.
[ACTIVE CONNECTIONS] 1
[NEW CONNECTION] ('127.0.0.1', 59126) connected.
[ACTIVE CONNECTIONS] 2
- Send messages from both clients and observe the server receiving them:
[('127.0.0.1', 59124)] Hello from client 1
[('127.0.0.1', 59126)] Hello from client 2
- When you're done, type 'quit' in each client to disconnect, or press Ctrl+C in the server terminal to shut down the server.
Handling Server Not Running
Let's also test what happens when the server isn't running:
-
Make sure the server is stopped (press Ctrl+C if it's running)
-
Try to run a client:
python3 ~/project/socket_lab/client.py
You should see:
[CONNECTING] Connecting to server at 127.0.0.1:12345...
[REFUSED] Connection refused. Make sure the server is running.
[DISCONNECTED] Disconnected from server
The client now handles the error gracefully, informing the user that the server might not be running.
With these enhancements, we've created a robust client-server system that can handle multiple clients and various error conditions. This is a solid foundation for developing more complex networked applications.