Django websocket send messages async in a loop
I'm trying to create a websocket channel using the AsyncJsonWebsocketConsumer
class. I want to send messages in a loop after every 5 seconds and my consumer of the websocket channel (ReactJS App Client) should also receive the messages after every 5 seconds. Though my consumer app receives all the messages at once after the final message is sent or when the receive_json
function is completed. I cannot seem to fix this. Need help. Thanks
test.py
from channels.generic.websocket import AsyncJsonWebsocketConsumer
class TestController(AsyncJsonWebsocketConsumer):
async def connect(self):
await self.accept()
async def receive_json(self, content: dict):
for i in range(0, 5):
await self.send_json({
"text": f"Loop - {i + 1}"
})
sleep(5)
await self.send_json({
"text": {
"type": "finished"
}
})
async def disconnect(self, close_code):
print()
print("DISCONNECTED")
print(close_code)
print()
routing.py
from django.urls import path
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.security.websocket import AllowedHostsOriginValidator
from app.test import TestController
channel_routing = ProtocolTypeRouter({
"websocket": AllowedHostsOriginValidator(
AuthMiddlewareStack(
URLRouter([
path("websocket/test", TestController.as_asgi())
])
)
)
})
This code is highly confusing. What are you trying to do?
Your async def receive_json(self, content: dict):
is supposed to be called every time a message is received.
So at the time of writing, if you fix the error @Igonato mentioned by replacing sleep(5)
with await asyncio.sleep(5)
, your code would be sending 6 ws messages over approximately 25 seconds in response to every incoming ws message.
I like to think of the websocket code running on our server - what runs in consumers.py - as reactive code. In other words, code that reacts to the commands that are sent to it via a the same or another websocket connection. This is also going to help save resources on the server; instead of running multiple loops or counting time for multiple users, we let the user calculate the time and send our answer back to them.
Consider a 'TestController' or daemon that is responds to a command sent to it by showing the user the desired message. The client is running a javascript or reactJS loop that sends a message every 5 seconds. I see here that you use a counter within a for loop range, you could do the same thing in javascript, keeping track of the count in a loop, then sending it to the daemon with as a variable in a string. We are going to use the first character of our string to denote a command, we are going to use the second character of our string to denote a variable for that command function.
Consider the following code to see how you can setup your receive function to react to different commands. When we send our data from the front-end client to our websocket, setup your content
as a dictionary with each string or character that you need.
var pingNum = 1;
var pingID = 1;
function newFunction() {
socket.send(JSON.stringify({
"command": "ping",
"pingID" : pingID,
"pingNum": pingNum
}));
pingNum += 1;
}
setInterval(newFunction, 5000);
Next, lets receive that json in our receive_json
function and look for each key in the JSON to see what it says to guide our program forward and respond back to the client.
class TestController(AsyncJsonWebsocketConsumer):
##### WebSocket event handlers
async def connect(self):
"""
Called when the websocket is handshaking as part of initial connection.
"""
# Accept the connection
await self.accept()
async def receive_json(self, content):
"""
Called when we get a text frame. Channels will JSON-decode the payload
for us and pass it as the first argument.
"""
# Messages will have a "command" key we can switch on, otherwise its a ready check
command = content.get("command", None)
pingNum = content.get("pingNum", None)
if command == "ping":
if pingNum <= 5:
await self.send_json({
"text": "Loop - " + str(pingNum)
})
else:
await self.send_json({
"text": {
"type": "finished"
}
})
Please use await asyncio.sleep(5)
import asyncio
from channels.generic.websocket import AsyncJsonWebsocketConsumer
class TestController(AsyncJsonWebsocketConsumer):
async def connect(self):
await self.accept()
async def receive_json(self, content: dict):
for i in range(0, 5):
await self.send_json({
"text": f"Loop - {i + 1}"
})
await asyncio.sleep(5)
await self.send_json({
"text": {
"type": "finished"
}
})
async def disconnect(self, close_code):
print()
print("DISCONNECTED")
print(close_code)
print()
It's because you are using the blocking sleep function. Instead, you should use the asynchronous asyncio.sleep function, which doesn't block the event loop.
E.g.
import asyncio
from channels.generic.websocket import AsyncJsonWebsocketConsumer
class TestController(AsyncJsonWebsocketConsumer):
async def connect(self):
await self.accept()
async def receive_json(self, content: dict):
for i in range(5):
await self.send_json({"text": f"Loop - {i + 1}"})
await asyncio.sleep(5)
await self.send_json({"text": {"type": "finished"}})
async def disconnect(self, close_code):
print("DISCONNECTED")
print(close_code)
That should work perfectly.