Когда браузер клиенту нужно постоянное обновление данных с сервера, на ум сразу приходят сокеты. Но после множества просмотренных мной гайдов по данной теме, я не нашел ничего одновременно и актуального, и с нормальными объяснениями ну или хотя бы работающего. В итоге просидев пару-тройку часов у меня получилось собрать пазл из миллиона статей с Хабра и пары видеороликов от моих коллег из Индии.
Установка
В этой статье я не буду рассказывать как устанавливать Django, создавать модели и классы представлений. Предполагается, что у вас уже есть готовый проект и вы просто хотите впилить в него сокеты.
Python
pip install channels
pip install channels-redis
pip install daphne
Если при установке daphne пип выдает ошибку про C++, то скачиваем BuildTools и во время установки выбираем "Разработка классических приложений на C++". Ребутаем комп и снова запускаем pip install.
Шаг №1
В файл settings.py
добавляем загруженные пакеты (Причем исходя из документации - daphne надо добавить обязательно в начало списка):
INSTALLED_APPS = [
'daphne',
'channels',
...
]
В этом же файле изменяем:
WSGI_APPLICATION = 'yourapp.wsgi.application'
на:
ASGI_APPLICATION = 'yourapp.asgi.application'
Добавляем данные Channel layers в settings.py
, конфигурация зависит от того, где у вас стоит Redis, подробнее тут. Для тестов я использовал такой сетап:
CHANNELS_LAYERS = {
'default': {
'BACKEND': 'channels.layers.InMemoryChannelLayer'
}
}
Шаг №2
Создаем файл consumers.py
в директории нашего приложения. Если вы работали с Class Based Views в Django то Consumers вам покажется знакомым.
Создаем свой обработчик:
from channels.consumer import AsyncConsumer
YourConsumer(AsyncConsumer):
async def websocket_connect(self, event):
await self.send({"type": "websocket.accept"})
async def websocket_recieve(self, text_data):
await self.send({
"type": "websocket.send",
"text": "Hello from Django socket"
})
async def websocket_disconnect(self, event):
pass
Каждый метод класса YourConsumer
отвечает за свой тип запросов через сокеты. Очень похоже на CBV
Шаг №3
Изменяем данные файла asgi.py
в корневой директории проекта:
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter
from channels.routing import URLRouter
from django.core.asgi import get_asgi_application
from django.urls import path
import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'yourapp.settings')
from yourapp.consumers import YourConsumer
django_asgi_app = get_asgi_application()
application = ProtocolTypeRouter({
'http': django_asgi_app,
'websocket': AuthMiddlewareStack(
URLRouter([
path('ws', YourConsumer.as_asgi())
])
)
})
В URLRouter
мы прописываем путь, по которому будет доступен Consumer. Например в данном случае для 127.0.0.1 это - ws://127.0.0.1:8000/ws. Это очень похоже на концепцию urls.py
, кстати при желании можно вынести все пути в отдельный файл routers.py
и импортировать их потом в корневую директорию.
Тестируем
Запускаем локалку и отправляем запросы к сокету. Например через Js:
const socket = new WebSocket('ws://127.0.0.1:8000/ws');
socket.onopen = function(e) {
socket.send(JSON.stringify({
message: 'Hello from Js client'
}));
};
socket.onmessage = function(event) {
try {
console.log(event);
} catch (e) {
console.log('Error:', e.message);
}
};
Ссылки
Комментарии (7)
vagon333
18.11.2022 09:20У меня проект на .Net
Тестировал SocketIO и др. решения для фонового обновления клиентского кеша (IndexedDB).
В конечном итоге использую SignalR:не нужен отдельный порт для ws
простая реализация на клиентской и серверной стороне
интеграция в сущестующее веб-приложение, без необходимости в отдельном сервере
baldr
18.11.2022 13:28Ох, сэр.. Я сталкивался с SignalR когда мне нужно было получать какие-то данные по этому протоколу как клиенту. У меня основной код был на Python, поэтому пришлось искать библиотеку на нем. Нашел, конечно.. Конечно же, сразу не завелось. И пришлось поразбираться в исходниках, как обычно..
И если SocketIO не очень совместим с вебсокетами, то вот SignalR - это здоровенный комбайн с абстракциями и интерфейсами и полупроприетарным протоколом. Я уже не помню что у меня там не работало - что-то с сертификатами или шифрованием, кажется. Прикрутить это, вроде, можно было, но каким-то образом задача отменилась и я, с облегчением, все выкинул.
Я не могу сказать что он плохой - если у кого-то архитектура на .NET и все клиенты - это только веб-фронт, то почему нет, поскользу он ближе этой среде. Но брать как протокол для других языков - ох нет.
vagon333
18.11.2022 16:25Верно, для других языков SignalR не подойдет.
Как вы написали выше - в SignalR под капотом масса логики, включая авто-переход на long polling, если websockets по какой-то причине не поддерживается.
А на поверхности все просто.
baldr
Не используйте channels - возьмите что-нибудь вроде socket.io - там и клиентская часть есть, и движок с автореконнектом и может откатиться на long polling если websockets недоступен по какой-то причине. Автор питоновского движка довольно оперативно отвечает на баги и на SO.
MatveySss Автор
Мне нужен был минимальный функционал, потому что в качестве клиента выступает расширение для браузера. Взял самое простое и популярное решение.
Yuribtr
При перечисленных вами плюсах, у SocketIO есть помимо описанных плюсов некоторые минусы:
Протокол общения SocketIO не совместим с вебсокетами, это связано с тем, что SocketIO работает не только на вебсокетах
SocketIO генерирует заметно больше трафика при подключении.
Для запуска SocketIO нужно подгрузить JS библиотеку в браузер, в то время как вебсокеты работают «из коробки»
У библиотек SocketIO, которая используется на фронте (Javascript), и библиотеки SocketIO на сервере (python-socketio) должны совпадать мажорные версии. Там даже есть целая таблица совместимостей, то есть апгрейд на мажорную версию возможен только одновременно обоих движков. Например фронтовый движок версии 1,2 совместим с протоколом версии 3,4 который поддерживает движок версии 3. В крупных компаниях это неудобно делать.
У бэковой части SocketIO (EngineIO) проблема с распараллеливанием при установленном параметре workers в gunicorn больше единицы - ранее открытые вебсокет соединения теряются при открытии новых. Разработчик предписывает применять горизонтальное масштабирование без конкурентного выполнения.
неудобна концепция обработчиков на бэке - EngineIO крутится в loop параллельно основному приложению и путь прописывается у каждого экземпляра ASGIApp, то например сделать привычное версионирование API уже проблема. Плюс надо заранее закладывать имена всех событий которые могут прийти с фронта иначе просто не будет обработки такого сообщения.
Перехват событий коннекта и дисконнекта возможен только для всего AsyncServer а не для каждой версии API (ASGIApp) отдельно, а именно в них обычно производится авторизация.
baldr
Все верно, спасибо за подробный список. Но насчет последних двух пунктов - у channels то же самое. Плюс гораздо более простой движок, который не позволяет совсем ничего, в то время как SocketIO можно самому расширить, хотя придется покопаться в коде.
Не знаю как сейчас, но раньше у channels были и с масштабированием вопросы и нельзя было выполнить простейшую операцию типа узнать количество подключенных клиентов. Никак. В SocketIO из коробки тоже нельзя, но это решается. И масштабировать через брокер можно.
Поддержка чистых вебсокетов есть уже почти в каждом http-фреймворке - и в httpx, и в aiohttp.