Build a Real-Time Chat Room with Python, WebSockets, and asyncio

python chat room

Introduction

Python Chat Room – Instant messaging has become a key part of today’s web apps. Chat systems are now essential in social networks, teamwork tools, game platforms, and customer help desks.

A live chat app lets users talk to each other right away, creating a lively, fun experience where messages show up on many devices.

In this guide, we’ll show you how to build a live room chat app using WebSockets and Python’s asyncio library. The chat’s live feature works thanks to WebSockets, which set up lasting, two-way talk channels between users and servers.

Unlike old-school HTTP requests, which are limited to a request-response, Besides WebSockets, we’ll use Python’s asyncio library to handle multiple connections at once without slowing things down.

Asyncio is built for asynchronous programming and does a great job managing lots of tasks at the same time without getting in the way of the main program.

What are WebSockets?

WebSockets are a way for clients and servers to talk to each other. They allow two-way communication through a single long-lasting connection. This is different from HTTP, which works on a back-and-forth basis.

With HTTP, the client must ask for info to get a response. WebSockets let both sides send data whenever they want. There’s no need to keep shaking hands or making new requests.

This makes WebSockets great for apps that need updates right away. Some examples are chat programs online games, stock trading platforms, and systems for connected devices.

Credit: wallarm

What is Asyncio?

Asynchronous programming is about doing things concurrently rather than sequentially. In Python, the asyncio library makes it easier to write asynchronous programs, especially if you’re working with I/O-bound tasks like those encountered while dealing with multiple network connections in a web server or an API, or for that matter, real-time applications like chat systems. 

Generally, you define functions in your program that don’t block the main thread of your program when they encounter a blocking step waiting for some sort of output (for example, from a network request or from a database).

This means you take less time per unit command and can do more concurrency-wise. You can experience drastic improvements, particularly in high-concurrency environments.

Writing the WebSocket Server Script

Our script kicks off by bringing in key modules – asyncio for asynchronous programming and websockets for dealing with WebSocket chats.

Next up we set up a global variable called connected_clients. This is a dictionary that’s super important for keeping track of who’s connected to our WebSocket and what their names are.

import asyncio
import websockets

connected_clients = {}  # Dictionary to store WebSocket connections and their associated usernames

Next, let us proceed to explain the procedure for defining the handle_connection function. This function is written as an asynchronous coroutine to serve a single WebSocket connection.

On the connection of the new client, this function asks the client to enter a username, which is placed into the dictionary of connected_clients.

In order to inform all clients about the new participant, it builds up a join message and posts it up on the console window.

async def handle_connection(websocket, path):
    # Prompt user for a username
    username = await websocket.recv()
    
    # Add the connection and username to the dictionary
    connected_clients[websocket] = username
    join_message = f"{username} joined the chat"
    print(join_message)

    # Broadcast the join message to all connected clients
    tasks = [client.send(join_message) for client in connected_clients]
    if tasks:  # Check if there are tasks to be awaited
        await asyncio.gather(*tasks)

Next, The handle_connection function employs an async for loop to receive messages coming from the established client.

To stream these created messages, the function creates a task list for sending the message to each of the clients, except the person who sent the message, and waits for the completion of these tasks.

In case a connection is lost, it handles the exception websockets.ConnectionClosed internally. After the processing, in the finally block, the function deletes the connection from the connected_clients dictionary and sends the rest of the clients a leave message, and the relevant code for this set of actions is well illustrated in the code below.

    try:
        async for message in websocket:
            # Format the message with the username
            formatted_message = f"{username}: {message}"

            # Prepare a list of tasks to send the formatted message to all connected clients except the sender
            tasks = [client.send(formatted_message) for client in connected_clients if client != websocket]
            if tasks:  # Check if there are tasks to be awaited
                await asyncio.gather(*tasks)
    except websockets.ConnectionClosed:
        pass
    finally:
        # Remove the connection and username from the dictionary
        del connected_clients[websocket]
        leave_message = f"{username} left the chat"
        print(leave_message)

        # Broadcast the leave message to all connected clients
        tasks = [client.send(leave_message) for client in connected_clients]
        if tasks:  # Check if there are tasks to be awaited
            await asyncio.gather(*tasks)

