i am pretty new in django-channels and python, i make a demo using django-channels and django to send data from backend to front activly. the code in consumers.py as below:
websocket_clients = []
class WebsocketConsumer(AsyncWebsocketConsumer):
def __init__(self, *args, **kwargs):
"""
initial a new thread when new websocket instance initialed.
"""
super(WebsocketConsumer, self).__init__(self)
self.index_websocket = GendataThread()
async def connect(self):
self.index_websocket.channels_name = self.channel_name
print('channel_name', self.channel_name)
self.index_websocket.start()
websocket_clients.append(self.channel_name)
await self.accept()
async def disconnect(self, close_code):
"""disconnected a websocket instance, and stop the thread."""
self.index_websocket.stop()
time.sleep(2)
index = websocket_clients.index(self.channel_name)
websocket_clients.pop(index)
async def receive(self, text_data):
"""receive message from connected client, wouldn't work in this project."""
text_data = '{"message": "%s"}' % (text_data)
text_data_json = json.loads(text_data)
async def chat_message(self, event):
"""send message to websocket."""
print('event', event)
await self.send(text_data=json.dumps(
{'message': event['message']}))
def websocket_send_data(data, channel_name):
"""
send data to front in websocket protocol,
:param data: data which will be sent,
:param channel_name: a symbol to mark connected client.
:return:
"""
channel_layer = get_channel_layer()
event = {
"type": "chat.message",
"message": data
}
async_to_sync(channel_layer.send)(channel_name, event)
as you can see, i make a function name: websocket_send_data(data, channel_name),which i can call in GendataThread() as below:
class GendataThread(threading.Thread):
"""new thread
thread for getting data from cluster, and send to front with websocket protocol.
"""
def __init__(self, *args, **kwargs):
super(GendataThread, self).__init__(*args, **kwargs)
self.channels_name = None
def run(self):
while true:
websocker_send_data('1', self.channels_name)
in this case, i can send data from backend to front while django project is running, i connect to the url
ws://127.0.0.1:8000/ws/intime_data
which is set in routing.py as below:
websocket_urlpatterns = [
url(r'^ws/intime_data$', consumers.WebsocketConsumer),
]
i can receive data at front, but as it runs a few minutes, things were going wrong:
aioredis.errors.ReplyError: OOM command not allowed when used memory > 'maxmemory'.
i think is because redis is full by saving data,but i don't want these data,i try to config redis.conf by:
maxmemory-policy allkeys-lru
but still comes error when maxmemory is used. and sometimes will comes such error:
channels.exceptions.Channels Full
i think channels is using redis as a cache, but i don't know why errors come, can anyone helps me? thanks in advance.
you add this code to your settings.py:
CHANNEL_LAYERS = {
"default": {
"BACKEND": "asgi_redis.RedisChannelLayer",
"CONFIG": {
"hosts": [(XXX, XXX)],
},
"ROUTING": "XXXXXchannel_routing"
},
}
Related
I use Django Channels with channel_layers (RedisChannelLayer).
Using Channels I only need to get live messages from signals when post_save event happens.
I try to send a message from the signals.py module.
The fact that the first message is sending properly, I got it successfully in the js console,
but then disconnection from the socket happens with an Exception:
RuntimeError: Task got Future attached to a different loop.
It refers to ...redis/asyncio/connection.py:831
All my settings were done properly in accordance with the documentation.
My project also uses DRF, Celery(on Redis), Redis itself, Daphne server.
I only try to implement it with Debug=True mode, let alone production.
I have no idea what happens and how to solve it.
Here are snippets from my code:
#consumers.py
class LikeConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.room_group_name = "likes"
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
)
await self.accept()
async def chat_message(self, event):
message = event['message']
await self.send(text_data=json.dumps({
'type': 'chat',
'message': message
}))
#signals.py
channel_layer = get_channel_layer()
#receiver(post_save, sender=Like)
def new_like(sender, instance, **kwargs):
async_to_sync(channel_layer.group_send)(
'likes',
{
"type": "chat_message",
"message": "1212341324, 213413252345"
}
)
//script.js
const likeSocket = new WebSocket(url);
likeSocket.onmessage = function(e) {
let data = JSON.parse(e.data);
console.log(data);
};
After a few days of investigating the issue, I have realized the Exception, RuntimeError, in that case, is just a Warning, not Error.
Because the server doesn't break and the Socket is going on further.
And that "Warning" is still in a Bug stage in Channels developers.
It looks not beauty in the server console, but it works properly.
I tried to try-except it, but can't figure it out - don't understand what code invokes the Error to try-except it.
I run this code the handshake and connection work fine but I don't get any results after I run the client.even though the code is working perfectly without any issues.
is there any idea to fix the problem?
note: I'm using graphql and Django framework in my project
import asyncio
import graphene
from channels.routing import ProtocolTypeRouter, URLRouter
class Query(graphene.ObjectType):
hello = graphene.String()
#staticmethod
def resolve_hello(obj, info, **kwargs):
return "world"
class Subscription(graphene.ObjectType):
"""Channels WebSocket consumer which provides GraphQL API."""
count_seconds = graphene.Float()
async def resolve_count_seconds(root, info):
for i in range(10):
yield i
await asyncio.sleep(1.)
schema = graphene.Schema(query=Query, subscription=Subscription)
class MyGraphqlWsConsumer(channels_graphql_ws.GraphqlWsConsumer):
"""Channels WebSocket consumer which provides GraphQL API."""
schema = schema
async def on_connect(self, payload):
pass
application = channels.routing.ProtocolTypeRouter({
"websocket": channels.routing.URLRouter([
django.urls.path("graphql/", MyGraphqlWsConsumer.as_asgi()),
])
})
ASGI_APPLICATION = 'graphql_ws.urls.application'
client:
from graphql_client import GraphQLClient
ws = GraphQLClient('ws://localhost:8000/graphql/')
def callback(_id, data):
print("got new data..")
print(f"msg id: {_id}. data: {data}")
query = """
subscription {
countSeconds
}
"""
sub_id = ws.subscribe(query, callback=callback)
I'm attempting to send consumers.py information to display on the client end outside of consumers.py.
I've referenced Send message using Django Channels from outside Consumer class this previous question, but the sub process .group_send or .group_add don't seem to exist, so I feel it's possible I'm missing something very easy.
Consumers.py
from channels.generic.websocket import WebsocketConsumer
from asgiref.sync import async_to_sync
class WSConsumer(WebsocketConsumer):
def connect(self):
async_to_sync(self.channel_layer.group_add)("appDel", self.channel_name)
self.accept()
self.render()
appAlarm.py
def appFunc(csvUpload):
#csvUpload=pd.read_csv(request.FILES['filename'])
csvFile = pd.DataFrame(csvUpload)
colUsernames = csvFile.usernames
print(colUsernames)
channel_layer = get_channel_layer()
for user in colUsernames:
req = r.get('https://reqres.in/api/users/2')
print(req)
t = req.json()
data = t['data']['email']
print(user + " " + data)
message = user + " " + data
async_to_sync(channel_layer.group_send)(
'appDel',
{'type': 'render', 'message': message}
)
It's throwing this error:
async_to_sync(channel_layer.group_send)(
AttributeError: 'NoneType' object has no attribute 'group_send'
and will throw the same error for group_add when stripping it back more to figure out what's going on, but per the documentation HERE I feel like this should be working.
To anyone looking at this in the future, I was not able to use redis or even memurai in Windows OS due to cost. I ended up using server side events (SSE), specifically django-eventstream, and so far it's worked great as I didn't need the client to interact with the server, for a chat application this would not work.
Eventstream creates an endpoint at /events/ the client can connect to and receive a streaming http response.
Sending data from externalFunc.py:
send_event('test', 'message', {'text': 'Hello World'})
Event listener in HTML page:
var es = new ReconnectingEventSource('/events/');
es.addEventListener('message', function (e) {
console.log(e.data);
var source = new EventSource("/events/")
var para = document.createElement("P");
const obj = JSON.parse(event.data)
para.innerText = obj.text;
document.body.appendChild(para)
}, false);
es.addEventListener('stream-reset', function (e) {
}, false);
I have created a channel that implements some text operations using a shared task which returns the response back to the channel layer.
#consumers.py
import json
import pdb
from asgiref.sync import async_to_sync
from channels.generic.websocket import AsyncWebsocketConsumer
from . import tasks
COMMANDS = {
'help': {
'help': 'Display help message.',
},
'sum': {
'args': 2,
'help': 'Calculate sum of two integer arguments. Example: `sum 12 32`.',
'task': 'add'
},
'status': {
'args': 1,
'help': 'Check website status. Example: `status twitter.com`.',
'task': 'url_status'
},
}
class Consumer(AsyncWebsocketConsumer):
async def receive(self, text_data):
text_data_json = json.loads(text_data)
message = text_data_json['message']
# response_message = 'Please type `help` for the list of the commands.'
message_parts = message.split()
if message_parts:
command = message_parts[0].lower()
if command == 'help':
response_message = 'List of the available commands:\n' + '\n'.join([f'{command} - {params["help"]} ' for command, params in COMMANDS.items()])
elif command in COMMANDS:
if len(message_parts[1:]) != COMMANDS[command]['args']:
response_message = f'Wrong arguments for the command `{command}`.'
else:
getattr(tasks, COMMANDS[command]['task']).delay(self.channel_name, *message_parts[1:])
# response_message = f'Command `{command}` received.'
response_message = message
await self.channel_layer.send(
self.channel_name,
{
'type': 'chat_message',
'message': response_message
}
)
#tasks.py
#shared_task
def add(channel_layer, x, y):
message = '{}+{}={}'.format(x, y, int(x) + int(y))
async_to_sync(channel_layer.send)({"type": "chat.message", "message": message})
I want to share this channel as an api which could be accessed using http request. for which i have written following views.
views.py
#csrf_exempt
#api_view(['POST'])
def api(request):
channel_layer = get_channel_layer()
async_to_sync(channel_layer.send)('test_channel', {'type': 'hello'})
ret = async_to_sync(channel_layer.receive)(channel_name)
return JsonResponse({"msg":ret})
While receiving from the views I get the same message that I have sent. How can I share the channel or handle incoming messages without connecting using WebSockets from the template?
If you just want the POST request to send a message
views.py
#csrf_exempt
#api_view(['POST'])
def api(request):
channel_layer = get_channel_layer()
async_to_sync(channel_layer.send)('test_channel', {'type': 'chat.message'})
return JsonResponse({"msg":"sent"})
You will need to ensure you have subscribed to test_channel in your consumer. And you will need a method on that consumer chat_message.
If you want to wiat for the response in your post
your not going to be able to do this using channel_layer.send since that is async to the extend that you dont have any concept fo response. In addition there might not even be an instance of your consumer running since Channels only creates instances when it has open websocket connections that rout to them.
so I think you can do either:
To create an instance of your consumer and send message to it from synchronise python code is going to be very complex. I suggest you do not do this approach It is complex, dirty and likely to break in all sorts of un-expected
instead I suggest to move the code you want to share between your HTTP view and your websocket view into a single place (not part of the consumer) were they both can call these functions.
I'm currently trying to create a backend server to communicate with some clients with a websocket. The clients makes some request to the backend and the backend responds directly to the client through a consumer.
In addition, I've got an API that needs to send some requests to the client. It has to go through the opened socket of the consumer. I'm using Django Rest Framework for the API. So I've got 2 apps for now. One for the consumer and one for the API. I want to know if it's the correct way or not.
This is actually the code I'm thinking about right now:
# mybackendapp/consumers.py
class MyConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.client_id = self.scope['url_route']['kwargs']['client_id']
# This line I don't get it very well. It comes from:
# [channels doc: single channels][1]
# I don't know if I should create the Clients myself or if it's
# created automatically
Clients.objects.create(channel_name=self.channel_name,
self.client_id)
self.accept()
async def disconnect(self):
Clients.objects.filter(channel_name=self.channel_name).delete()
async def receive(self, text_data):
self.recv_data = json.loads(text_data)
if self.recv_data[0] == CLIENT_REQUEST:
self.handler = ClientRequestHandler(self.client_id,
self.recv_data)
await self.handler.run()
self.sent_data = self.handler.response
self.send(self.sent_data)
elif self.recv_data[0] == CLIENT_RESPONSE:
self.handler = ClientResponseHandler(self.client_id,
self.recv_data)
channel_layer = get_channel_layer()
# Here I'm not sure but I may have several API requests so
# several row with the same client_id.
# I welcome info on how to deal with that.
api_channel_name = self.another_handler.ext_channel_name
channel_layer.send(api_channel_name, {
"text_data": self.handler.response,
})
async def message_from_api(self, event):
self.api_channel_name = event['channel_name_answer']
# this line is for hiding the fact that I'm only manipulating data
# to send it to a correct format to the socket
self.another_handler = ExternalDataHandler(event['json_data'])
query_to_client = another_handler.get_formatted_query()
self.send(query_to_client)
In receive, this consumer handles differently the messages from the client depending if it's initiated by the client or the rest API. You can see that with CLIENT_REQUEST and CLIENT_RESPONSE constants.
Now from the API view:
# myapi/views.py
from channels.layers import get_channel_layer
def my_api_view(request, client_id):
channel_layer = get_channel_layer()
if request.method == 'POST':
ext_request_data_json = request.data
client_channel_name = Clients.objects.get(
client_id=client_id).channel_name
# I don't know what type is listen_channel_name. I assume it's str
listen_channel_name = async_to_sync(channels_layers.new_channel)()
async_to_sync(channel_layer.send)(
client_channel_name, {
'type': 'message.from.api',
'json_data': ext_request_data_json,
'channel_name_answer': listen_channel_name
}
)
received_msg = channel_layer.receive(listen_channel_name)
I believe that this code should work. I want to know if it's the correct way to do it.
See djangochannelsrestframework as a possible alternative solution.
Django Channels Rest Framework provides a DRF like interface for building channels-v3 websocket consumers.