I have been working on a project and using Flask API with Socket.io. This API supports rooms as a built in feature – so making a chat becomes very easy. I decided to create a chat with private chat feature and multiple rooms and see how difficult it would be to implement. Here is the final product below.
SocketIO vs. WebSocket
One thing that is always confusing is the difference between WebSocket and SocketIO. They both have same kind of application. WebSocket is a low level API that opens a two way TCP channel between the client (normally web browser) and server. Socket.io is built on top of WebSocket. However in case WebSocket is not available, it can also fallback to other mechanisms for the two way communication e.g. Ajax long polling. For ease of development socket.io is the way to go. It will auto connect when connection is dropped. It allows for message broadcast as well as ‘rooms’ are available by default. Socket.io also supports ‘namespaces’ for logical message separation over the same TCP channel.
We can leverage the ‘rooms’ available by default in our chat application. This will allow us to open up multiple group chats within the same server.
Installation
Apart for socket.io, we will also use Flask for Python for this project. So we will be using the Flask wrapper for socket.io.
We will need to install the following additional Python libraries (use pip to install).
- Flask
- Flask-socketio
- Eventlet
Design Goals
Let’s start with the design goals. We will use the following.
- Chat will support any number of users
- Users can log into any rooms they want to create
- Chat will also be within the same room
- At any point of time there can be more that one room available
- We should be able to do a one-to-one private chat within a room
- Users can send messages and images (no other files will be supported)
Let’s start by creating a new Python project. As always, I will check in the POC code to my Github public account.
Python Server
We will start by importing all required libraries and initializing globals. This will include creating an instance of new Flask application and SocketIO.
import json import logging import eventlet from flask import Flask, request from flask_socketio import SocketIO, send, join_room, leave_room PORT = 5005 eventlet.monkey_patch() app = Flask(__name__) app.config['SECRET_KEY'] = 'abc123def456' sio = SocketIO(app, cors_allowed_origins='*', async_mode='eventlet') users = {}
We have to maintain a list of users who have joined the chat and also what rooms they belong to. For this we have a dictionary defined as users. Next we write a few utility methods that is used to get user information.
""" Adds a new user to global dictionary username: Handle for this user roomnane: Room of the user sid: Session ID for the user """ def add_user(username, roomname, sid): user = {'name': username.upper(), 'room': roomname.upper(), 'sid': sid} join_room(roomname) users[sid] = user """ Gets a user based on the session ID sid: SID for the user """ def get_user_by_sid(sid): if sid in users: return users[sid] return None """ gets a user based on user handle name: Handle for this user """ def get_user_by_name(name): for key, value in users.items(): if value['name'] == name.upper(): return value return None """ Deletes a user from global dictionary roomnane: Room of the user sid: SID for the user """ def del_user(sid, roomname): elm = None if sid in users: elm = users[sid] leave_room(roomname) del users[sid] return elm """ Gets all users in a Room roomnane: Room of the user """ def get_all_users(roomname): all_users = [] for key, value in users.items(): if value['room'] == roomname.upper(): all_users.append(value['name']) return all_users
Chat message formats
Before proceeding further, let’s define the message formats used for chat communication. We will need to handle several types of messages. There will be messages about user joining or leaving, messages dealing with what users are in a room, messages for chat and transferring images.
The first type of message to handle is users joining and leaving rooms. In this case we will be sending the user and room. We also define an event tag that will be used to differentiate between various messages.
{ "event": "join/ leave", "user": "<username>", "room": "<roomname>" }
The second message we define is relatively simpler. We use this message to show all users in a room.
{ "event": "users", "room": "<roomname>" }
Now we define a message to send chat message. This will have both room name and username. API will send user tag for a private one-to-message. In this case, user tag will contain the requested user handle. If neither user or room is sent, this message will be broadcast.
{ "event": "chat", "user": "<@ username>", "room": "<roomname>", "message": "<message>" }
Last but not the least, we will define the message format for sending images. Tag bimage is used to send a base64 image source.
{ "event": "image", "room": "<roomname>", "bimage": "<base64 image>" }
Now that all of these is out of the way, we can concentrate on the main program loop.
SocketIO Event Handling
@sio.on('chat') def handle_chat(message): logger.debug('Chat for session % received: %s' % (request.sid, message)) try: evt = message['event'] if 'event' in message else 'users' if evt == 'join': user = message['user'] if 'user' in message else 'JDOE' room = message['room'] if 'room' in message else 'PARKED' add_user(user, room, request.sid) msg2send = { 'type': 'newuser', 'message': 'New User %s joined room %s' % (user, room) } sio.emit('chat', msg2send, room=room) elif evt == 'leave': user = message['user'] if 'user' in message else 'JDOE' room = message['room'] if 'room' in message else 'PARKED' del_user(request.sid, room) msg2send = { 'type': 'deluser', 'message': 'User %s left room %s' % (user, room) } sio.emit('chat', msg2send, room=room) elif evt == 'users': room = message['room'] if 'room' in message else 'PARKED' all_users = get_all_users(room) msg2send = { 'type': 'users', 'message': 'Users in room: ' + str(all_users) } sio.emit('chat', msg2send, room=request.sid) elif evt == 'chat': me = get_user_by_sid(request.sid) room = message['room'] if 'room' in message else None user = message['user'] if 'user' in message else None text = message['text'] if 'text' in message else 'Knock! Knock!' msg2send = { 'type': 'chat', 'from': me['name'], 'message': text } if room is None: # Broadcast logger.debug('Broadcasting message...') sio.emit('chat', msg2send, skip_sid=request.sid, broadcast=True) else: if user is None: # Send to everyone in room sio.emit('chat', msg2send, skip_sid=request.sid, room=room) else: elm = get_user_by_name(user) if elm is not None: logger.debug('Sending chat message to user %s at socket: %s' % (user, elm['sid'])) sio.emit('chat', msg2send, room=elm['sid'], skip_sid=request.sid) else: logger.error('User requested %s, not found...' % user) elif evt == 'image': me = get_user_by_sid(request.sid) msg2send = { 'type': 'image', 'from': me['name'], 'bimage': message['bimage'] } sio.emit('chat', msg2send, skip_sid=request.sid, room=message['room']) except json.decoder.JSONDecodeError as jse: send(json.dumps({ 'code': 99, 'message': 'JSON Parse failed' }))
This is the chat server code. I have enabled line numbers so that it is easier to reference. Join event starts at line 6. Here we add the user to the room and also to our dictionary. Conversely, for leave event, we will delete the user from dictionary and force them out of the room.
Chat event starts at line 32. This is where all text chat happens. If no room is present in the message, we will broadcast this text. If a room is found and also an @ user, we will only send this text to the requested user. See line 54. For all other cases, we will send the message to all recipients in the room (except sender).
Image starts at line 57. This is fairly basic. It takes the message and forwards it across to all recipients in the room (except sender).
Client
I decided to put the client in a separate JavaScript/ HTML page instead of using a template location from Flask. The primary reason for this is that in production you will normally server static pages from nginx.
<body> <div class="container"> <div class="userlogin"> <label for="username">Name</label> <input type="text" id="username" placeholder="User Name" onkeyup="this.value = this.value.toUpperCase();"></text> <input type="text" id="roomname" placeholder="Room Name" onkeyup="this.value = this.value.toUpperCase();"></text> <br /> <button id="btnJoin">Join</button> <button id="btnLeave">Leave</button> <button id="btnUsers">Users</button> </div> <hr /> <div class="userlogin"> <label for="sendmsg">Message</label> <input type="text" id="sendmsg" placeholder="Send Message"></text> <button id="btnSendMsg">Send</button> <input type="file" id="imgfile" class="file" accept="image/*"> <button id="btnSendAtt">Image</button> </div> <div id="chatbox" class="chatbox"></div> </div> </body>

