WebSocket в Django
Что такое WebSocket?
Ну, это протокол компьютерной связи, который используется для двунаправленной связи.
Он может использоваться сервером для отправки данных клиенту.
Короче говоря, они используются для связи в реальном времени.
Пример: чат-приложения, синхронизация данных в реальном времени в играх, SSE и т.д.
Хотя WebSocket кажется крутым, он может быть слишком перегружен для простых приложений, таких как приложение погоды, которое опрашивает данные через несколько интервалов.
Как вы можете реализовать их в Django?
Что ж, это легко, мы можем использовать пакет python, называемый channels
.
Они созданы теми же разработчиками, что и Django.
Просто откройте терминал и введите следующее, чтобы установить channels
python -m pip install -U channels["daphne"]
Он установит каналы и специальный веб-сервер ASGI под названием daphne
, который используется для обработки протокола ws://
.
Теперь в этом посте мы будем делать индикатор присутствия пользователя в сети. Это приложение сообщит клиенту количество пользователей, подключенных к серверу.
Также мы не будем использовать channel_layer
для трансляции сообщения, так как это слишком усложнит ситуацию.
Создайте приложение django
django-admin startproject django_websocket
Перейдите в папку django_websocket
и введите эту команду, чтобы создать новое приложение django.
cd django_websocket
python manage.py startapp presence
В settings.py
добавьте сервер daphne
и наше приложение presence
.
Добавьте daphne
сверху.
# django_websocket/settings.py
INSTALLED_APPS = [
'daphne' # ⬅ ASGI Webserver
'presence', # ⬅ Our Custom App
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
Добавьте следующие строки в settings.py
, чтобы позволить django также обрабатывать ASGI, а также обрабатывать перенаправление входа и выхода.
# django_websocket/settings.py
# Daphne Conf
ASGI_APPLICATION = "django_websocket.asgi.application"
LOGIN_REDIRECT_URL = "index"
LOGOUT_REDIRECT_URL = "index"
Теперь добавим код в asgi.py
и заменим его следующим:
import os
from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter # <- Add this
from channels.auth import AuthMiddlewareStack # <- Add this
from presence.consumers import PresenceConsumer # <- Add this
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_websockets.settings')
application = ProtocolTypeRouter(
{
"http": get_asgi_application(),
"websocket": AuthMiddlewareStack(
PresenceConsumer.as_asgi()
),
}
)
Давайте разберем это:
- С
1
по6
строчку импортируем все необходимые модули
os
: Для установки некоторых необходимых переменных окруженияget_asgi_application
: Просто обрабатывает http-сторону нашего приложения.ProtocolTypeRouter
: Позвольте нам обрабатывать различные типы интерфейсов протоколов, например, дляhttp
мы просто будем делать вещи http, но для соединений протокола WebSocket мы позволим нашему приложению обрабатывать часть веб-сокетов.AuthMiddlewareStack
: Поддерживает стандартную аутентификацию Django, при которой данные пользователя хранятся в сеансе. Он разрешает доступ только для чтения к пользовательскому объекту вscope
. Короче говоря, это позволяет нам получить текущийrequest.user
.PresenceConsumer
: Это наш заказной потребитель ASGI, который будет обрабатывать веб-сокеты. Мы будем делать это дальше- В строке
8
мы добавляем файл, который добавляет проектsettings.py
в окружение с именемDJANGO_SETTINGS_MODULE
. Нам не о чем беспокоиться. - В строке с
10
по17
мы объявляем приложение. Это то же самое"django_websocket.asgi.application
.
- Приложение эквивалентно
ProtocolTypeRouter
, который принимает словарь протоколов, таких какhttp
иwebsocket
. - WebSocket использует функцию, называемую
AuthMiddlewareStack
, которая использует наших потребителей
Теперь мы переходим к интересной части, которая делает самих потребителей WebSocket.
Во-первых, создайте имя файла Consumers.py
в папке presence
и напишите в нем этот код.
import json
from channels.generic.websocket import WebsocketConsumer
class PresenceConsumer(WebsocketConsumer):
connections = []
def connect(self):
self.accept()
self.user = self.scope["user"]
self.connections.append(self)
self.update_indicator(msg="Connected")
def disconnect(self, code):
self.update_indicator(msg="Disconnected")
self.connections.remove(self)
return super().disconnect(code)
def update_indicator(self, msg):
for connection in self.connections:
connection.send(
text_data=json.dumps(
{
"msg": f"{self.user} {msg}",
"online": f"{len(self.connections)}",
"users": [f"{user.scope['user']}" for user in self.connections],
}
)
)
Этот код может показаться пугающим, но на самом деле он довольно прост, если вы понимаете основную концепцию потребителей.
Давайте сломаем это.
- Со строк
1
по3
мы импортируем некоторые важные модули, такие какjson
иWebsocketConsumer
. - В строке
6
мы создаем класс с именемPresenceConsumer
, который является расширением классаWebsocketConsumer
.
WebsocketConsumer
дает нам некоторые методы для WebSocket, такие как
- метод
connect
вызывается, когда клиент подключается к WebSocket. Если мы переопределяем этот метод, нам нужно добавитьself.accept()
. - Метод
disconnect
вызывается, когда клиент отключается от WebSocket. - Метод
receive
вызывается, когда клиент отправляет запрос в WebSocket.
- Метод с именем
update_indicator
— это пользовательский метод, который мы создали. - connections — это пустой список, в котором будут храниться подключенные пользователи.
В методе connect
мы переопределяем методы, поэтому, когда новый пользователь подключается к WebSocket, мы получаем пользователя из сеанса с помощью этого кода self.user = self.scope["user"]
, так как мы обернули наш PreescenceConsumer
в AuthMiddlewareStack
, мы можем получить текущего пользователя из сеанса и сохранить его в файле self.user
. После того, как мы получили пользователя, мы собираемся добавить текущее пользовательское соединение к connection
, а затем вызвать self.update_indicator
и передать msg
как connected
.
Теперь, если мы посмотрим на update_indicator
, мы увидим, что мы перебираем список connection
и отправляем всем клиентам данные json.
Пример:
Если пользователь с именем Sid подключается к WebSocket с 3 пользователями, подключенными, все клиенты, подключенные к WebSocket, включая Sid, получат следующий json
{
"msg": "Sid Connected",
"online": 4,
"users" [
"user1",
"user2",
"user3",
"Sid"
]
}
Теперь мы обработаем некоторые из наших представлений для отображения индексной страницы и страницы входа. Просто добавьте эти 4 строки кода, и все готово.
from django.shortcuts import render
def index(request):
return render(request, "index.html")
Да, это все.
Теперь мы займемся маршрутизацией WebSocket.
Это похоже на URL-адрес, отображающий ваши представления в типичном приложении django.
Теперь в основном в очень больших приложениях мы должны просто разделить маршрутизацию представлений и маршрутизацию ws, но здесь, чтобы сэкономить время, мы просто напишем это в urls.py
Сначала создайте файл urls.py
в приложении presence
и добавьте следующий код.
from django.urls import path
from . import consumers
from . import views
from django.contrib.auth.views import LoginView, LogoutView
urlpatterns = [
path('', views.index, name="index"),
path('login/', LoginView.as_view(template_name='login.html'), name='login'),
path('logout/', LogoutView.as_view(), name='logout'),
path("ws/presence", consumers.PresenceConsumer.as_asgi()),
]
Здесь мы создаем сопоставление URL-адресов для наших представлений и WebSocket.
Как видите, мы также используем представления аутентификации django, чтобы получить функции входа и выхода в нашем приложении.
Чтобы добавить WS Consumer, нам просто нужно отобразить его в urlpatterns
, как и любые другие представления на основе классов, только вместо .as_view()
здесь мы пишем .as_asgi()
.
Это была backend часть, теперь нам нужно настроить нашу frontend часть.
Теперь давайте перейдем к frontend части
- Создайте каталог
template
в приложенииpresence
и создайте два файла с именамиindex.html
иlogin.html
.
В index.html
напишите следующий код.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Online Presence Indicator</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
</head>
<body>
<div class="container">
<section class="section">
<div class="columns">
<div class="column">
{% if user.is_authenticated %}
Hello <strong>{{ user }}</strong>
<br>
<a href="{% url 'logout' %}">Logout</a>
{% else %}
<a href="{% url 'login' %}">Login</a>
{% endif %}
<h1 class="title">Online Presence Indicator</h1>
<div id="presence"><span class="tag is-success" id="pre_cnt">0</span> users online</div>
<ul id="messages"></ul>
</div>
<div class="column">
<div class="box">
<h1 class="title">Online Users</h1>
<div id="online-users"></div>
</div>
</div>
</div>
</section>
</div>
<script>
const ws = new WebSocket('ws://localhost:8000/ws/presence/');
const presenceEl = document.getElementById('pre_cnt');
const messagesEl = document.getElementById('messages');
const onlineUsers = document.querySelector("#online-users");
ws.onmessage = (event) => {
onlineUsers.innerHTML = "";
let data = JSON.parse(event.data)
presenceEl.innerHTML = data.online;
const li1 = document.createElement('li');
li1.innerHTML = data.msg;
messagesEl.appendChild(li1);
data.users.forEach(user => {
const li2 = document.createElement("li");
li2.classList.add("on-us")
li2.innerHTML = user;
onlineUsers.appendChild(li2);
});
};
</script>
</body>
</html>
Здесь мы используем Bulma для стилизации нашей страницы, вы можете использовать свою собственную таблицу стилей, если хотите.
Главное, что мы должны здесь посмотреть, это javascript.
- Во-первых, мы создаем объект
WebSocket
и передаем URL-адрес WebSocket. - Далее мы сохраняем элементы html с помощью
getElementById
. - Затем мы обработаем событие
WebSocket.onmessage
для сообщений в реальном времени с сервера.Event
содержит атрибутdata
, к которому мы будем использоватьJSON.parse
для преобразования приходящего JSON в объект. - Когда WebSocket получит сообщение от службы, мы покажем, что пользователь подключен в div
#messages
и добавим пользователя в div#online-users
.
Теперь давайте просто быстро создадим нашу страницу входа.
Откройте login.html
и напишите этот html.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Online Presence Indicator</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
</head>
<body>
<div class="container">
<section class="section">
<h1 class="title">Login</h1>
<form action="." method="post">
{{form.as_p}}
{% csrf_token %}
<br>
<button class="button">Login</button>
</form>
</section>
</div>
</body>
</html>
Вот и все, теперь наше приложение готово.
Давайте теперь проверим это, запустив сервер
python manage.py runserver
Вы должны увидеть следующий поток.
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
May 23, 2023 - 22:54:21
Django version 4.2.1, using settings 'myproject.settings'
Starting ASGI/Daphne version 4.0.0 development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.
Мы видим, что Django использует сервер ASGI/Daphne для запуска этого веб-сайта.
Теперь откройте http://localhost:8000, и вы должны увидеть следующую страницу.
Вы увидите, что в окне сообщения будет написано AnonymousUser Connected
.
Давайте создадим учетную запись и протестируем ее.
python manage.py createsuperuser
Теперь, когда мы создали суперпользователя, давайте войдем под этой учетной записью и посмотрим.
Теперь откройте частную страницу и откройте обе страницы в режиме разделенного просмотра.
Демонстрационное видео
Вот ссылка GitHub для исходного кода этого приложения: https://github.com/foxy4096/django_websocket_demo
Так что за дело, ребята.