How to run WebSocket (django-channels) in production Pythonanywhere? - django

I created a simple chat app using WebSockets after following the official Django-channels tutorial. However, I can't get it to work in production. I have done a few google searching to find out a forum in Pythonanywhere saying that they don't support WebSocket, I contacted the team and they told me the same thing.
I have done even more google searching and found things related to Daphne server, Nginx, and a few other things I never heard about before.
As I'm new to Django-channels I'm currently very confused! Is there something I can do to make my WebSocket website run normally in Pythonanywhere on production (for free of course) Or I have to delete all of the WebSocket code and replace it with repetitive Http called to check of new messages (With AJAX)?
And if there is no other solution but to move to repetitive Http calls, is there any other web hosting service that offers free play which includes free SSL certification, the domain name (such as mydomain.servicename.com) instead of random characters, and WebSocket support?
Thanks
the code i use
I don't know if it was relevant, also it's working perfect in development so i don't think there is an error in it
settings.py:
INSTALLED_APPS = [
'channels',
...
'django_cleanup',
]
ASGI_APPLICATION = 'orgachat.routing.application'
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
"hosts": [('127.0.0.1', 6379)],
},
},
}
main routing.py (in route folder)
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
from chat.routing import websocket_urlpatterns as chat_routing
application = ProtocolTypeRouter({
"websocket": AuthMiddlewareStack(
URLRouter(
chat_routing,
)
)
})
routing.py for chat app
from django.urls import path
from . import consumers
websocket_urlpatterns = [
path('ws/chat/room/<int:room_id>/', consumers.RoomConsumer),
]
consumers.py
import json
from channels.generic.websocket import AsyncWebsocketConsumer
class RoomConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.group_name = str(self.scope['url_route']['kwargs']['room_id'])
await self.channel_layer.group_add(self.group_name, self.channel_name)
await self.accept()
async def disconnect(self, code):
await self.channel_layer.group_discard(self.group_name, self.channel_layer)
async def receive(self, text_data):
message_json = json.loads(text_data)
await self.channel_layer.group_send(self.group_name, {
'type': 'send_message',
'content': message_json['content'],
'area': message_json['area'],
'area_id': message_json['area_id'],
'username': self.scope['user'].username,
})
async def send_message(self, event):
await self.send(json.dumps(event))
full js script
<script>
// -------------------
// WEBSOCKET SETUP
// -------------------
var wsStart = 'ws://'
var hostName = window.location.hostname + ':8000'
if (window.location.protocol.includes('https')) {
wsStart = 'wss://'
hostName = window.location.hostname
};
let endpoint = wsStart + hostName + '/ws' + window.location.pathname
console.log(endpoint)
var socket = new WebSocket(endpoint);
socket.onmessage = function (e) {
// todo not show message if in a different room
data = JSON.parse(e.data);
console.log(data.area_id)
console.log(data.area)
var sender = 'other'
var username = data.username
if (data.username == "{{ user.username }}") {
sender = 'self';
username = 'You'
}
document.querySelector('.messages').innerHTML += `
<div class="message ${sender}">
<p>${username} — ${data.area}:</p>
<p>${data.content}</p>
</div>
`
document.querySelector('#notification_sound').play()
}
socket.onerror = function (e) {
alert("SERVER ERROR 500, You won't be able to see messages unless you refresh,")
}
socket.onclose = function (e) {}
document.addEventListener('DOMContentLoaded', function () {
document.querySelector('#sendMessage').onclick = function (e) {
e.preventDefault();
// ------------AJAX: SEND AND MESSAGE---------------
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = function (e) {
if (this.readyState == 4 && this.status == 200) {
document.querySelector('#id_content').value = '';
}
}
xhr.open("POST", "{% url 'chat:room' room.id %}", true);
xhr.setRequestHeader('Content-type', "application/x-www-form-urlencoded");
data = {
'csrfmiddlewaretoken': '{{ csrf_token }}',
'content': document.querySelector('#id_content').value,
'area': parseInt(document.querySelector('#id_area').value),
}
xhr.send(JSON.stringify(data));
// ---------------WEBSOCKET: ECHO MESSAGE---------------
let area = document.getElementById('id_area')
socket.send(JSON.stringify({
'content': document.querySelector('#id_content').value,
'area': area.options[area.selectedIndex].text,
'area_id': document.querySelector('#id_area').value
}));
}
});
</script>

