Module 22 Network Programming in Python Using Sockets Building A Chat Application
Module 22 Network Programming in Python Using Sockets Building A Chat Application
1.1 IP Address
Now, let's implement the full GUI chat application, ensuring both the server and clients have
interactive interfaces and can send/receive messages.
1
Key Design Principles for GUI Chat:
Python
import socket
import threading
import tkinter as tk
from tkinter import scrolledtext, messagebox, simpledialog
import queue # For thread-safe communication between threads and GUI
class ChatServerGUI:
def __init__(self, master):
self.master = master
master.title("Python Chat Server")
master.geometry("700x600") # Increased size for better layout
master.resizable(True, True) # Allow window resizing
master.protocol("WM_DELETE_WINDOW", self.on_closing) # Handle
window close event
self.is_server_running = False
self.server_socket = None
self.server_thread = None
2
self.control_frame = tk.Frame(master, bd=2, relief=tk.GROOVE,
padx=5, pady=5)
self.control_frame.pack(fill=tk.X, pady=5)
self.send_button = tk.Button(self.broadcast_frame,
text="Broadcast", command=self.send_broadcast_message, bg="#FFC107",
fg="black")
self.send_button.pack(side=tk.RIGHT, padx=5)
def clear_log(self):
"""Clears the content of the log text widget."""
3
self.log_text.config(state='normal')
self.log_text.delete(1.0, tk.END)
self.log_text.config(state='disabled')
def start_server_gui(self):
"""Starts the server in a separate thread."""
if not self.is_server_running:
self.is_server_running = True
self.status_label.config(text="Server Status: Starting...",
fg="orange")
self.start_button.config(state=tk.DISABLED)
self.stop_button.config(state=tk.NORMAL)
self.message_entry.config(state=tk.NORMAL)
self.send_button.config(state=tk.NORMAL)
self.server_thread = threading.Thread(target=self._run_server)
self.server_thread.daemon = True # Allows main program to exit
even if thread is running
self.server_thread.start()
self.log_message(f"Attempting to start server on
{SERVER_IP}:{SERVER_PORT}")
def stop_server_gui(self):
"""Stops the server and cleans up."""
if self.is_server_running:
self.is_server_running = False
self.status_label.config(text="Server Status: Stopping...",
fg="orange")
self.stop_button.config(state=tk.DISABLED)
self.start_button.config(state=tk.NORMAL)
self.message_entry.config(state=tk.DISABLED)
self.send_button.config(state=tk.DISABLED)
self.log_message("Server stopped.")
self.status_label.config(text="Server Status: Not Running",
fg="red")
def _run_server(self):
"""Actual server logic running in a separate thread."""
try:
4
self.server_socket = socket.socket(socket.AF_INET,
socket.SOCK_STREAM)
self.server_socket.setsockopt(socket.SOL_SOCKET,
socket.SO_REUSEADDR, 1)
self.server_socket.bind((SERVER_IP, SERVER_PORT))
self.server_socket.listen(MAX_CLIENTS)
message_queue.put(f"[SERVER] Server listening on
{SERVER_IP}:{SERVER_PORT}")
self.master.after(0, lambda:
self.status_label.config(text="Server Status: Running", fg="green"))
while self.is_server_running:
try:
self.server_socket.settimeout(0.5) # Set a small
timeout to check self.is_server_running
client_socket, client_address =
self.server_socket.accept()
except socket.timeout:
continue # Timeout occurred, check
self.is_server_running again
except OSError as e:
if self.is_server_running: # Only log if not
intentionally stopped
message_queue.put(f"[ERROR] Server accept error:
{e}")
break # Break loop on other OS errors (e.g., socket
closed)
except Exception as e:
message_queue.put(f"[ERROR] Unexpected error in server
loop: {e}")
break
except socket.error as e:
message_queue.put(f"[ERROR] Server socket binding/setup error:
{e}")
self.master.after(0, lambda:
self.status_label.config(text="Server Status: Error", fg="red"))
finally:
if self.server_socket:
self.server_socket.close()
5
message_queue.put("[SERVER] Server socket closed.")
self.master.after(0, lambda:
self.status_label.config(text="Server Status: Not Running", fg="red"))
self.master.after(0, lambda:
self.start_button.config(state=tk.NORMAL))
self.master.after(0, lambda:
self.stop_button.config(state=tk.DISABLED))
try:
# First message from client should be their name
initial_data = client_socket.recv(BUFFER_SIZE).decode('utf-8')
if initial_data.startswith("NAME:"):
client_name = initial_data[len("NAME:"):]
message_queue.put(f"[SERVER] Client {client_address}
identified as '{client_name}'.")
self._broadcast_message(f"'{client_name}' has joined the
chat.", "SERVER")
else:
message_queue.put(f"[SERVER] Client {client_address} sent
unexpected initial data. Assuming name '{client_name}'.")
# Handle the first message as a regular message if not a
name
message_queue.put(f"[{client_name}] {initial_data}")
self._broadcast_message(initial_data, client_name)
with clients_lock:
clients.append((client_socket, client_address,
client_name))
while self.is_server_running:
message = client_socket.recv(BUFFER_SIZE).decode('utf-8')
if not message: # Client disconnected
break
message_queue.put(f"[{client_name}] {message}")
self._broadcast_message(message, client_name)
except ConnectionResetError:
message_queue.put(f"[DISCONNECTED] Client '{client_name}'
({client_address}) reset connection.")
except Exception as e:
message_queue.put(f"[ERROR] Error handling client
'{client_name}' ({client_address}): {e}")
finally:
with clients_lock:
if (client_socket, client_address, client_name) in clients:
clients.remove((client_socket, client_address,
client_name))
try:
client_socket.close()
except OSError:
pass # Ignore if already closed
message_queue.put(f"[DISCONNECTED] Client '{client_name}'
({client_address}) left.")
self._broadcast_message(f"'{client_name}' has left the chat.",
"SERVER")
6
def _broadcast_message(self, message, sender_name):
"""Sends a message to all connected clients except the sender."""
# Format message as it should appear on clients
full_message = f"<{sender_name}> {message}".encode('utf-8')
with clients_lock:
clients_to_remove = []
for client_sock, addr, name in clients:
if name != sender_name: # Don't send back to sender
try:
client_sock.sendall(full_message)
except Exception as e:
message_queue.put(f"[BROADCAST ERROR] Could not
send to '{name}' ({addr}): {e}")
clients_to_remove.append((client_sock, addr, name))
# Mark for removal
def send_broadcast_message(self):
"""Sends the message from the server's input field to all connected
clients."""
if not self.is_server_running:
messagebox.showwarning("Server Not Running", "Server must be
running to broadcast messages.")
return
message = self.message_entry.get().strip()
if not message:
return # Don't send empty messages
def process_queue(self):
"""Periodically checks the message queue and updates the GUI."""
while not message_queue.empty():
message = message_queue.get()
self.log_message(message, sender="SERVER Log") # Use a distinct
sender for internal logs
self.master.after(100, self.process_queue) # Check again after
100ms
7
def on_closing(self):
"""Handles closing the Tkinter window."""
if self.is_server_running:
if messagebox.askokcancel("Quit Server", "Server is running. Do
you want to stop it and quit?"):
self.stop_server_gui() # Ensure server is stopped
gracefully
self.master.destroy()
else:
if messagebox.askokcancel("Quit Server", "Do you want to
quit?"):
self.master.destroy()
if __name__ == "__main__":
from datetime import datetime # Import here for logging timestamps
root = tk.Tk()
app = ChatServerGUI(root)
root.mainloop()
Python
import socket
import threading
import tkinter as tk
from tkinter import scrolledtext, messagebox, simpledialog
import queue # For thread-safe communication with GUI
class ChatClientGUI:
def __init__(self, master):
self.master = master
master.title("Python Chat Client")
master.geometry("550x650") # Increased size
master.resizable(True, True)
master.protocol("WM_DELETE_WINDOW", self.on_closing)
self.client_socket = None
self.receive_thread = None
self.is_connected = False
self.client_name = ""
8
self.conn_name_frame.pack(fill=tk.X, pady=5)
self.disconnect_button = tk.Button(self.button_frame,
text="Disconnect", command=self.disconnect_from_server, state=tk.DISABLED,
bg="#f44336", fg="white")
self.disconnect_button.pack(side=tk.LEFT, padx=10, pady=5,
expand=True)
9
self.message_entry.bind("<Return>", self.send_message_event) # Bind
Enter key to send
def connect_to_server(self):
"""Initiates connection to the server."""
self.client_name = self.name_entry.get().strip()
if not self.client_name:
messagebox.showerror("Error", "Please enter your name to join
the chat.")
return
server_ip = self.ip_entry.get().strip()
server_port_str = self.port_entry.get().strip()
10
try:
server_port = int(server_port_str)
if not (1024 <= server_port <= 65535):
raise ValueError("Port number out of valid range (1024-
65535).")
except ValueError as e:
messagebox.showerror("Error", f"Invalid Port number: {e}")
return
self.client_socket = socket.socket(socket.AF_INET,
socket.SOCK_STREAM)
try:
self.client_socket.connect((server_ip, server_port))
self.set_ui_state(connected=True)
self.display_message(f"--- Connected to
{server_ip}:{server_port} as '{self.client_name}' ---")
self.client_socket.sendall(f"NAME:{self.client_name}".encode('utf-8'))
self.receive_thread =
threading.Thread(target=self._receive_messages)
self.receive_thread.daemon = True # Allow main program to exit
self.receive_thread.start()
except ConnectionRefusedError:
messagebox.showerror("Connection Error", "Connection refused.
Make sure the server is running and reachable.")
self.set_ui_state(connected=False)
except Exception as e:
messagebox.showerror("Connection Error", f"An unexpected error
occurred during connection: {e}")
self.set_ui_state(connected=False)
def disconnect_from_server(self):
"""Disconnects from the server."""
if self.client_socket:
try:
# Send a disconnect message to the server (optional, but
polite)
self.client_socket.sendall(f"<{self.client_name}> has left
the chat.".encode('utf-8'))
self.client_socket.shutdown(socket.SHUT_RDWR) # Signal
shutdown
self.client_socket.close()
except OSError:
pass # Ignore errors if socket is already closed or
connection was reset
except Exception as e:
print(f"Error during socket shutdown/close: {e}")
self.client_socket = None
self.set_ui_state(connected=False)
self.display_message("--- Disconnected from server ---")
self.client_name = "" # Clear name on disconnect
def _receive_messages(self):
"""Thread function to continuously receive messages from the
server."""
while self.is_connected:
11
try:
message = self.client_socket.recv(BUFFER_SIZE).decode('utf-
8')
if not message: # Server disconnected or sent empty message
message_queue.put("[DISCONNECTED] Server disconnected.
Please reconnect.")
self.master.after(0, self.disconnect_from_server) #
Schedule UI update on main thread
break
message_queue.put(message) # Put message into queue for GUI
update
except ConnectionResetError:
message_queue.put("[DISCONNECTED] Server reset connection.
Please reconnect.")
self.master.after(0, self.disconnect_from_server)
break
except Exception as e:
message_queue.put(f"[ERROR] Error receiving message: {e}")
self.master.after(0, self.disconnect_from_server)
break
def send_message(self):
"""Sends the message from the input field to the server."""
if not self.is_connected or not self.client_socket:
messagebox.showwarning("Not Connected", "You are not connected
to the server.")
return
message = self.message_entry.get().strip()
if not message:
return # Don't send empty messages
try:
# Send the message (server will prepend sender name for
broadcast)
self.client_socket.sendall(message.encode('utf-8'))
self.display_message(f"You: {message}") # Display own message
immediately
self.message_entry.delete(0, tk.END) # Clear input field
except Exception as e:
messagebox.showerror("Send Error", f"Failed to send message:
{e}")
self.disconnect_from_server() # Disconnect on send error
def process_queue(self):
"""Periodically checks the message queue and updates the GUI."""
while not message_queue.empty():
message = message_queue.get()
self.display_message(message)
self.master.after(100, self.process_queue) # Check again after
100ms
def on_closing(self):
"""Handles closing the Tkinter window."""
if self.is_connected:
if messagebox.askokcancel("Quit Chat", "You are connected. Do
you want to disconnect and quit?"):
12
self.disconnect_from_server() # Ensure socket is closed
self.master.destroy()
else:
if messagebox.askokcancel("Quit Chat", "Do you want to quit?"):
self.master.destroy()
if __name__ == "__main__":
from datetime import datetime # Import here for timestamps
root = tk.Tk()
app = ChatClientGUI(root)
root.mainloop()
13
5. Disconnect/Quit:
o Client: Use the "Disconnect" button or close the client window (which will
prompt for confirmation) to gracefully close the client connection.
o Server: Click the "Stop Server" button on the server GUI to shut it down.
Closing the server window will also prompt for confirmation to stop the
server.
14