Building a Chat Room in Python

Building a Chat Room in Python

Chatroom GIFs | Tenor

Chat rooms are a popular way for people to communicate and interact with each other in real time over the Internet. In this article, we will explore how to build a simple chat room using Python, specifically focusing on the socket and threading modules. We will examine the provided code that implements a server and client for the chat room and explain each of the individual functions involved.

Introduction to Socket and Threading

Socket Programming in Python (Guide) – Real Python

  • Socket

    Sockets are endpoints for communication between two machines over a network. They allow programs to send and receive data across the network. In our chat room implementation, we will use sockets to establish connections between the server and clients.

    Python's socket module provides a low-level interface for socket programming. It offers various functions and methods to create and manipulate sockets. We will utilize the socket.socket class to create sockets and perform socket-related operations.

  • Threading

    Threading is a technique that allows concurrent execution of multiple threads within a single program. Threads are lightweight and independent units of execution that can perform tasks simultaneously. In our chat room implementation, we will use threading to handle multiple client connections and enable concurrent communication.

    Python's threading module provides a high-level interface for creating and managing threads. It offers the Thread class, which allows us to define thread functions and start them concurrently.

Server Implementation

  • Initialization

    The Server class initializes various attributes such as host, port, server socket, a list of disconnected keywords, an empty list of clients, an empty list of nicknames, and a logger for logging server events.

import socket
import threading
import structlog

class Server:
    def __init__(self):
        self.host = socket.gethostbyname(socket.gethostname())
        self.port = 5051
        self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.DISCONNETED = ['q', 'exit', 'close', 'disconnect']
        self.clients = []
        self.nicknames = []
        self.logger = structlog.get_logger()
  • Broadcasting Messages

    The broadcast method sends a message to all clients in the chat room except the client who sent the message. The broadcast method iterates over the list of connected clients and sends the message to each client except the client who sent the message.

def broadcast(self, msg, client=None):
    for _client in self.clients:
        if _client != client:
            _client.send(msg)
  • Starting the Server

    The start_server method first binds the server socket to the specified host and port using the bind method. Then it starts listening for incoming connections using the listen method. Finally, it calls the listen method to handle incoming requests.

def start_server(self):
    self.server.bind((self.host, self.port))
    self.server.listen()
    self.logger.info(f'Server is listening on {self.host}:{self.port}')
    self.listen()
  • Handling Client Connections

    The handle_client method runs in a loop and continuously receives messages from the client using the recv method. If the received message is one of the disconnected keywords, it sets the is_connected flag to False and breaks out of the loop.

    Otherwise, it broadcasts the received message to all other clients using the broadcast method. Additionally, it logs the total number of active connections using the threading.activeCount() function.

    In case of any exceptions during message reception, the client is removed from the list of clients, the socket is closed, and the associated nickname is removed from the list of nicknames. Finally, the method breaks out of the loop.

def handle_client(self, client, addr):
    is_connected = True
    while is_connected:
        try:
            msg = client.recv(1024)
            if msg in self.DISCONNETED:
                is_connected = False
                break
            self.broadcast(msg, client)
            self.logger.info("Total Connections: {}".format(threading.activeCount() - 1))
        except:
            index = self.clients.index(client)
            self.clients.remove(client)
            client.close()
            nickname = self.nicknames[index]
            self.broadcast('{} left!'.format(nickname).encode('ascii'), client)
            self.nicknames.remove(nickname)
            break
  • Listening for Connections

    The listen method runs in an infinite loop, accepting incoming client connections using the accept method of the server socket.

    Once a connection is established, it sends the message 'GET_NAME' to the client using the send method to request the nickname of the user. Then it receives the nickname from the client using the recv method and decodes it.

    The received nickname is added to the list of nicknames, and the client socket is added to the list of clients. A welcome message is broadcasted to all clients to notify that a new user has joined. Additionally, a connection message is sent to the newly connected client.

    Finally, a new thread is started using the threading.Thread class, which calls the handle_client method to handle the client's messages.

def listen(self):
    while True:
        client, addr = self.server.accept()
        client.send('GET_NAME'.encode('ascii'))
        self.nickname = client.recv(1024).decode('ascii')
        self.nicknames.append(self.nickname)
        self.clients.append(client)
        self.logger.info("Welcome {} to the chatroom".format(self.nickname))
        self.broadcast("{} joined!".format(self.nickname).encode('ascii'), client)
        client.send("{} connected to server!".format(self.nickname).encode('ascii'))
        self.logger.info("Starting thread for {}".format(self.nickname))
        threading.Thread(target=self.handle_client, args=(client, addr)).start()

Client Implementation

Now let's move on to the client-side implementation of our chat room.

  • Initialization

    The Client class initializes various attributes such as the client's nickname (name), the host IP address, the port number, the client socket (client), and a logger for logging client events.

import socket
import threading
import structlog

class Client:
    def __init__(self, nickname):
        self.name = nickname
        self.host = socket.gethostbyname(socket.gethostname())
        self.port = 5051
        self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.client.connect((self.host, self.port))
        self.logger = structlog.get_logger()
  • Receiving Messages

    The receive method runs in a loop and continuously receives messages from the server using the recv method. If the received message is 'GET_NAME', it sends the client's nickname to the server using the send method.

    Otherwise, it prints the received message to the console. If any exceptions occur during message reception, the client socket is closed, and the method breaks out of the loop.

def receive(self):
    while True:
        try:
            message = self.client.recv(1024).decode('ascii')
            if message == 'GET_NAME':
                self.client.send(self.name.encode('ascii'))
            else:
                print(message)
        except:
            print("An error occurred!")
            self.client.close()
            break
  • Writing Messages

    The write method runs in a loop and prompts the user to input a message. It constructs a message string using the client's nickname and the input message. Then it sends the message to the server using the send method.

def write(self):
    while True:
        message = '{}: {}'.format(self.name, input(''))
        self.client.send(message.encode('ascii'))
  • Connecting to the Server

    The connect_to_server method starts two threads, one for receiving messages (receive) and one for writing messages (write), enabling concurrent communication between the client and the server.

def connect_to_server(self):
    receive_thread = threading.Thread(target=self.receive)
    receive_thread.start()

    write_thread = threading.Thread(target=self.write)
    write_thread.start()
  • Putting It All Together

    In the above code, the user is prompted to enter their name, which is then used to create a Client instance. The connect_to_server method is called on the client instance to establish a connection with the server and start the message-receiving and writing threads.

if __name__ == '__main__':
    name = input("Enter your name: ")
    client = Client(name)
    client.connect_to_server()

Note:- We need to run multiple instances of the `client.py` in different terminals.

Output

  • Starting the server.

  • Connecting clients.

    • Client - A

    • Client - B

  • Sending Messages.