Now that we know what handle_connection is, let us define a function called main(). This asynchronous function is responsible for performing management duties with regard to the setup and the starting of the WebSocket server.

async def main():
    # Start the WebSocket server
    async with websockets.serve(handle_connection, "localhost", 8765):
        await asyncio.Future()  # Run forever

It uses websockets.serve for the purpose of creating a server on address localhost and port 8765, where handling of incoming connections is assigned to the handle_connection function.

In order to make sure that the server runs incessantly, await asyncio.Future() is used, which only means the server will not stop till it is turned off.

Lastly, the last portion of the script contains the entry point to test whether the script is being executed as the main module. In this case, it is satisfied and therefore runs the main coroutine using asyncio.run and thus launches the websocket server.

if __name__ == "__main__":
    asyncio.run(main())

Now, let’s consolidate all the code into a single code base. This will provide a complete view of how each component integrates to create a fully functional WebSocket server.

import asyncio
import websockets

connected_clients = {}  # Dictionary to store WebSocket connections and their associated usernames

async def handle_connection(websocket, path):
    # Prompt user for a username
    username = await websocket.recv()
    
    # Add the connection and username to the dictionary
    connected_clients[websocket] = username
    join_message = f"{username} joined the chat"
    print(join_message)

    # Broadcast the join message to all connected clients
    tasks = [client.send(join_message) for client in connected_clients]
    if tasks:  # Check if there are tasks to be awaited
        await asyncio.gather(*tasks)

    try:
        async for message in websocket:
            # Format the message with the username
            formatted_message = f"{username}: {message}"

            # Prepare a list of tasks to send the formatted message to all connected clients except the sender
            tasks = [client.send(formatted_message) for client in connected_clients if client != websocket]
            if tasks:  # Check if there are tasks to be awaited
                await asyncio.gather(*tasks)
    except websockets.ConnectionClosed:
        pass
    finally:
        # Remove the connection and username from the dictionary
        del connected_clients[websocket]
        leave_message = f"{username} left the chat"
        print(leave_message)

        # Broadcast the leave message to all connected clients
        tasks = [client.send(leave_message) for client in connected_clients]
        if tasks:  # Check if there are tasks to be awaited
            await asyncio.gather(*tasks)

async def main():
    # Start the WebSocket server
    async with websockets.serve(handle_connection, "localhost", 8765):
        await asyncio.Future()  # Run forever

if __name__ == "__main__":
    asyncio.run(main())

We have finished setting up our Python WebSocket server-side script. Our next mission will be to create a client-side webpage for the chat system.

Building the WebSocket Client-Side Webpage

The page is designed in such a way that there is a chat area which contains two essential sections: the message view section and the message input section.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Real-Time Chat</title>
    <!-- Bootstrap CSS -->
    <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <div id="chat-container" class="d-flex flex-column">
        <div id="messages" class="overflow-auto p-3">
            <!-- Messages will be displayed here -->
        </div>
        <div id="input-container" class="d-flex p-2 border-top bg-light">
            <input type="text" id="username" class="form-control mr-2" placeholder="Enter your username...">
            <button class="btn btn-primary" onclick="setUsername()">Set Username</button>
        </div>
        <div id="input-container" class="d-flex p-2 border-top bg-light">
            <input type="text" id="message" class="form-control mr-2" placeholder="Type a message..." disabled>
            <button class="btn btn-primary" onclick="sendMessage()" disabled>Send</button>
        </div>
    </div>

    <!-- Bootstrap and WebSocket JavaScript -->
    <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.6/dist/umd/popper.min.js"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
