DEV Community

Cover image for Multi Client TCP Chat with Tkinter
CJ
CJ

Posted on

Multi Client TCP Chat with Tkinter

Table of Content

  1. Introduction
  2. Technical Breakdown
  3. Diagrams & Visuals
  4. Code Walkthrough

1. Introduction

A TCP socket-based chat application allows multiple clients to communicate with a central server in real time. This tutorial walks through building a multi-client chat application using Python’s socket and threading modules for backend communication and Tkinter for a graphical user interface (GUI). This is a great project to learn about networking, concurrency, and GUI development.

2. Technical Breakdown

Understanding Sockets in Python

Sockets enable network communication between processes. Python’s socket module provides the necessary tools to create a server-client architecture where clients send and receive messages through a central server.

Server-Client Architecture for Multi-Client Setup

  1. Server: Listens for incoming connections and handles multiple clients simultaneously.
  2. Clients: Connect to the server, send messages, and receive broadcasts from other clients.

Implementing Threading for Multiple Clients

Since a single-threaded server would block while handling one client, we use Python’s threading module to allow concurrent client connections.

Developing a Tkinter-Based GUI

A simple Tkinter interface will provide a user-friendly way to send and receive messages.

3. Diagrams & Visuals

Network Architecture

Network Architecture

Message Flowchart

Message Flowchart

4. Code Walkthrough

Server.py


import socket  # Importing socket for network communication
import threading  # Importing threading to handle multiple clients concurrently
import tkinter as tk  # Importing Tkinter for GUI
from tkinter import scrolledtext  # Importing scrolledtext for displaying chat messages

class ChatServer:
    def __init__(self, root):
        self.root = root
        self.root.title("Server Chat")  # Setting the window title

        # Creating a server socket (IPv4, TCP)
        self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

        # Binding the server to localhost (127.0.0.1) on port 8888
        self.server_socket.bind(("127.0.0.1", 8888))

        # Listening for up to 5 simultaneous connections
        self.server_socket.listen(5)

        # List to keep track of connected clients
        self.clients = []

        # Starting a background thread to accept new clients
        threading.Thread(target=self.accept_clients, daemon=True).start()

        # Creating a scrollable text widget to display chat messages
        self.chat_display = scrolledtext.ScrolledText(root, state='disabled', width=50, height=15)
        self.chat_display.pack(pady=10)

    def accept_clients(self):
        """Accepts incoming client connections and starts a new thread for each."""
        while True:
            client_socket, addr = self.server_socket.accept()  # Accept client connection
            self.clients.append(client_socket)  # Add the client to the list
            threading.Thread(target=self.handle_client, args=(client_socket,)).start()  # Start handling client

    def handle_client(self, client_socket):
        """Handles receiving messages from a client and broadcasting them."""
        while True:
            try:
                # Receive message from the client (max 1024 bytes)
                message = client_socket.recv(1024).decode("utf-8")

                # If the client disconnects, exit the loop
                if not message:
                    break

                # Send the received message to all other clients
                self.broadcast(message, client_socket)

            except:  # If an error occurs (e.g., client disconnects)
                self.clients.remove(client_socket)  # Remove client from the list
                client_socket.close()  # Close the connection
                break  # Exit loop

    def broadcast(self, message, sender_socket):
        """Sends received messages to all clients except the sender."""
        for client in self.clients:
            if client != sender_socket:  # Ensure the sender doesn't receive their own message
                client.send(message.encode("utf-8"))  # Send message to client

        # Display the message in the server GUI
        self.chat_display.config(state='normal')  # Enable text editing
        self.chat_display.insert(tk.END, message + "\n")  # Insert message
        self.chat_display.config(state='disabled')  # Disable text editing
        self.chat_display.yview(tk.END)  # Scroll to the latest message

if __name__ == "__main__":
    root = tk.Tk()  # Create the main Tkinter window
    server = ChatServer(root)  # Initialize and start the chat server
    root.mainloop()  # Run the Tkinter event loop

Enter fullscreen mode Exit fullscreen mode

Client.py

import socket  # Importing socket for network communication
import threading  # Importing threading to handle receiving messages concurrently
import tkinter as tk  # Importing Tkinter for GUI
from tkinter import scrolledtext, messagebox  # Importing scrolledtext for chat display, messagebox for alerts

