|
| 1 | +# FastAPI Day 12: Real-Time Chat with WebSockets |
| 2 | + |
| 3 | +Welcome to **Day 12** of the FastAPI tutorial series! Today, we're diving into the world of real-time communication with WebSockets. You'll learn how to build a simple chat application where multiple clients can connect and exchange messages in real-time. |
| 4 | + |
| 5 | +--- |
| 6 | + |
| 7 | +## What You'll Learn |
| 8 | + |
| 9 | +- **WebSocket Endpoints**: Implement a WebSocket endpoint in FastAPI to handle persistent connections. |
| 10 | +- **Connection Management**: Create a `ConnectionManager` to keep track of active WebSocket connections. |
| 11 | +- **Broadcasting Messages**: Broadcast messages to all connected clients to enable real-time chat functionality. |
| 12 | +- **Client-Side Integration**: Build a simple HTML and JavaScript client to interact with your WebSocket server. |
| 13 | +- **Testing WebSockets**: Write tests for your WebSocket endpoint using `pytest` and FastAPI's `TestClient`. |
| 14 | + |
| 15 | +--- |
| 16 | + |
| 17 | +## Key Concepts |
| 18 | + |
| 19 | +For this tutorial, we've built a simple chat server with a corresponding client. |
| 20 | + |
| 21 | +- `src/main.py`: Contains the main FastAPI application, the `ConnectionManager`, and the WebSocket endpoint. |
| 22 | +- `client/index.html`: A simple HTML file with JavaScript to connect to the WebSocket and handle messaging. |
| 23 | +- `tests/test_main.py`: Contains tests for the WebSocket endpoint. |
| 24 | + |
| 25 | +### 1. The Connection Manager |
| 26 | + |
| 27 | +To manage all active WebSocket connections, we use a `ConnectionManager` class. This class is responsible for adding new connections, removing disconnected ones, and broadcasting messages to all clients. |
| 28 | + |
| 29 | +```python-beginner/workspace/7_framework/fastapi/day12/server/src/main.py#L4-L18 |
| 30 | +class ConnectionManager: |
| 31 | + def __init__(self): |
| 32 | + self.active_connections: List[WebSocket] = [] |
| 33 | +
|
| 34 | + async def connect(self, websocket: WebSocket): |
| 35 | + await websocket.accept() |
| 36 | + self.active_connections.append(websocket) |
| 37 | +
|
| 38 | + def disconnect(self, websocket: WebSocket): |
| 39 | + self.active_connections.remove(websocket) |
| 40 | +
|
| 41 | + async def broadcast(self, message: str): |
| 42 | + for connection in self.active_connections: |
| 43 | + await connection.send_text(message) |
| 44 | +``` |
| 45 | + |
| 46 | +### 2. The WebSocket Endpoint |
| 47 | + |
| 48 | +The core of our application is the WebSocket endpoint, defined with the `@app.websocket()` decorator. This endpoint handles incoming connections, listens for messages, and broadcasts them to all other clients. |
| 49 | + |
| 50 | +```python-beginner/workspace/7_framework/fastapi/day12/server/src/main.py#L26-L38 |
| 51 | +@app.websocket("/ws/{client_id}") |
| 52 | +async def websocket_endpoint(websocket: WebSocket, client_id: int): |
| 53 | + """ |
| 54 | + This endpoint handles WebSocket connections for the chat room. |
| 55 | + """ |
| 56 | + await manager.connect(websocket) |
| 57 | + await manager.broadcast(f"Client #{client_id} has entered the chat") |
| 58 | + try: |
| 59 | + while True: |
| 60 | + data = await websocket.receive_text() |
| 61 | + await manager.broadcast(f"Client #{client_id}: {data}") |
| 62 | + except WebSocketDisconnect: |
| 63 | + manager.disconnect(websocket) |
| 64 | + await manager.broadcast(f"Client #{client_id} has left the chat") |
| 65 | +``` |
| 66 | + |
| 67 | +### 3. Client-Side Implementation |
| 68 | + |
| 69 | +The `client/index.html` file provides a simple user interface for the chat. It uses JavaScript's `WebSocket` API to connect to the server, send messages, and display received messages. |
| 70 | + |
| 71 | +```python-beginner/workspace/7_framework/fastapi/day12/client/index.html#L48-L69 |
| 72 | +<script> |
| 73 | + var clientId = Date.now(); |
| 74 | + document.querySelector("#ws-id").textContent = clientId; |
| 75 | + var ws = new WebSocket(`ws://localhost:8000/ws/${clientId}`); |
| 76 | + ws.onmessage = function (event) { |
| 77 | + var messages = document.getElementById("messages"); |
| 78 | + var message = document.createElement("li"); |
| 79 | + var content = document.createTextNode(event.data); |
| 80 | + message.appendChild(content); |
| 81 | + messages.appendChild(message); |
| 82 | + messages.scrollTop = messages.scrollHeight; |
| 83 | + }; |
| 84 | + function sendMessage(event) { |
| 85 | + var input = document.getElementById("messageText"); |
| 86 | + ws.send(input.value); |
| 87 | + input.value = ""; |
| 88 | + event.preventDefault(); |
| 89 | + } |
| 90 | +</script> |
| 91 | +``` |
| 92 | + |
| 93 | +### 4. Testing WebSockets |
| 94 | + |
| 95 | +Testing WebSockets is straightforward with FastAPI's `TestClient`. We can use `client.websocket_connect()` to simulate a client connection and test the message flow. |
| 96 | + |
| 97 | +```python-beginner/workspace/7_framework/fastapi/day12/server/tests/test_main.py#L14-L32 |
| 98 | +def test_websocket_broadcast(client): |
| 99 | + """ |
| 100 | + Test the WebSocket broadcasting functionality. |
| 101 | + - It connects two clients to the /ws/{client_id} endpoint. |
| 102 | + - It checks the initial connection messages in the correct order. |
| 103 | + - Has one client send a message. |
| 104 | + - Asserts that both clients receive the broadcasted message. |
| 105 | + """ |
| 106 | + with client.websocket_connect("/ws/1") as websocket1: |
| 107 | + # Client 1 connects and receives its own connection message |
| 108 | + assert websocket1.receive_text() == "Client #1 has entered the chat" |
| 109 | +
|
| 110 | + with client.websocket_connect("/ws/2") as websocket2: |
| 111 | + # Client 2 connects, and both clients receive the announcement |
| 112 | + assert websocket1.receive_text() == "Client #2 has entered the chat" |
| 113 | + assert websocket2.receive_text() == "Client #2 has entered the chat" |
| 114 | +
|
| 115 | + # Test broadcasting a message from client 1 |
| 116 | + websocket1.send_text("Hello from client 1") |
| 117 | + assert websocket1.receive_text() == "Client #1: Hello from client 1" |
| 118 | + assert websocket2.receive_text() == "Client #1: Hello from client 1" |
| 119 | +``` |
| 120 | + |
| 121 | +--- |
| 122 | + |
| 123 | +## Next Steps |
| 124 | + |
| 125 | +- Navigate to the server directory and install the dependencies: `cd server && pip install -r requirements.txt`. |
| 126 | +- Run the application with `uvicorn src.main:app --reload`. |
| 127 | +- Open `client/index.html` in your web browser. You can open multiple tabs to simulate multiple clients. |
| 128 | +- Send messages and see them appear in all open tabs. |
| 129 | +- Run the automated tests with `python -m pytest`. |
| 130 | + |
| 131 | +--- |
0 commit comments