Django channels — using WebSockets
-
Back-End
Django channels — using WebSockets
-two-way communication between web-server and client-page-
Http protocol follow the strict procedural schema: client request- server response. The server cannot send any real time update information to client page, unless the client page have predesigned ajax procedures for requesting information from time to time which is not very handy and not quite “real time”. WebSockets could do that: the server could send information to client page and so, we have a bi-directional communication between server and client.
Let’s begin by setting up a Django project using WebSockets.
python -m venv env source env/bin/activate pip install django pip install channels #asyncronous view support mkdir channels cd channels django-admin startproject config .
This is our starting project (based on Django 3.1.4):
channels/ ├── config │ ├── asgi.py │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py └── manage.py
Before installing channels, be sure you have installed python3-devel package, otherwise installation will fail with the error “Python.h: No such file or directory”.
After adding to ‘/config/settings.py’
INSTALLED_APPS = [
...
channels’,
]...
ASGI_APPLICATION = 'config.asgi.application'
and running
python manage.py runserver
we will see that we already have a Django ASGI enabled application:
... Django version 3.1.4, using settings 'config.settings' Starting ASGI/Channels version 3.0.3 development server at http://127.0.0.1:8000/ ...
Channels needs to use Redis (a Remote Dictionary Service, an in-memory data structure store, used as a database, cache). First, add in python environment
pip install channels-redis #channels interface with redis
Redis could be available as a docker service — after installing docker, start redis with:
sudo systemctl enable docker sudo docker run -p 6379:6379 -d redis:5
or as an individual service, with some methods of installation :
1)Redis server
yum install redis # on Fedora sudo systemctl enable redis
2) Python method redis-server install
pip install redis-serverIn a python terminal: >>> import redis_server >>> redis_server.REDIS_SERVER_PATH /home/mihai/all/data/work2020/env/lib64/python3.9/site-packages/redis_server/bin/redis-server In a shell terminal start 'redis-server' as a background process: /home/mihai/all/data/work2020/env/lib64/python3.9/site-packages/redis_server/bin/redis-server &
3) or you could install from source.
To verify that redis is active and listening on port 6379 , we can use:
ss -an | grep 6379 tcp LISTEN 0 511 0.0.0.0:6379 0.0.0.0:* tcp LISTEN 0 511 [::]:6379 [::]:*
or test redis by running a python script (see docs): verif_channels.py
import os, sys, django
project_path = "."
project_settings = "config"if project_path == "":
sys.path.append(os.getcwd())
else:
sys.path.append(project_path)
os.environ.setdefault("DJANGO_SETTINGS_MODULE", project_settings + ".settings")
django.setup()
import channels.layers
channel_layer = channels.layers.get_channel_layer()
from asgiref.sync import async_to_sync
async_to_sync(channel_layer.send)('test_channel', {'type': 'hello'})
ret = async_to_sync(channel_layer.receive)('test_channel')
print(ret)
if(ret == {'type': 'hello'}):
print("Ok: redis is working")
else:
print("Error: redis not working")
At this point, we need to inform django project to use redis by adding in ‘/config/settings.py’:
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
"hosts": [('127.0.0.1', 6379)],
},
},
}
Let’s also ad an app as a playground space.
python manage.py startapp app
Make ‘/config/urls.py’ as:
from django.contrib import admin
from django.urls import path, includeurlpatterns = [
path('admin/', admin.site.urls),
path('', include('app.urls')),
]
And let to be ‘/app/urls.py’
from django.urls import path
from . import viewsurlpatterns = [
path('', views.home, name = 'home'),
]
add to ‘/app/views.py’
from django.http import HttpResponsedef home(request):
return render(request, 'app/index.html', {})
and app to ‘/config/settings.py’
INSTALLED_APPS = [
...
'app',
]
create /app/templates/app/index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>WebSockets!</title>
</head>
<body>
<h1>Hello, WebSockets!</h1>
</body></html>
To suppress warning messages, run also
python manage.py migrate
Now, we have a minimal Django project frame to dive in WebSockets “adventure” communication. (localhost:8000)
Consumers.py is the WebSockets equivalent of views.py procedures.
A simple syncron consumer will be like that:
class EchoConsumer(WebsocketConsumer): def connect(self):
self.accept()
self.send(text_data='connected') def receive(self, *, text_data):
print(text_data)
self.send(text_data="echo: "+text_data) def disconnect(self, message):
print('diconnect')
The same consumer, defined as asyncron, has the structure:
class AsyncEchoConsumer(AsyncWebsocketConsumer): async def connect(self):
self.n = 0
await self.accept()
await self.send(text_data='connected') async def receive(self, *, text_data):
self.n += 1
print(text_data)
await self.send(text_data="echo: "+ str(self.n) + " " + text_data) async def disconnect(self, message):
print('diconnect')
Corresponding WebSocket client, launched in frontend (html page) has the following structure:
...
<textarea id="display" rows="10" cols="50"></textarea>
<input id="submit" type="button" value="Send">
<script>
//echo
const echoSocket = new WebSocket(
'ws://'
+ window.location.host
+ '/ws/echo/'
);
echoSocket.onmessage = function(e) {
textarea = document.querySelector('#display');
textarea.value += (e.data + '\n');
//scroll to end
textarea.scrollTop = textarea.scrollHeight;
};
echoSocket.onopen = function(){
echoSocket.send("open conn");
}
echoSocket.onclose = function(){
echoSocket.send("close conn");
}
document.querySelector('#submit').onclick = function(e) {
echoSocket.send("hello!");
}
</script>
/app/routing.py is the WebSocket equivalent of /app/urls.py
from django.urls import re_path
from . import consumers
websocket_urlpatterns = [
re_path(r'ws/echo/',consumers.AsyncEchoConsumer.as_asgi()),
]
For much fun is welcomed the addition of a clock refreshed by async WebSockets data refreshes from the server to the page:
client page socket:
<script>
//clock
const clockSocket = new WebSocket(
'ws://'
+ window.location.host
+ '/ws/clock/'
);
clockSocket.onmessage = function(e) {
const data = JSON.parse(e.data);
document.querySelector('#clock').innerHTML = (data.message);
};
</script>
server async consumer which sends time data every 1 sec to the html page:
class clock(AsyncWebsocketConsumer):
async def connect(self):
await self.accept()
await self.start_periodic_task()
async def start_periodic_task(self):
while True:
bucharest = pytz.timezone('Europe/Bucharest')
now_local = datetime.now(tz=bucharest)
current_time = now_local.strftime("%H:%M:%S - %d/%m/%Y -") + " Europe/Bucharest"
#now = datetime.now()
#current_time = now.strftime("%H:%M:%S")
await self.send(text_data=json.dumps({
'message': current_time
}))
await asyncio.sleep(1)
But WebSockets would have limited usefullnes without the possibility of broadcasting message to a group of sockets. This is done by including by grouping sockets in the same “channel_layer”.
We will update our AsyncEchoConsumer like that:
class EchoConsumer(WebsocketConsumer):
def connect(self):
self.accept()
self.send(text_data='connected')
def receive(self, *, text_data):
print(text_data)
self.send(text_data="echo: "+text_data)
def disconnect(self, message):
print('disconnect')
class AsyncEchoConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.n = 0
self.group_name = "mygroup"
# Join group
await self.channel_layer.group_add(
self.group_name,
self.channel_name
)
await self.accept()
await self.send(text_data='connected')
async def receive(self, *, text_data):
self.n += 1
print(text_data)
#this below will send message only to the sender socket not to group
#await self.send(text_data="echo: "+ str(self.n) + " " + text_data)
# Send message to group
await self.channel_layer.group_send(
self.group_name,
{
'type': 'group_message', #this is sending pocedure name
'message': text_data,
'n': self.n
}
)
# Sending message to group
async def group_message(self, event):
message = event['message']
n = event['n']
await self.send(text_data="echo: "+ str(n) + " " + message)
async def disconnect(self, message):
# Leave group
await self.channel_layer.group_discard(
self.group_name,
self.channel_name
)
print('disconnect')
Add new sockets in client page:
<textarea id="display2" rows="10" cols="30"></textarea>
<textarea id="display3" rows="10" cols="30"></textarea>
<script>
...
//echo2
const echoSocket2 = new WebSocket(
'ws://'
+ window.location.host
+ '/ws/echo2/'
);
echoSocket2.onmessage = function(e) {
textarea = document.querySelector('#display2');
textarea.value += (e.data + '\n');
//scroll to end
textarea.scrollTop = textarea.scrollHeight;
};
//echo3
const echoSocket3 = new WebSocket(
'ws://'
+ window.location.host
+ '/ws/echo3/'
);
echoSocket3.onmessage = function(e) {
textarea = document.querySelector('#display3');
textarea.value += (e.data + '\n');
//scroll to end
textarea.scrollTop = textarea.scrollHeight;
};
</script>
and new entries in routing.py:
re_path(r'ws/echo2/',consumers.AsyncEchoConsumer.as_asgi()), re_path(r'ws/echo3/',consumers.AsyncEchoConsumer.as_asgi()),
Resource: git repository
A more real application of grouping sockets by updating (syncronizing) data in multiple different page (one common counter which is access in the same time by all users, just simple app without any authentication system) is below:
counter-channels git repository