class ChatClient:
    def __init__(self, root):
        self.root = root
        self.root.title("Client Chat")  # Setting the window title

        self.client_socket = None  # Will hold the socket connection
        self.running = False  # Tracks whether the client is connected

        # GUI Elements
        self.username_label = tk.Label(root, text="Username:")  # Username label
        self.username_label.pack()

        self.username_entry = tk.Entry(root)  # Input field for username
        self.username_entry.pack()

        self.connect_button = tk.Button(root, text="Connect", command=self.connect_to_server)  # Connect button
        self.connect_button.pack(pady=5)

        self.chat_display = scrolledtext.ScrolledText(root, state='disabled', width=50, height=15)  # Chat display area
        self.chat_display.pack(pady=10)

        self.message_entry = tk.Entry(root, width=40)  # Input field for messages
        self.message_entry.pack(pady=5)

        self.send_button = tk.Button(root, text="Send", command=self.send_message)  # Send button
        self.send_button.pack(pady=5)

        self.disconnect_button = tk.Button(root, text="Disconnect", command=self.disconnect)  # Disconnect button
        self.disconnect_button.pack(pady=5)

    def connect_to_server(self):
        """Establishes a connection to the chat server."""
        if self.running:
            messagebox.showwarning("Warning", "Already connected!")
            return

        username = self.username_entry.get().strip()  # Get username from input
        if not username:  
            messagebox.showerror("Error", "Username is required!")  # Show error if username is empty
            return

        try:
            self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # Create a TCP socket
            self.client_socket.connect(("127.0.0.1", 8888))  # Connect to the server
            self.client_socket.send((username + " joined the chat!").encode("utf-8"))  # Send username as the first message
            self.running = True

            # Start a background thread to receive messages
            threading.Thread(target=self.receive_messages, daemon=True).start()

            # Update chat display to show successful connection
            self.chat_display.config(state='normal')
            self.chat_display.insert(tk.END, f"Connected to 127.0.0.1:8888 as {username}\n")
            self.chat_display.config(state='disabled')

        except Exception as e:
            messagebox.showerror("Error", f"Connection failed: {e}")  # Show error message if connection fails

    def receive_messages(self):
        """Continuously receives and displays messages from the server."""
        while self.running:
            try:
                message = self.client_socket.recv(1024).decode("utf-8")  # Receive message (max 1024 bytes)

                if not message:  # If message is empty, exit loop
                    break

                self.chat_display.config(state='normal')

                # Display received message in chat window
                self.chat_display.insert(tk.END, message + "\n")

                self.chat_display.config(state='disabled')
                self.chat_display.yview(tk.END)  # Auto-scroll to the latest message

            except ConnectionResetError:  # If the server disconnects
                self.chat_display.config(state='normal')
                self.chat_display.insert(tk.END, "Server disconnected.\n")
                self.chat_display.config(state='disabled')
                self.running = False
                break  # Exit loop

    def send_message(self):
        """Sends a message to the server."""
        if not self.running:
            messagebox.showerror("Error", "Not connected to any server!")  # Show error if not connected
            return

        message = self.message_entry.get().strip()  # Get the input message
        self.chat_display.config(state='normal')

        # Show the user's message in the chat window
        self.chat_display.insert(tk.END, str(self.username_entry.get()) + "(You): " + message + "\n")

        self.chat_display.config(state='disabled')
        self.chat_display.yview(tk.END)  # Auto-scroll to latest message

        if message:  # If message is not empty
            try:
                # Send the message to the server, prefixed with the username
                self.client_socket.send((str(self.username_entry.get()) + ": " + message).encode("utf-8"))
                self.message_entry.delete(0, tk.END)  # Clear the input field after sending
            except Exception as e:
                messagebox.showerror("Error", f"Message sending failed: {e}")  # Show error if sending fails

    def disconnect(self):
        """Disconnects from the chat server."""
        if not self.running:
            messagebox.showwarning("Warning", "Already disconnected!")  # Show warning if already disconnected
            return

        self.running = False  # Set running to False
        self.client_socket.close()  # Close the socket connection

        # Update chat display to show disconnection message
        self.chat_display.config(state='normal')
        self.chat_display.insert(tk.END, "Disconnected from server.\n")
        self.chat_display.config(state='disabled')

if __name__ == "__main__":
    root = tk.Tk()  # Create the main Tkinter window
    client = ChatClient(root)  # Initialize and start the chat client
    root.mainloop()  # Run the Tkinter event loop

Enter fullscreen mode Exit fullscreen mode

Top comments (0)