Websockets, and therefore, django-channels are not supported on PythonAnywhere.

Related

Django Cookiecutter Channels3 - connection opens, doesn't send

I started a a project with Django Cookiecutter w/ Docker: https://cookiecutter-django.readthedocs.io/en/latest/
I'm trying to add Channels and follow the tutorial in their docs: https://channels.readthedocs.io/en/stable/tutorial
I added Channels 3.0.4 to requirements.txt, rebuilt the docker container.
I added channels to settings/base.py, and this:
WSGI_APPLICATION = "config.wsgi.application"
ASGI_APPLICATION = "config.asgi.application"
I updated my config/asgi.py file:
import os
import sys
from pathlib import Path
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application
from the_pub.chat import routing
ROOT_DIR = Path(__file__).resolve(strict=True).parent.parent
sys.path.append(str(ROOT_DIR / "the_pub"))
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local")
django_application = get_asgi_application()
from config.websocket import websocket_application # noqa isort:skip
application = ProtocolTypeRouter({
"https": django_application,
"websocket": AuthMiddlewareStack(
URLRouter(
routing.websocket_urlpatterns
)
),
})
async def application(scope, receive, send):
if scope["type"] == "http":
await django_application(scope, receive, send)
elif scope["type"] == "websocket":
await websocket_application(scope, receive, send)
else:
raise NotImplementedError(f"Unknown scope type {scope['type']}")
created a config/websocket.io file
async def websocket_application(scope, receive, send):
while True:
event = await receive()
if event["type"] == "websocket.connect":
await send({"type": "websocket.accept"})
if event["type"] == "websocket.disconnect":
break
if event["type"] == "websocket.receive":
if event["text"] == "ping":
await send({"type": "websocket.send", "text": "pong!"})
views:
# chat/views.py
from django.shortcuts import render
def index(request):
return render(request, 'chat/index.html')
def index(request):
return render(request, 'chat/index.html', {})
def room(request, room_name):
return render(request, 'chat/room.html', {
'room_name': room_name
})
routing:
# chat/routing.py
from django.urls import re_path
from the_pub.chat import consumers
websocket_urlpatterns = [
re_path(r'ws/chat/(?P<room_name>\w+)/$', consumers.ChatConsumer.as_asgi()),
]
chat/urls:
# chat/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.index, name='index'),
path('<str:room_name>/', views.room, name='room'),
]
consumer:
# chat/consumers.py
import json
from channels.generic.websocket import WebsocketConsumer
class ChatConsumer(WebsocketConsumer):
def connect(self):
self.accept()
def disconnect(self, close_code):
pass
def receive(self, text_data):
text_data_json = json.loads(text_data)
message = text_data_json['message']
self.send(text_data=json.dumps({
'message': message
}))
app.py
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
class ChatConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'the_pub.chat'
verbose_name= _("Chat")
def ready(self):
try:
import the_pub.users.signals # noqa F401
except ImportError:
pass
template:
<!-- chat/templates/chat/room.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Chat Room</title>
</head>
<body>
<textarea id="chat-log" cols="100" rows="20"></textarea><br>
<input id="chat-message-input" type="text" size="100"><br>
<input id="chat-message-submit" type="button" value="Send">
{{ room_name|json_script:"room-name" }}
<script>
const roomName = JSON.parse(document.getElementById('room-name').textContent);
const chatSocket = new WebSocket(
'ws://'
+ window.location.host
+ '/ws/chat/'
+ roomName
+ '/'
);
chatSocket.onmessage = function(e) {
const data = JSON.parse(e.data);
document.querySelector('#chat-log').value += (data.message + '\n');
console.log(data);
};
chatSocket.onclose = function(e) {
console.error('Chat socket closed unexpectedly');
};
document.querySelector('#chat-message-input').focus();
document.querySelector('#chat-message-input').onkeyup = function(e) {
if (e.keyCode === 13) { // enter, return
document.querySelector('#chat-message-submit').click();
}
};
document.querySelector('#chat-message-submit').onclick = function(e) {
const messageInputDom = document.querySelector('#chat-message-input');
const message = messageInputDom.value;
chatSocket.send(JSON.stringify({
'message': message
}));
messageInputDom.value = '';
};
</script>
</body>
</html>
When I test the websocket in a chrome plugin I can send messages and it logs. When I hit send on the form it does nothing. no console alerts, nothing in docker logs. All it does is clear the text in the text box. I didn't think a third party could check the socket because I wrapped it in an authentication layer, but it's the opposite, my app acts like the javascript to send the message to the socket doesn't exist.
when you install Channels, it says to do 'pip -m install -U channels'. I added channels to the requirements.txt base file and let django cookiecutter run install with the rest of the libraries. did this break it?
Also, I'm running this project has it was set up by cookiecutter, which I guess is wsgi. Is it even possible to use both wsgi and asgi like this or should I be looking at how to run the whole site on asgi?
I get an error in the console "DevTools failed to load source map: Could not load content for /requestProvider.js.map. I normally ignore these errors but this seams suspiciously related to the socket.send() function on triggering an .onmessage.
You should remove the async def application(...) from config/asgi.py since you are defining the application as a variable right above.
If using channels, the config/websocket.py file is not exactly needed, since you're handling websocket connections from consumers.
Make sure channels_redis is installed if you're using Redis for the channel layer.
Your chat/views.py has two index views. Not sure if that's intended, but it will create unnecessary confusion when it doesn't work.