</body>
</html>

The page is overlaid with Bootstrap 4 to enhance its stylistics including its layout and responsiveness so as to make its approach more beautiful and effective.

Moreover, extra CSS is used to give more details to the look of the chat container, the style of message bubbles and the appearance of input areas.

body {
    background-color: #f4f4f9;
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
    margin: 0;
}

#chat-container {
    width: 100%;
    max-width: 600px;
    height: 80vh;
    display: flex;
    flex-direction: column;
    border: 1px solid #ddd;
    border-radius: 8px;
    background-color: #ffffff;
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}

#messages {
    flex: 1;
    padding: 10px;
    overflow-y: auto;
    border-bottom: 1px solid #ddd;
    display: flex;
    flex-direction: column;
}

.message {
    max-width: 75%;
    margin-bottom: 10px;
    padding: 10px;
    border-radius: 8px;
    word-wrap: break-word;
    display: inline-block;
}

.message.sent {
    background-color: #e0ffe0;
    align-self: flex-end;
    text-align: right;
}

.message.received {
    background-color: #e0e0ff;
    align-self: flex-start;
    text-align: left;
}

#input-container {
    display: flex;
    padding: 10px;
    border-top: 1px solid #ddd;
    background-color: #f9f9f9;
}

#message {
    flex: 1;
    margin-right: 10px;
}

Now, let’s include the JavaScript part that takes care of the WebSocket interactions at the client’s end. The script focuses first and foremost on creating a connection to WebSocket.

const ws = new WebSocket('ws://localhost:8765');
        let username = '';

        ws.onmessage = function (event) {
            const messages = document.getElementById('messages');
            const message = document.createElement('div');
            message.className = 'message received';
            message.textContent = event.data;
            messages.appendChild(message);
            messages.scrollTop = messages.scrollHeight; // Auto-scroll to bottom
        };

        function setUsername() {
            const input = document.getElementById('username');
            username = input.value.trim();
            if (username) {
                // Send the username to the server
                ws.send(username);

                // Enable the message input and send button
                document.getElementById('message').disabled = false;
                document.querySelector('button[onclick="sendMessage()"]').disabled = false;

                // Hide the username input
                document.getElementById('username').style.display = 'none';
                document.querySelector('button[onclick="setUsername()"]').style.display = 'none';
            }
        }

        function sendMessage() {
            const input = document.getElementById('message');
            const message = input.value.trim();
            if (message) {
                const messageDiv = document.createElement('div');
                messageDiv.className = 'message sent';
                messageDiv.textContent = `${username}: ${message}`;
                document.getElementById('messages').appendChild(messageDiv);
                ws.send(message);
                input.value = '';
                document.getElementById('messages').scrollTop = document.getElementById('messages').scrollHeight; // Auto-scroll to bottom
            }
        }

This script opens a bi-directional channel between the client and the server and the address for this connection is ws://localhost:8765.

It has also a mechanism for accepting new messages, which are put in the chat window with a ‘received’ class, and provides that the chat is scrolled to the latest message.

A unique name is required from users before they are able to send messages; after filling in such names, the username input fields and the ‘Set Username’ button disappears while the message input and the Send button is enabled.

Once the message is sent, it appears in the chat with a ‘sent’ class and is sent using the websocket server where the chat still pans to the latest message.

Conclusion

The last stage in this tutorial is that we will run the Python script to turn on the WebSocket server. That would set the server up for accepting client connections and exchanging messages.

Bash
$ python3 server.py

After the server has been started, we shall then open the webpage, which is the HTML client. The client’s webpage will be connected to the web socket server on its own such that the users will be able to enter the chatroom and send and receive messages as they wish.

By this system, in which the server is started first and then the client is made lighter, we are able to enhance and maintain the performance of real-time chat between users, as demonstrated in the video below.

python chat room
output.jpg

🧷Explore the complete source code on GitHub


Light
×