I started looking into websockets and I would liek to implement realtime communication between web-client (Angular 13) and Python app. As backend I used Django with implemented websockets and channels
As i am having problem i simplified code as much as i could so a lot of values are hardcoded for a sake to make it work. (Right now it is One WebClient and one Bot)
I implemented 2 Consumers. Bot and webclient:
class BotConsumer(AsyncJsonWebsocketConsumer):
async def connect(self):
await self.accept()
await self.channel_layer.group_add("bot","dev")
async def disconnect(self, close_code):
await self.channel_layer.group_discard("bot","dev")
async def receive(self, text_data):
await self.channel_layer.group_send("dev",{"type": "recv","text": "test"})
print("Data sent to group")
class WebClientConsumer(AsyncJsonWebsocketConsumer):
async def connect(self):
await self.accept()
await self.channel_layer.group_add("client","dev")
async def recv(self,event):
print("Consumer received smthing from channels")
async def disconnect(self, close_code):
await self.channel_layer.group_discard("client","dev")
async def receive(self, text_data):
print("Smthing received")
my channel_layer setup in Setting.py
CHANNEL_LAYERS = {
'default': {
# 'BACKEND': 'channels_redis.core.RedisChannelLayer',
# 'CONFIG': {
# "hosts": [('192.168.1.10', 6379)],
# },
"BACKEND": "channels.layers.InMemoryChannelLayer"
}}
I tried both Redis with local server running and in memory channels
My websocket's routing.py:
from django.urls import re_path
from . import consumers
websocket_urlpatterns = [
re_path(r'live/bot', consumers.BotConsumer.as_asgi()),
re_path(r'live/webclient', consumers.WebClientConsumer.as_asgi()),
]
My behavior:
Django logs:
HTTP POST /core/UpdateAvailableServers 200 [0.07, 127.0.0.1:50494]
HTTP POST /core/GetPostInfo 200 [0.02, 127.0.0.1:50497]
**WebSocket HANDSHAKING /live/bot [127.0.0.1:50498]**
**WebSocket CONNECT /live/bot [127.0.0.1:50498]**
HTTP POST /core/GetStatus 200 [0.00, 127.0.0.1:50501]
HTTP POST /core/GetPostInfo 200 [0.02, 127.0.0.1:50504]
HTTP POST /core/GetBots 200 [0.00, 127.0.0.1:50524]
**WebSocket HANDSHAKING /live/webclient [127.0.0.1:50527]**
**WebSocket CONNECT /live/webclient [127.0.0.1:50527]**
HTTP POST /core/GetStatus 200 [0.01, 127.0.0.1:50528]
HTTP POST /core/UpdateAvailableServers 200 [0.02, 127.0.0.1:50531]
HTTP POST /core/GetPostInfo 200 [0.02, 127.0.0.1:50534]
HTTP POST /core/GetStatus 200 [0.00, 127.0.0.1:50537]
HTTP POST /core/GetPostInfo 200 [0.02, 127.0.0.1:50540]
HTTP POST /core/UpdateAvailableServers 200 [0.06, 127.0.0.1:50544]
HTTP POST /core/GetStatus 200 [0.00, 127.0.0.1:50547]
HTTP POST /core/GetPostInfo 200 [0.02, 127.0.0.1:50550]
**Data sent to group**
HTTP POST /core/GetPostInfo 200 [0.00, 127.0.0.1:50554]
HTTP POST /core/GetStatus 200 [0.00, 127.0.0.1:50557]
HTTP POST /core/UpdateAvailableServers 200 [0.06, 127.0.0.1:50560]
HTTP POST /core/GetPostInfo 200 [0.02, 127.0.0.1:50563]
As you can see in the log i got successfull websocket connection from my python app (BOT) and also angular app (WebClient) Both are added into group at connect Function.
Bot app will then send some data using websockets and they are suppose to be send to whole group which I will get confirmation as it is shown in log.
Problem is that My webclient will totally ignore this. recv method is never executed nor WebClient consumer will not show any activity on this action.
Is there some obvious mistake I am doing? (I can provide any additional info if needed)
You are not supposed to create a custom channel. Assign a custom group to the channel instead.
await self.channel_layer.group_add("bot",self.channel_name)
Also, send the socket message to the client group in the same channel.
await self.channel_layer.group_send("client",{
"type":"recv",
"message":text_data
})
See the corrected implementation below.
class BotConsumer(AsyncJsonWebsocketConsumer):
async def connect(self):
await self.accept()
await self.channel_layer.group_add("bot",self.channel_name)
async def disconnect(self, close_code):
await self.channel_layer.group_discard("bot",self.channel_name)
async def receive(self, text_data):
await self.channel_layer.group_send("client",{
"type":"recv",
"message":text_data
})
print("Data sent to group")
class WebClientConsumer(AsyncJsonWebsocketConsumer):
async def connect(self):
await self.accept()
await self.channel_layer.group_add("client",self.channel_name)
async def recv(self,event):
print("Consumer received smthing from channels")
async def disconnect(self, close_code):
await self.channel_layer.group_discard("client",self.channel_name)
async def receive(self, text_data):
print("Smthing received")
Related
I want to connect my Django WebSocket with a third-party web socket. This program is one I wrote, and it functions properly. To avoid having to re-login with the third-party API, I have now added the code to check whether the same room is present in my database. if we use the same API KEY to re-connect to the third-party API. It gives the following error:
{"event":"login","status":401,"message":"Connected from another location"}
I want to see if the same cryptocurrency coin is already connected or not. We don't want to logon with the same API KEY once we're connected. I have two issues here:
Don't send the login request to that web socket again.
Don't send the subscribe request, if the same coin already exists. Let's say BTCUSD already connected and giving me the data. I want to just connect to the next user to same room and get the data on next request.
import websocket
import time
import ssl
from channels.generic.websocket import AsyncWebsocketConsumer
from .models import Room
login = {
"event": "login",
"data": {
"apiKey": "API_KEY",
},
}
class CryptoConsumer(AsyncWebsocketConsumer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.ws = websocket.WebSocket(sslopt={"cert_reqs": ssl.CERT_NONE})
self.ws.connect("wss://crypto.financialmodelingprep.com")
async def connect(self):
self.room_name = self.scope["url_route"]["kwargs"]["room_name"]
self.room_group_name = "crypto_%s" % self.room_name
# ****** Code Block ******
subscribe = {
"event": "subscribe",
"data": {
"ticker": self.room_name,
},
}
room = await Room.add(self.room_name) # Method in models.py to add the user and return True
if room is False:
self.ws.send(json.dumps(login))
print("The group with the name %s doesn't exist" % self.room_group_name)
time.sleep(1)
self.ws.send(json.dumps(subscribe))
# ****** End Code Block ******
# Join room group
await self.channel_layer.group_add(self.room_group_name, self.channel_name)
await self.accept()
async def disconnect(self, close_code):
unsubscribe = {
"event": "unsubscribe",
"data": {
"ticker": self.room_name,
},
}
self.ws.send(json.dumps(unsubscribe))
self.ws.close()
# Leave room group
await self.channel_layer.group_discard(self.room_group_name, self.channel_name)
# Receive message from WebSocket
async def receive(self, text_data="{'text': 'Dummy Text'}"):
text_data_json = json.loads(text_data)
message = text_data_json["message"]
message = str(self.ws.recv())
# Send message to room group
await self.channel_layer.group_send(
self.room_group_name, {"type": "chat_message", "message": message}
)
# Receive message from room group
async def chat_message(self, event):
message = event["message"]
# Send message to WebSocket
await self.send(text_data=json.dumps({"message": message}))
Note: Why I want to do this entire step because we don't want the API KEY made public.
So, the code coming from the front end will connect to our Django web socket, and then we'll connect to the third-party web socket and return the data that was sent by them.
I get this error only in chrome (not in safari and not in firefox) when trying to connect to the server via a websocket:
Sent non-empty 'Sec-WebSocket-Protocol' header but no response was received.
The server accepts the connection but chrome unexpectedly closes it immediately.
This is how I create a websocket connection on the frontend:
const websocket = new WebSocket('ws://127.0.0.1:8000/ws/', ['Token', 'user_secret_token'])
This is how my consumers.py looks:
class MyConsumer(JsonWebsocketConsumer):
def connect(self):
self.room_group_name = 'example_room'
# Join room group
async_to_sync(self.channel_layer.group_add)(self.room_group_name, self.channel_name)
self.accept()
The solution was to add the websocket protocol I was using (Token) as the subprotocol parameter when I call self.accept() on the server.
So here is how my consumer looks now:
class MyConsumer(JsonWebsocketConsumer):
def connect(self):
self.room_group_name = 'example_room'
# Join room group
async_to_sync(self.channel_layer.group_add)(self.room_group_name, self.channel_name)
# incorrect
# self.accept()
# correct
self.accept('Token')
It is connecting just fine but I don't receive the message I want to send.
class ArticleConsumer(AsyncJsonWebsocketConsumer):
async def connect(self):
print("WebSocket Connected")
await self.accept()
async def send(self, text_data=None, bytes_data=None):
print("Hello")
print("Sending WebSocket Text Data : ", text_data)
print("Sending WebSocket Bytes Data : ", bytes_data)
await send(text_data="Hi this is text")
await send(bytes_data="Hi this is frame")
async def disconnect(self):
print("WebSocket Disconnected")
The terminal just shows this
WebSocket Connected
WebSocket CONNECT /api/ [127.0.0.1:52360]
WebSocket HANDSHAKING /api/ [127.0.0.1:52362]
WebSocket Connected
WebSocket CONNECT /api/ [127.0.0.1:52362]
which means async def send isn't working.
I also tried to change it to async def receive, still it doesn't work.
Here is the backend project repo :
https://www.github.com/prateekamana/tempstack
you should send data to the frontend in JSON format.
await send(text_data=json.dumps({"message": "Hi this is text"}))
I am using a combination of DRF 3.11.0 and Channels 2.4.0 to implement a backend, and it is hosted on Heroku on 1 dyno with a Redis resource attached. I have a socket on my React frontend that successfully sends/received from the backend server.
I am having an issues where any message sent back to the front end over the socket is being sent twice. I have confirmed through console.log that the front end is only pinging the back end once. I can confirm through print() inside of the API call that the function is only calling async_to_sync(channel_layer.group_send) once as well. The issue is coming from my consumer - when I use print(self.channel_name) inside of share_document_via_videocall(), I can see that two instances with different self.channel_names are being called (specific.AOQenhTn!fUybdYEsViaP and specific.AOQenhTn!NgtWxuiHtHBw. It seems like the consumer has connected to two separate channels, but I'm not sure why. When I put print() statements in my connect() I only see it go through the connect process once.
How do I ensure that I am only connected to one channel?
in settings.py:
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
#"hosts": [('127.0.0.1', 6379)],
"hosts": [(REDIS_HOST)],
},
},
}
Consumer:
import json
from asgiref.sync import async_to_sync
from channels.db import database_sync_to_async
from channels.generic.websocket import AsyncWebsocketConsumer
from rest_framework.authtoken.models import Token
from django.contrib.auth.models import AnonymousUser
from .exceptions import ClientError
import datetime
from django.utils import timezone
class HeaderConsumer(AsyncWebsocketConsumer):
async def connect(self):
print("connecting")
await self.accept()
print("starting")
print(self.channel_name)
await self.send("request_for_token")
async def continue_connect(self):
print("continuing")
print(self.channel_name)
await self.get_user_from_token(self.scope['token'])
await self.channel_layer.group_add(
"u_%d" % self.user['id'],
self.channel_name,
)
#... more stuff
async def disconnect(self, code):
await self.channel_layer.group_discard(
"u_%d" % self.user['id'],
self.channel_name,
)
async def receive(self, text_data):
text_data_json = json.loads(text_data)
if 'token' in text_data_json:
self.scope['token'] = text_data_json['token']
await self.continue_connect()
async def share_document_via_videocall(self, event):
# Send a message down to the client
print("share_document received")
print(event)
print(self.channel_name)
print(self.user['id'])
await self.send(text_data=json.dumps(
{
"type": event['type'],
"message": event["message"],
},
))
#database_sync_to_async
def get_user_from_token(self, t):
try:
print("trying token" + t)
token = Token.objects.get(key=t)
self.user = token.user.get_profile.json()
except Token.DoesNotExist:
print("failed")
self.user = AnonymousUser()
REST API call:
class ShareViaVideoChat(APIView):
permission_classes = (permissions.IsAuthenticated,)
def post(self, request, format=None):
data = request.data
recipient_list = data['recipient_list']
channel_layer = get_channel_layer()
for u in recipient_list:
if u['id'] != None:
print("sending to:")
print('u_%d' % u['id'])
async_to_sync(channel_layer.group_send)(
'u_%d' % u['id'],
{'type': 'share_document_via_videocall',
'message': {
'document': {'data': {}},
'sender': {'name': 'some name'}
}
}
)
return Response()
with respect to you getting to calls with different channel names are you sure your frontend has not connected twice to the consumer? Check in the debug console in your browser.
i get same problem with nextjs as a frontend of Django channels WebSocket server.
and after searching i found the problem related with tow things:
1- react strict mode (the request sending twice) :
to disable react strict mode in next.js , go to module name "next.config.js" , and change the value for strict mode to false , as the following :
/** #type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: false,
}
module.exports = nextConfig
2- in nextjs the code run twice (outside useEffect Hook) , one on server side and the second on the client side (which means each user will connect to websocket server twice, and got two channels name , and join to same group twice each time with different channel name . ) ,
so i changed my codes to connect with Django channels server only from client side , if you like see my full code / example , kindly visit the following URL , and note the checking code about "typeof window === "undefined":
frontend nextjs code :
https://stackoverflow.com/a/72288219/12662056
i don't know if my problem same your problem , but i hope that helpful.
TL;DR
Want this flow:
ws://...
websocket client 1 <-----------> websocket client 2
^
|
server (send messages via views)
So I have the following:
views.py
def alarm(request):
layer = get_channel_layer()
async_to_sync(layer.group_send)('events', {
'type': 'events.alarm',
'content': 'triggered'
})
return HttpResponse('<p>Done</p>')
consumers.py
class EventConsumer(JsonWebsocketConsumer):
def connect(self):
print('inside EventConsumer connect()')
async_to_sync(self.channel_layer.group_add)(
'events',
self.channel_name
)
self.accept()
def disconnect(self, close_code):
print('inside EventConsumer disconnect()')
print("Closed websocket with code: ", close_code)
async_to_sync(self.channel_layer.group_discard)(
'events',
self.channel_name
)
self.close()
def receive_json(self, content, **kwargs):
print('inside EventConsumer receive_json()')
print("Received event: {}".format(content))
self.send_json(content)
def events_alarm(self, event):
print('inside EventConsumer events_alarm()')
self.send_json(
{
'type': 'events.alarm',
'content': event['content']
}
)
in routing.py
application = ProtocolTypeRouter({
'websocket': AllowedHostsOriginValidator(
AuthMiddlewareStack(
URLRouter(
chat.routing.websocket_urlpatterns,
)
)
),
})
where websocket_urlpatterns is
websocket_urlpatterns = [
url(r'^ws/chat/(?P<room_name>[^/]+)/$', consumers.ChatConsumer),
url(r'^ws/event/$', consumers.EventConsumer),
]
urls.py
urlpatterns = [
url(r'^alarm/$', alarm, name='alarm'),
]
when I call /alarm/ , only the HTTP request is made and the message is not sent to the websocket
The following are the logs:
[2018/09/26 18:59:12] HTTP GET /alarm/ 200 [0.07, 127.0.0.1:60124]
My intention is to make django view send to a group (use case would be for a server to send notification to all connected members in the group).
What setting am I missing here.
I am running django channels 2.1.3 with redis as backend. The CHANNELS_LAYERS etc. have all been setup.
Reference Links:
sending messages to groups in django channels 2
github issue
channels docs
EDIT:
I could send the message using the websocket-client from the view
from websocket import create_connection
ws = create_connection("ws://url")
ws.send("Hello, World")
But is it possible to send without using the above (donot wan't to create a connection)?
Source code: chat-app
credits to #Matthaus Woolard for making the concept pretty clear.
So this was the problem:
The client had disconnected when I tried to send the message from the django view. This happened as the server restarted upon code change. I refreshed the browser and it started to work.
Silly mistake
So to summarize:
Add the following in connect() in case of Synchronous consumer:
async_to_sync(self.channel_layer.group_add)('events', self.channel_name)
or add this incase of Async Consumer:
await self.channel_layer.group_add('events', self.channel_name)
create a view as follows:
def alarm(request):
layer = get_channel_layer()
async_to_sync(layer.group_send)('events', {
'type': 'events.alarm',
'content': 'triggered'
})
return HttpResponse('<p>Done</p>')