Django Channels receive message from room group doesn't appear to be working

I am trying to build a notifications system using Django Channels. I completed the intial setup and when I run my server I get confirmation of Handshake and connection confirmed. However, when I view my console log, I cannot see the message to be sent. I also put a print statement on the send_notification function but it is never reached. I am new to using Django Channels so any help would be appreciated. What am I terribly doing wrong?
Here is my ASGI file setup:
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'clicksource.settings')
from channels.auth import AuthMiddleware, AuthMiddlewareStack
from notifications.routing import websocket_urlpatterns
application = ProtocolTypeRouter({
"http": get_asgi_application(),
"websocket": AuthMiddlewareStack(
URLRouter(
websocket_urlpatterns
)
)
})
Here is my consumers.py:
import json
from channels.generic.websocket import AsyncWebsocketConsumer
class NotificationConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.room_name = self.scope['url_route']['kwargs']['room_name']
self.room_group_name = 'notification_%s' % self.room_name
# 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):
# Leave room group
await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name
)
# Receive message from room group
async def send_notification(self, event):
message = event['message']
# Send message to WebSocket
await self.send(text_data=json.dumps({
'message':message
}))
Here is my routing.py:
from django.urls import re_path
from . import consumers
websocket_urlpatterns = [
re_path(r'ws/notification/(?P<room_name>\w+)/$', consumers.NotificationConsumer.as_asgi()),
]
And the script in my base.html file:
<script>
const roomName = JSON.parse(document.getElementById('room-name').textContent);
const notificationSocket = new WebSocket(
'ws://'
+ window.location.host
+ '/ws/notification/'
+ roomName
+ '/'
);
notificationSocket.onmessage = function(e) {
const data = JSON.parse(e.data);
//document.querySelector('#chat-log').value += (data.message + '\n');
console.log(data);
document.getElementById("notifications-dropdown").innerHTML = "<li class='dropdown-item'>" + data + "</li><hr class='dropdown-divider'>" + document.getElementById("notifications-dropdown").innerHTML;
document.getElementById("notification-badge").innerHTML = parseInt(document.getElementById("notification-badge").innerHTML) + 1;
};
notificationSocket.onclose = function(e) {
console.error('Chat socket closed unexpectedly');
};
</script>
You need to define .receive() method on NotificationConsumer. Whenever a client sends a message, .receive() gets it and echoes it back to the group and then everyone subscribed to that group gets it.
Check out this link: https://channels.readthedocs.io/en/stable/tutorial/part_3.html

