15

Take a look at this example.

As you can see, some sort of event is constantly being sent to the client. I want to imitate this using Django-Channels, inside consumers.py. Here's a simplified version of what I have:

class ChatConsumer(AsyncConsumer):
    async def ws_connect(self, event):
        self.send = get_db_object()

        ....

        await self.send({
            "type": "websocket.accept"
        })

    # I need to CONSTANTLY receive & send data
    async def ws_receive(self, event):

        obj = ...# query DB and get the newest object

        json_obj = {
            'field_1': obj.field_1,
            'field_2': obj.field_2,
        }

        await self.send({
            "type": "websocket.send",
            "text": json.dumps(json_obj)
        })


    @database_sync_to_async
    def get_db_object(self, **kwargs):
        return Some_Model.objects.get(**kwargs)[0]

Here, I want my Django backend to constantly:

  1. Query DB
  2. Receive obj from DB
  3. Send the received obj to Front-End WebSocket as event

How can I achieve this? The important thing is that I need to CONSTANTLY send data to the client.

Most of the Django-Channels resources on the internet cover only Chat Apps, which don't necessarily constantly send data to the client. I couldn't find any working code that does this job.

Please, no more recommendation for Redis or channels documentation... or some random 3rd party libraries that lacks good documentation... It's easy to recommend but hard to implement. For example, I found someone recommending Snorky, but it really lacks documentation on how to implement it.

However, if there's a website that specifically does this job, I might take a look at it, even if it doesn't use Django-Channels.

Eric Kim
  • 2,493
  • 6
  • 33
  • 69

3 Answers3

17

consumers.py

import asyncio
from channels.consumer import AsyncConsumer

class ChatConsumer(AsyncConsumer):

    async def websocket_connect(self, event):
        self.connected = True
        print("connected", event)
        await self.send({
            "type": "websocket.accept"
        })

        while self.connected:
            await asyncio.sleep(2)

            obj = # do_something (Ex: constantly query DB...)
 
            await self.send({
                'type': 'websocket.send',
                'text': # obj,
            })

    async def websocket_receive(self, event):
        print("receive", event)

    async def websocket_disconnect(self, event):
        print("disconnected", event)
        self.connected = False

Javascript

var loc = window.location;
var wsStart = 'ws://';
if (loc.protocol == 'https:') {
    wsStart = 'wss://'
}
var endpoint = wsStart + loc.host + loc.pathname;

var socket = new WebSocket(endpoint);

socket.onmessage = function(e){
    console.log("message", e);
};
socket.onopen = function(e){
    console.log("open", e);
};
socket.onerror = function(e){
    console.log("error", e)
};
socket.onclose = function(e){
    console.log("close", e)
};

All you need to do is just modify obj and send it. You can extend this function as much as you want. So, right now I'm interested in getting the latest inserted row in my PostgreSQL and injecting that row into my WebSocket. I can query my DB every 2 seconds as it was specified by await asyncio.sleep(2), and inject it into the Front-End socket.

kagronick
  • 2,552
  • 1
  • 24
  • 29
