Dive into Python’s asyncio, part 4 – simple chat with Sanic
Let's roll with something practical, namely a simple chat application using Sanic framework mentioned in previous post.
Sanic supports websockets out of the box thanks to the websockets library. It's super easy to write a handler function by using decorator (#1):
@app.websocket('/feed') # 1
async def feed(request, ws):
while True:
data = await ws.recv()
await ws.send(data)This is what code looks like for a simple echo server. No error handling or communication with external world is done here.
In order to provide a functional chat service, we need a little more. Our clients should be able to talk to themselves. A concept that represents a virtual place where chat talks are carried out is called a room. A room can be joined and left. In my example it also takes new messages and broadcasts them to all room members.
class Room:
def join(self, client):
pass
def leave(self, client):
pass
async def send_message(self, message, sender):
pass
def __len__(self):
passThis is how room class scaffolding might look like. For a little convenience (mainly during tests) I added __len__ method, so we can easily ask room for its number of members using pythonic idiom - len(room).
The most interesting part is the send_message method:
async def send_message(self, message):
for receiver in self.clients: #1
try:
await receiver.send(message) #2
except ConnectionClosed: # 3
self.leave(receiver) # 4We simply iterate over connected folks (#1), sending them message passed as an argument (#2).
We must not forget about error handling - (#3). Disconnection just a nanosecond before sending a message is an ordinary situation. In such case we remove a flawed client from our room (#4). Please take a note that operation of removing member of a collection (self.clients) during iteration may be dangerous and will throw exceptions. In this concrete implementation I used a list. It does not complain about removing subsequent items.
One vital part of chat code remains to be uncovered - websocket handler function. Here it is:
@app.websocket('/chat')
async def feed(request, ws):
global_room.join(ws) # 1
while True:
try:
message = await ws.recv() # 2
except ConnectionClosed: # 3
global_room.leave(ws)
break # 4
else:
await global_room.send_message(message) # 5
At the beginning (#1) we add every new guy to our room, so he/she can receive messages from the moment of opening the websocket connection. In the meantime we wait for any incoming message (#2). We need the same error handling (#3) as seen before in send_message implementation. This may look like a redundancy, but it covers completely different use case - for example there is only one client that enters empty room and after a while leaves. This would raise exception in #2. So we break the loop (#3) allowing Sanic to finalize connection. Otherwise, we broadcast received message to everyone in the room (#5).
Further considerations:
We might not want to wait in line marked #5 for all other room's members to receive our message, since one slow client would block processing incoming messages. Wrapping this stuff with asyncio.Task would help.
Full sources: here