chat app work perfectly in local but stopped working when deployed to heroku - django

I did a chatting part in my web app with django it works perfectly in local but after I deployed it to heroku , it doesn't work:
this is my consumers.py class:
from channels.generic.websocket import AsyncWebsocketConsumer
from django.contrib.auth.models import User
from django.db.models import Q
from asgiref.sync import sync_to_async
import json
from chat.models import Thread, Message
from users.serializers import UserSerializer
class ChatConsumer(AsyncWebsocketConsumer):
async def connect(self):
friend = None
me = self.scope['user'] # logged in user
friend_name = self.scope['url_route']['kwargs']['friend'] # get the username of that user, whoom you want to chat
friend_instance = await sync_to_async(User.objects.get, thread_sensitive=True)(username=friend_name) # get user object of friend
# create a new Thread object if thread of specific chat does not exists, otherwise return the thread
thread = None
try:
thread = await sync_to_async(Thread.objects.get, thread_sensitive=True)((Q(user1=me) & Q(user2=friend_instance)) | (Q(user1=friend_instance) & Q(user2=me)))
except:
thread = await sync_to_async(Thread.objects.create, thread_sensitive=True)(user1=me, user2=friend_instance)
self.room_name = thread.room_name # room name
await self.channel_layer.group_add(
self.room_name,
self.channel_name
)
await self.accept()
async def disconnect(self, close_code):
'''
disconnect the websocket connection.
'''
await self.channel_layer.group_discard (
self.room_name,
self.channel_name
)
async def receive(self, text_data):
text_data_json = json.loads(text_data)
message = text_data_json['message']
from_user = text_data_json['user']
to_user = text_data_json['friend']
from_user_instanse = await sync_to_async(User.objects.get, thread_sensitive=True)(username=from_user['username']) # get user object of friend
to_user_instanse = await sync_to_async(User.objects.get, thread_sensitive=True)(username=to_user['username']) # get user object of friend
thread_obj = await sync_to_async(Thread.objects.get, thread_sensitive=True)((Q(user1=from_user_instanse) & Q(user2=to_user_instanse)) | (Q(user1=to_user_instanse) & Q(user2=from_user_instanse)))
message_instane = await sync_to_async(Message.objects.create, thread_sensitive=True)(messag_body=message, from_user=from_user_instanse, to_user=to_user_instanse, thread=thread_obj)
await self.channel_layer.group_send(
self.room_name,
{
'type': 'chatroom_messages',
'message': message_instane.messag_body,
'user': message_instane.from_user
}
)
async def chatroom_messages(self, event):
message = event['message']
user = event['user']
user_serialized_data = UserSerializer(user)
await self.send(text_data=json.dumps({
'message': message,
'user': user_serialized_data.data
}))
in the template this is the connection request part with js:
chatSocket.send(JSON.stringify({ //the error shows in this part that WebSocket is already in CLOSING or CLOSED state
'message': msg,
'user': me,
'friend': friendName
}));
....
/* connection request */
const chatSocket = new WebSocket(
'wss://'
+ window.location.host
+ '/ws/chat/'
+ friendName['username']
+ '/'
);
in my chat app this I have routing.py
from django.urls import re_path
from . import consumers
websocket_urlpatterns = [
re_path(r'ws/chat/(?P<friend>\w+)/$', consumers.ChatConsumer.as_asgi()),
]
in my project I have also routing.py
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
import chat.routing
application = ProtocolTypeRouter ({
'websocket': AuthMiddlewareStack(
URLRouter (
chat.routing.websocket_urlpatterns
)
)
})
and this is actually my asgi.py :
import os
import django #added
#from django.core.asgi import get_asgi_application #removed
from channels.routing import get_default_application # added
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
django.setup() #added
application = get_default_application() #added
#application = get_asgi_application() #removed
in my procfile :
release: python manage.py migrate
web: daphne myproject.asgi:application --port $PORT --bind 0.0.0.0 -v2
worker: python manage.py runworker channels --settings=myproject.settings -v2
in my settings.py
ASGI_APPLICATION = "myproject.routing.application"
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [os.environ.get('REDIS_URL', 'redis://localhost:6379')],
},
},
}
it works perfectly in local but not anymore working after deployement, am I messing something

