Scalable websocket with FastAPI and Broadcaster[Redis]

Git Commit: websocket based code sharing
Note: The earlier websocket and HTTP endpoints will collide with these new endpoints, make sure you comment/remove them.

In this tutorial we are going to build a codesharing application, It lets us share code in real-time. It could be used for interviewing or for sharing a snippet with colleagues. Our earlier implementation of websocket is not scalable as everything is handled in memory, in a single dictionary, it will only work while the process is running, and will only work with a single process. Let's use the suggested broadcaster library for a more robust implementation.

 

Let's first install the basic requirements. Put these additional requirements in requirements.txt file.

broadcaster==0.2.0
asyncio-redis==0.16.0 

Now, we can utilize broadcaster and its redis based pub-sub capabilities.
 

from broadcaster import Broadcast

from fastapi import FastAPI, WebSocket, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from fastapi.concurrency import run_until_first_complete


broadcast = Broadcast("redis://redis:6379")
templates = Jinja2Templates(directory="templates")
app = FastAPI(on_startup=[broadcast.connect], on_shutdown=[broadcast.disconnect])


@app.get("/{room_id}/", response_class=HTMLResponse)
async def get(request: Request, room_id:str):
    context = {"room_id":room_id,"request":request}
    return templates.TemplateResponse("codeshare.html",context)


async def chatroom_ws_receiver(websocket: WebSocket, room_id: str):
    async for message in websocket.iter_text():
        await broadcast.publish(channel=f"chatroom_{room_id}", message=message)


async def chatroom_ws_sender(websocket: WebSocket, room_id: str):
    async with broadcast.subscribe(channel=f"chatroom_{room_id}") as subscriber:
        async for event in subscriber:
            await websocket.send_text(event.message)


@app.websocket("/{room_id}")
async def websocket_chat(websocket: WebSocket, room_id: str):
    await websocket.accept()
    await run_until_first_complete(
        (chatroom_ws_receiver, {"websocket": websocket, "room_id":room_id}),
        (chatroom_ws_sender, {"websocket": websocket, "room_id":room_id}),
    )

A Broadcast instance is created to facilitate message broadcasting, configured to communicate with a Redis server at the address redis://redis:6379 as defined in our docker-compose service. The broadcast.connect function is set to run during startup, ensuring that the broadcasting mechanism is properly established. Conversely, the broadcast.disconnect function will be executed during shutdown, allowing for a graceful closure of the broadcasting infrastructure. 

Broadcaster is utilizing Redis' pub-sub capabilities internally. The code employs the run_until_first_complete function to concurrently execute the chatroom_ws_receiver and chatroom_ws_sender functions. This concurrent execution ensures that the chatroom can handle both incoming and outgoing messages simultaneously, enabling a seamless real-time chat experience for users.
Let's include the below HTML template for the UI. templates > codeshare.html
 

<!DOCTYPE html>
<html>
    <head>
        <title>CodeShare</title>
        <script src="https://cdn.tailwindcss.com"></script>
    </head>
    <body>
        <div id="room-id" style="display: none;">{{room_id}}</div>
        <form>
            <div class="w-full mb-4 border border-gray-200 rounded-lg bg-gray-50 dark:bg-gray-700 dark:border-gray-600">
                <div class="flex items-center justify-between px-3 py-2 border-b dark:border-gray-600">
                    <div class="flex flex-wrap items-center divide-gray-200 sm:divide-x dark:divide-gray-600">
                        <div class="flex items-center space-x-1 sm:pr-4">
                          <h1 class="text-rose-500 font-bold mx-5">Algoholic</h1>
                        </div>
                    </div>
                </div>
                <div class="px-4 py-2 bg-white rounded-b-lg dark:bg-gray-800">
                    <label for="editor" class="sr-only">Publish post</label>
                    <textarea id="editor" rows="8" class="block w-full px-0 text-sm text-gray-800 bg-white border-0 dark:bg-gray-800 focus:ring-0 dark:text-white dark:placeholder-gray-400" placeholder="Paste/type your code..." required></textarea>
                </div>
            </div>
        </form>
        
        <script>
            let editor = document.getElementById("editor")
            room_id = document.getElementById('room-id').textContent
            let ws = new WebSocket(`ws://localhost:8000/{{room_id}}`);
            console.log("Websocket endpoint is",ws)

            ws.onmessage = function(event) {
                old_value = document.getElementById("editor").value
                document.getElementById("editor").value = event.data
            };

            function sendMessage(event) {
                let input = document.getElementById("editor").value
                console.log("Text are value is: ",input)
                ws.send(input)
            }

            editor = document.getElementById("editor")
            editor.addEventListener("paste", sendMessage);
            editor.addEventListener("input", sendMessage);
        </script>
    </body>
</html>

Let's see this in action :D

FastAPITutorial

Brige the gap between Tutorial hell and Industry. We want to bring in the culture of Clean Code, Test Driven Development.

We know, we might make it hard for you but definitely worth the efforts.

Contacts

Refunds:

Refund Policy
Social

Follow us on our social media channels to stay updated.

© Copyright 2022-23 Team FastAPITutorial