Eric Kim
  • 2,493
  • 6
  • 33
  • 69
  • 6
    Your loop will never exit even if the user disconnects. Set a flag in your disconnect handler to stop the loop and run your loop in a function with asyncio's create_task. I believe Channels will kill it eventually but that is more of an error handler. – kagronick Aug 09 '18 at 15:13
  • @Eric can you help me with the same thing using channels 1? – ans2human Jul 25 '19 at 09:22
  • @kagronick is right, here facing some problem, while it was looping, the receive /disconnect is not working, can you pease let me know how can we achieve this. – Ajay Kumar Apr 04 '20 at 11:31
  • @AjayKumar Use asyncio's `create_task` to start your async function that loops. When `disconnect` fires set a flag to true and have the task exit it's loop. – kagronick Apr 06 '20 at 13:53
  • 1
    @kagronick Thanks for your reply, i'm working on it , can you please explain it with piece of code for more clarity. Thanks in advance. – Ajay Kumar Apr 07 '20 at 07:17
  • 1
    Put these methods inside the class. I haven't tested it. There might be some typos. ``` async def connect(self): self.connected = True asyncio.create_task(self.do_stuff) asyc def do stuff(self): while self.connected: # do stuff async def disconnect(self): self.connected = False ``` Ok apparently SO doesn't allow linebreaks in comments. – kagronick Apr 07 '20 at 13:31
  • @kagronick Thanks for the kind reply i will try and let you know ..... – Ajay Kumar Apr 08 '20 at 08:05
  • @kagronick I tried your suggested code and it doesn't work. It still keeps running after I have disconnected the client – Sabito stands with Ukraine Sep 23 '21 at 15:48
  • 1
    @Sabito錆兎 I have updated it with what you are looking for. – kagronick Oct 04 '21 at 17:17
0

Using channels==1.* and Django==1.* you can use the threading module for example:

# Some view.py
import threading
import time

class Publisher(threading.Thread):
    def __init__(self, reply_channel, frequency=0.5):
        super(Publisher, self).__init__()
        self._running = True
        self._reply_channel = reply_channel
        self._publish_interval = 1.0 / frequency

    def run(self):
        while self._running:
           self._reply_channel.send({'text': 'some data'})
           time.sleep(self._publish_interval)

    def stop(self):
        self._running = False

publishers = {}

def ws_connect(message):
    message.reply_channel.send({'accept': True})
    publisher = Publisher(reply_channel=message.reply_channel)
    publisher.start()
    publishers[message.reply_channel] = publisher

def ws_disconnect(message):
    publisher = publishers[message.reply_channel]
    publisher.stop()
    del publishers[message.reply_channel]
Stefan
  • 49
  • 3
  • You have to wait actually thread finish before deleting publishers[message.reply_channel]. like using thread.join() – rjhcnf Feb 05 '21 at 17:13
  • I would strongly avoid trying to use threads inside software specifically meant to enable developers to avoid handling all the hassles of threading in distributed systems. – Giga Chad Coding Oct 16 '22 at 18:35
0

A little late to the party here, but when it comes to Django you should always try to do things "their way" first...

So, to do this I simply used Django Channels. My client sends a message to the server, which the server then responds to with the needed database info. It looks as follows:

class SettingsConsumer(WebsocketConsumer):
    def connect(self):
        self.accept()

    def disconnect(self, close_code):
        pass

    def receive(self, text_data):
        settings = get_settings(self.scope["user"])

        self.send(
            text_data=json.dumps(
                {
                    "playMode": settings.play_mode,
                    "isRecording": settings.is_recording,
                }
            )
        )

Now, as for the JS to trigger constant events... I simply use SetInterval to request updates from the consumer every .25s!

My logic is that it's the client so if it's doing a little extra work no biggie, as the server is gonna be responding anyways. The JS looks as follows...

const chatSocket = new WebSocket(
            'ws://'
            + window.location.host
            + '/ws/macros/update/'
        );

      chatSocket.onmessage = function(e) {
          const data = JSON.parse(e.data);
          if(data["playMode"] == true) {
            $('#playmode-checkbox').attr('checked', true);
          } else {
            $('#playmode-checkbox').attr('checked', false);
          }

          if(data["isRecording"] == true) {
            $('#recording-checkbox').attr('checked', true);
          } else {
            $('#recording-checkbox').attr('checked', false);
          }
      };

      chatSocket.onclose = function(e) {
          console.error('Chat socket closed unexpectedly');
      };

      window.setInterval(function() {
          chatSocket.send(JSON.stringify({
              'message': "start"
          }));
      }, 250);

And yes, you can do more to make this more async-friendly and optimized. But, you asked for simple and working so I hope this helps!

Giga Chad Coding
  • 178
  • 3
  • 12