django channels redis took too long to shut down and was killed

so i was trying to do some basic stuff with channels.
i wrote a script in my html to return a message
script
<script>
const chatsocket = new WebSocket(
'ws://'+window.location.host+'/ws/test/'
)
chatsocket.onmessage = function (e) {
const data = JSON.parse(e.data)
console.log(data)
chatsocket.close()
}
</script>
my consumers.py
from channels.exceptions import StopConsumer
from channels.generic.websocket import AsyncWebsocketConsumer
import json
class NotificationConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.group_name = 'notificationgroup'
await self.channel_layer.group_add(
self.group_name,
self.channel_name
)
await self.accept()
await self.channel_layer.group_send(
self.group_name,
{
'type': 'tester_message',
'tester': 'hello world'
}
)
async def tester_message(self, event):
tester = event['tester']
await self.send(text_data=json.dumps({
'tester': tester
}))
async def disconnect(self, close_code):
await self.channel_layer.group_discard(
self.group_name,
self.channel_name
)
raise StopConsumer()
so basically i get my desired output in the console... when i have
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels.layers.InMemoryChannelLayer'
}
}
but when i use an aws redis cluster (after installing channels-redis obviously)
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
"hosts": [('aws redis endpoint', 6379)],
},
},
}
i get this error
WebSocket HANDSHAKING /ws/test/ [127.0.0.1:51551]
WebSocket DISCONNECT /ws/test/ [127.0.0.1:51551]
Application instance <Task pending name='Task-4' coro=<StaticFilesWrapper.__call__() running at E:\intes\sync\shero\venv\lib\site-packages\channels\staticfiles.py:44> wait_for=<Future pending cb=[BaseSelectorEventLoop._sock_write_done(1216)(), <TaskWakeupMethWrapper object at 0x000001F7D5226C70>()]>> for connection <WebSocketProtocol client=['127.0.0.1', 51551] path=b'/ws/test/'> took too long to shut down and was killed.
what am I doing wrong?
I was doing it all wrong. Apparently, you cannot get the aws-redis working outside. you must have your application running on an EC2 within the same VPN. Then, it works!

Why am I getting "No application configured for scope type 'web socket'" still?

