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).
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: here3