We distribute the space in two sections. The top section will be used for user login. We will have user handle and room entry. Second section will be the real chat section. This is where user will be communicating with other users. Additionally there is a button to send images. The space below is used to show all chat messages.
We have included the following two javascript libraries.
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/3.1.1/socket.io.min.js"></script> <script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
We open socketio as soon as the page loads. Rest of the events are as defined below.
var connectWs = function() { const sockt = io(SOCKS_URL, { reconnectionDelayMax: 10000 }); sockt.on('connect', () => { connected = true; }); sockt.on('disconnect', () => { connected = false; }); sockt.on('chat', (data) => { if (data.type === 'user' || data.type === 'users' || data.type === 'newuser' || data.type === 'deluser') { addInfo(data.message); } else if (data.type === 'chat') { addMessage(data.from, data.message, true); } else if (data.type === 'image') { addImage(data.from, data.bimage, true); } }); sockt.on('message', (data) => { console.log(data); }); return sockt; }
I am adding all the implementations below.
$("#btnJoin").click(function() { user = $("#username").val().trim(); room = $("#roomname").val().trim(); if (user === '' || room === '') { alert('Both room and user names are mandatory'); } else { // Validate jsn = { 'event': 'join', 'user': user, 'room': room }; socks.emit('chat', jsn); $("#btnJoin").prop("disabled", true); $("#username").prop("readonly", true); $("#roomname").prop("readonly", true); } }); $("#btnLeave").click(function() { user = $("#username").val().trim(); room = $("#roomname").val().trim(); if (user === '' || room === '') { alert('Both room and user names are mandatory'); } else { // Validate jsn = { 'event': 'leave', 'user': user, 'room': room }; socks.emit('chat', jsn); $("#btnJoin").prop("disabled", false); $("#username").prop("readonly", false); $("#roomname").prop("readonly", false); } });
Join and Leave works pretty much the same way. In case of join we send the user handle and room name to the server for adding to the chat. For leave request, we also send similar request. However, in this case the event sent is leave.
$("#btnSendMsg").click(function() { if (!connected) { alert('No socket connection available!'); } else { msg = $("#sendmsg").val().trim(); addMessage("YOU", msg); $("#sendmsg").val(''); if (msg.startsWith('@')) { user = msg.substring(1, msg.indexOf(' ')); jsn = { 'event': 'chat', 'user': user.toUpperCase(), 'room': $("#roomname").val(), 'text': msg.substring(msg.indexOf(' ') + 1, msg.length + 1) }; socks.emit('chat', jsn); } else { jsn = { 'event': 'chat', 'room': $("#roomname").val(), 'text': msg }; socks.emit('chat', jsn); } } });
For sending messages, we check to see if this is a private chat. If we find a @ sign, we will extract the user handle following it. We send this user to the API as the to user.
$('#btnSendAtt').click(function(e) { $('#imgfile').trigger('click'); console.log($('#imgfile')); }); $('#imgfile').change(function(e) { var fileobj = e.target.files[0] filename = fileobj.name; filemime = fileobj.type; if (!filemime.startsWith("image/")) { alert('Only Images supported at this time'); } else { var reader = new FileReader(); reader.onload = function(e) { addImage("YOU", e.target.result); jsn = { 'event': 'image', 'room': $("#roomname").val(), 'bimage': e.target.result }; socks.emit('chat', jsn); }; reader.readAsDataURL(fileobj); } });
This final code is for sending images. We read the image and send it to the API base64 encoded.
Conclusion
That is all required for a chat implementation. Even though it looks like a lot of code, it does not take too much time to implement. Ciao for now!