I am following , django channels 2 chat app tutorial
I am at the part "writing your first consumer", I exactly did what is in tutorial, you can find my code here
But , I am still unable to connect to my web socket! my error trace :
HTTP GET /chat/ji/ 200 [0.05, 127.0.0.1:51247]
2018-06-23 07:51:43,181 - ERROR - ws_protocol - [Failure instance: Traceback: <class 'ValueError'>: No application configured for scope type 'websocket'
/anaconda3/envs/chatapp/lib/python3.6/site-packages/twisted/internet/defer.py:500:errback
/anaconda3/envs/chatapp/lib/python3.6/site-packages/twisted/internet/defer.py:567:_startRunCallbacks
/anaconda3/envs/chatapp/lib/python3.6/site-packages/twisted/internet/defer.py:653:_runCallbacks
/anaconda3/envs/chatapp/lib/python3.6/site-packages/twisted/internet/defer.py:1442:gotResult
--- <exception caught here> ---
/anaconda3/envs/chatapp/lib/python3.6/site-packages/twisted/internet/defer.py:1384:_inlineCallbacks
/anaconda3/envs/chatapp/lib/python3.6/site-packages/twisted/python/failure.py:422:throwExceptionIntoGenerator
/anaconda3/envs/chatapp/lib/python3.6/site-packages/daphne/server.py:186:create_application
/anaconda3/envs/chatapp/lib/python3.6/site-packages/twisted/python/threadpool.py:250:inContext
/anaconda3/envs/chatapp/lib/python3.6/site-packages/twisted/python/threadpool.py:266:<lambda>
/anaconda3/envs/chatapp/lib/python3.6/site-packages/twisted/python/context.py:122:callWithContext
/anaconda3/envs/chatapp/lib/python3.6/site-packages/twisted/python/context.py:85:callWithContext
/anaconda3/envs/chatapp/lib/python3.6/site-packages/channels/staticfiles.py:41:__call__
/anaconda3/envs/chatapp/lib/python3.6/site-packages/channels/routing.py:58:__call__
]
Not Found: /favicon.ico
[2018/06/23 07:51:43] HTTP GET /favicon.ico 404 [0.03, 127.0.0.1:51249]
[2018/06/23 07:51:47] WebSocket DISCONNECT /ws/chat/ji/ [127.0.0.1:51248]
I am unable to get where it's going wrong!
My main django project is chatapp, in that chat is an app.
chatapp/routing.py
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
import chat.routing
application = ProtocolTypeRouter({
# (http->django views is added by default)
'websocket': AuthMiddlewareStack(URLRouter(chat.routing.websocket_urlpatterns)),
})
chat/routing.py
from django.conf.urls import url
from . import consumers
websocket_urlpatterns = [
url(r'^ws/chat/(?P<room_name>[^/]+)/$', consumers.ChatConsumer),
]
chat/consumers.py
from channels.generic.websocket import WebsocketConsumer
import json
class ChatConsumer(WebsocketConsumer):
def connect(self):
self.accept()
def disconnect(self, close_code):
pass
def receive(self, text_data):
text_data_json = json.loads(text_data)
message = text_data_json['message']
self.send(text_data=json.dumps({
'message': message
}))
chat room where I am trying to web socket:
chat/templates/room.html
<!-- chat/templates/chat/room.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Chat Room</title>
</head>
<body>
<textarea id="chat-log" cols="100" rows="20"></textarea><br/>
<input id="chat-message-input" type="text" size="100"/><br/>
<input id="chat-message-submit" type="button" value="Send"/>
</body>
<script>
var roomName = {{ room_name_json }};
var chatSocket = new WebSocket(
'ws://' + window.location.host +
'/ws/chat/' + roomName + '/');
chatSocket.onopen = function(){
alert('Connection open!');
}
chatSocket.onmessage = function(e) {
var data = JSON.parse(e.data);
var message = data['message'];
document.querySelector('#chat-log').value += (message + '\n');
};
chatSocket.onclose = function(e) {
if(e.code == 1006)
console.error('Chat socket closed unexpectedly');
};
document.querySelector('#chat-message-input').focus();
document.querySelector('#chat-message-input').onkeyup = function(e) {
if (e.keyCode === 13) { // enter, return
document.querySelector('#chat-message-submit').click();
} };
document.querySelector('#chat-message-submit').onclick = function(e) {
var messageInputDom = document.querySelector('#chat-message-input');
var message = messageInputDom.value;
chatSocket.send({
'message': message
});
messageInputDom.value = '';
};
</script>
</html>
my project structure
If you are using centos updating redis from version 3 default version in centos to a version that is equal or greater than 6 may help. Worked for me.