Post

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):
        pass

This 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)  # 4

We 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

This post is licensed under CC BY 4.0 by the author.

Comments powered by Disqus.