├── .DS_Store ├── .gitignore ├── .idea ├── FastApiWebSocket.iml ├── inspectionProfiles │ └── profiles_settings.xml ├── misc.xml ├── vcs.xml └── workspace.xml ├── Demo ├── .DS_Store └── FastApi Websocket Demo.mp4 ├── ReadMe.md ├── connection_manager.py ├── main.py ├── models └── register_to_room_model.py ├── requirements.txt └── static ├── index.html └── script.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterAkande/WebSocketChat-FastAPI/9c51287a425f9ab9e3253afbba8d96afc95bdee7/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | __pycache__ 3 | -------------------------------------------------------------------------------- /.idea/FastApiWebSocket.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 10 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/workspace.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 24 | 25 | 26 | 28 | 29 | 31 | { 32 | "associatedIndex": 6 33 | } 34 | 35 | 36 | 37 | 38 | 39 | 42 | { 43 | "keyToString": { 44 | "ASKED_ADD_EXTERNAL_FILES": "true", 45 | "ASKED_MARK_IGNORED_FILES_AS_EXCLUDED": "true", 46 | "ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true", 47 | "DefaultHtmlFileTemplate": "HTML File", 48 | "RunOnceActivity.OpenProjectViewOnStart": "true", 49 | "RunOnceActivity.ShowReadmeOnStart": "true", 50 | "SHARE_PROJECT_CONFIGURATION_FILES": "true", 51 | "git-widget-placeholder": "main", 52 | "last_opened_file_path": "/Users/akandepeter/DevProjects/Backend/personal/FastApiWebSocket", 53 | "settings.editor.selected.configurable": "preferences.pluginManager" 54 | } 55 | } 56 | 57 | 58 | 59 | 78 | 79 | 80 | 81 | 82 | 83 | 1696926283686 84 | 88 | 89 | 96 | 97 | 104 | 107 | 108 | 117 | 118 | 119 | 124 | -------------------------------------------------------------------------------- /Demo/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterAkande/WebSocketChat-FastAPI/9c51287a425f9ab9e3253afbba8d96afc95bdee7/Demo/.DS_Store -------------------------------------------------------------------------------- /Demo/FastApi Websocket Demo.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeterAkande/WebSocketChat-FastAPI/9c51287a425f9ab9e3253afbba8d96afc95bdee7/Demo/FastApi Websocket Demo.mp4 -------------------------------------------------------------------------------- /ReadMe.md: -------------------------------------------------------------------------------- 1 | ## Chat App WebSocket Implementation 2 | 3 | A FastAPI Backend Service that simulates the Websocket needs of a chat app that involves: 4 | - Users being able to Exchange messages with one another 5 | - The concept of rooms or group chats 6 | 7 | This uses an in memory List to keep track of Websocket connections. 8 | To check the version that uses redis PUB/SUB, check the [redis_db_branch](https://github.com/PeterAkande/WebSocketChat-FastAPI/tree/redis_backend) 9 | 10 | 11 | 12 | #### Demo 13 | 14 | https://github.com/PeterAkande/WebSocketChat-FastAPI/assets/64542587/00824b78-3092-4fd1-b7d1-5fefa5e56e27 15 | 16 | #### View Demo On Youtube Instead 17 | [![Watch the video](https://img.youtube.com/vi/G-YG04E1CuY/sddefault.jpg)](https://youtu.be/G-YG04E1CuY) 18 | -------------------------------------------------------------------------------- /connection_manager.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | from typing import Dict, Set, List, Tuple 3 | from operator import itemgetter 4 | 5 | from fastapi import WebSocket 6 | from starlette.websockets import WebSocketState 7 | 8 | 9 | class ConnectionManager: 10 | """ 11 | This would handle all Message broadcast and all connections to the Service. 12 | """ 13 | 14 | def __init__(self): 15 | """ 16 | self.connections would be an in memory db of the users connected to a DB. 17 | It would function with an forward index like structure. 18 | 19 | Something like: 20 | { 21 | room_id: {user1, user2, user3}, 22 | room_id_1: {user1, user3} 23 | } 24 | """ 25 | self.connections: Dict[str, Set[str]] = {} # The Room and the set of users connected 26 | self.user_connections: Dict[str, WebSocket] = {} # the user connections 27 | 28 | async def save_user_connection_record(self, ws_connection: WebSocket, user_id: str): 29 | """ 30 | This would save a user record to the Websocket connections that the connection manager is currently keeping 31 | """ 32 | 33 | self.user_connections[user_id] = ws_connection 34 | await self.send_message_to_ws_connection(message="Connection successful", ws_connection=ws_connection) 35 | 36 | async def add_user_connection_to_room(self, room_id: str, user_id: str) -> (bool, str): 37 | """ 38 | This function would add a user to a Room 39 | This user id would be added to the ids of the users that are listening to a room 40 | """ 41 | 42 | # Try to get the user details from the dictionary of connections 43 | # It's a HashMap, so the Time Complexity is O(1) 44 | 45 | user_ws_connection = self.user_connections.get(user_id, None) 46 | 47 | if user_ws_connection is None: 48 | return False, "User Not Found" 49 | 50 | # The user connection was gotten. 51 | 52 | is_connection_active = await self.check_if_ws_connection_is_still_active(user_ws_connection) 53 | 54 | if not is_connection_active: 55 | self.user_connections.pop(user_id) 56 | return False, "Connection not Active" 57 | 58 | # Check if the room ID exists. If it does not, create one. 59 | if room_id not in self.connections.keys(): 60 | self.connections[room_id] = {user_id} 61 | else: 62 | self.connections[room_id].add(user_id) 63 | 64 | return True, "Connection Successful" 65 | 66 | async def check_if_ws_connection_is_still_active(self, ws_connection: WebSocket, message=".") -> bool: 67 | """ 68 | This function would check if the connection is still active. It tries to send a message 69 | """ 70 | 71 | if not ( 72 | ws_connection.application_state == WebSocketState.CONNECTED and ws_connection.client_state == WebSocketState.CONNECTED): 73 | return False 74 | 75 | # Try to send a message 76 | try: 77 | await ws_connection.send_text(message) 78 | except RuntimeError: 79 | return False 80 | 81 | except Exception as e: 82 | traceback.print_exc() 83 | return False 84 | 85 | return True 86 | 87 | async def send_message_to_room(self, room_id: str, message: str): 88 | """ 89 | Messages in this Program are Texts. 90 | """ 91 | 92 | room_connections = self.connections.get(room_id, {}) 93 | if len(room_connections) == 0: 94 | return 95 | users_ws_connections = itemgetter(*room_connections)(self.user_connections) 96 | 97 | print(users_ws_connections) 98 | if type(users_ws_connections) is not tuple: 99 | users_ws_connections = [users_ws_connections] 100 | 101 | for connection in users_ws_connections: 102 | is_sent, sent_message_response_info = await self.send_message_to_ws_connection( 103 | message=f"Room {room_id} --> {message}", ws_connection=connection) 104 | 105 | # It can be chosen to remove the connection from the self.user_connections if is_sent is False. 106 | 107 | async def send_message_to_ws_connection(self, message: str, ws_connection: WebSocket) -> (bool, str): 108 | try: 109 | await ws_connection.send_text(message) 110 | return True, "Message sent!" 111 | except RuntimeError: 112 | return False, "Message Not Sent, Websocket is disconnected" 113 | 114 | except Exception as e: 115 | traceback.print_exc() 116 | 117 | return False, "Error Sending Message" 118 | 119 | def remove_user_connection(self, user_id): 120 | self.user_connections.pop(user_id) 121 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | from typing import Dict, Set 3 | 4 | from fastapi import FastAPI, WebSocket, WebSocketException, WebSocketDisconnect 5 | from fastapi.responses import HTMLResponse, FileResponse 6 | from fastapi.exceptions import HTTPException 7 | 8 | from connection_manager import ConnectionManager 9 | from models.register_to_room_model import RegisterToRoom 10 | 11 | number_of_socket_connections = 0 12 | connection_manager = ConnectionManager() 13 | 14 | app = FastAPI() 15 | 16 | 17 | @app.get("/") 18 | async def get(): 19 | return FileResponse("static/index.html") 20 | 21 | 22 | @app.post("/register_to_room/") 23 | async def register_user_to_room(body: RegisterToRoom): 24 | """ 25 | This route would register a user to a route 26 | """ 27 | 28 | is_added, message = await connection_manager.add_user_connection_to_room(user_id=body.user_id, room_id=body.room_id) 29 | 30 | print(connection_manager.user_connections) 31 | print(connection_manager.connections) 32 | # Do whatever you like with is_added later 33 | 34 | if not is_added: 35 | raise HTTPException(detail={"message": message}, status_code=400) 36 | 37 | return { 38 | "message": message 39 | } 40 | 41 | 42 | @app.websocket("/ws/{user_id}") 43 | async def websocket_endpoint(user_id: str, websocket: WebSocket): 44 | await websocket.accept() 45 | global number_of_socket_connections 46 | 47 | # Add the user to the connection stack 48 | await connection_manager.save_user_connection_record(user_id=user_id, ws_connection=websocket) 49 | try: 50 | number_of_socket_connections += 1 51 | while True: 52 | data = await websocket.receive_json() 53 | 54 | room_id = data["room_id"] 55 | message = data["message"] 56 | 57 | await connection_manager.send_message_to_room(message=message, room_id=room_id) 58 | except WebSocketDisconnect: 59 | # Remove the user from the connection stack 60 | connection_manager.remove_user_connection(user_id=user_id) 61 | 62 | except WebSocketException as e: 63 | traceback.print_exc() 64 | 65 | print("An error occurred and i dont know the details") 66 | -------------------------------------------------------------------------------- /models/register_to_room_model.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class RegisterToRoom(BaseModel): 5 | user_id: str 6 | room_id: str 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | annotated-types==0.6.0 2 | anyio==3.7.1 3 | click==8.1.7 4 | exceptiongroup==1.1.3 5 | fastapi==0.103.2 6 | h11==0.14.0 7 | httptools==0.6.0 8 | idna==3.4 9 | pydantic==2.4.2 10 | pydantic-core==2.10.1 11 | python-dotenv==1.0.0 12 | PyYAML==6.0.1 13 | sniffio==1.3.0 14 | starlette==0.27.0 15 | typing-extensions==4.8.0 16 | uvicorn==0.23.2 17 | uvloop==0.17.0 18 | watchfiles==0.20.0 19 | websockets==11.0.3 20 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Chat 5 | 6 | 7 |

WebSocket Chat

8 |
9 | 10 | 11 | 12 |
13 | 14 |
15 | 16 | 17 | 18 | 19 |
20 | 21 |
22 | 23 | 24 | 25 |
26 | 27 |
28 | 29 |
30 |
31 | 32 |
33 | 35 | 157 | 158 | -------------------------------------------------------------------------------- /static/script.js: -------------------------------------------------------------------------------- 1 | var ws = null 2 | var userId = null 3 | 4 | function connectToWS(event) { 5 | 6 | var userIdField = document.getElementById("userIdText") 7 | var connectionInfoSpan = document.getElementById("connectionInfo") 8 | 9 | if (userIdField.value.length == 0) { 10 | // The String is empty 11 | connectionInfoSpan.innerHTML = "Please input User Unique id" 12 | connectionInfoSpan.style.color = "red" 13 | 14 | event.preventDefault() 15 | return; 16 | } 17 | 18 | userId = userIdField.value 19 | connectionInfoSpan.innerHTML = "Connection Made Successfully!" 20 | connectionInfoSpan.style.color = "green" 21 | 22 | //The user Id was gotten 23 | //Initiate the connection 24 | let wsUrl = "ws://localhost:8000/ws/" + userId 25 | ws = new WebSocket(wsUrl); 26 | 27 | ws.onmessage = function (event) { 28 | var messages = document.getElementById('messages') 29 | var message = document.createElement('li') 30 | var content = document.createTextNode(event.data) 31 | message.appendChild(content) 32 | messages.appendChild(message) 33 | }; 34 | event.preventDefault() 35 | } 36 | 37 | async function listenToRoom(event) { 38 | var roomIdField = document.getElementById("connectRoomIdText") 39 | var connectRoomInfoSpan = document.getElementById("connectRoomInfo") 40 | 41 | if (roomIdField.value.length == 0) { 42 | connectRoomInfoSpan.innerHTML = "Please Enter the room Id" 43 | event.preventDefault() 44 | return; 45 | } 46 | 47 | 48 | if (userId == null) { 49 | connectRoomInfoSpan.innerHTML = "User Id not Set" 50 | event.preventDefault() 51 | return; 52 | } 53 | 54 | //The whole data has now been gotten, Make the request now 55 | 56 | response = await fetch("/register_to_room", { 57 | method: "POST", 58 | body: JSON.stringify({ 59 | user_id: userId, 60 | room_id: roomIdField.value, 61 | }), 62 | headers: { 63 | "Content-type": "application/json; charset=UTF-8" 64 | } 65 | } 66 | ) 67 | 68 | if (!response.ok) { 69 | connectRoomInfoSpan.innerHTML = "Error Joining Room" 70 | event.preventDefault() 71 | return; 72 | } 73 | 74 | res = await response.json() 75 | 76 | connectRoomInfoSpan.innerHTML = "Room Joined Successfully!" 77 | connectRoomInfoSpan.style.color = "green" 78 | event.preventDefault() 79 | 80 | 81 | } 82 | function sendMessage(event) { 83 | var input = document.getElementById("messageText") 84 | var roomId = document.getElementById("roomIDMessageText") 85 | var sendMessageInfo = document.getElementById("sendMessageInfo") 86 | 87 | 88 | if (roomId.value.length == 0) { 89 | sendMessageInfo.innerHTML = "Please Enter the room Id" 90 | event.preventDefault() 91 | return false; 92 | } 93 | 94 | if (input.value.length == 0) { 95 | sendMessageInfo.innerHTML = "Please enter a valid Message" 96 | event.preventDefault() 97 | return false; 98 | } 99 | 100 | 101 | if (ws == null) { 102 | sendMessageInfo.innerHTML = "No Connection Made" 103 | event.preventDefault() 104 | return false; 105 | } 106 | 107 | var message = { 108 | room_id: roomId.value, 109 | message: input.value, 110 | } 111 | 112 | ws.send(JSON.stringify(message)) 113 | 114 | input.value = '' 115 | sendMessageInfo.innerHTML = "" 116 | event.preventDefault() 117 | } 118 | --------------------------------------------------------------------------------