Django: WebSocket`ы и Channels

WebSockets — это технология, которая позволяет открывать сеанс интерактивной связи между браузером пользователя и сервером. С помощью этой технологии пользователь может отправлять сообщения на сервер и получать управляемые событиями ответы, не требуя длительного опроса, то есть без необходимости постоянно проверять сервер на предмет ответа. Подумайте, когда вы отвечаете на электронное письмо в Gmail, и в нижней части экрана вы видите всплывающее предупреждение «1 непрочитанное сообщение от [...]» от человека, на которого вы только что отвечали. Такая обратная связь в режиме реального времени обусловлена такими технологиями, как WebSockets!

Почему WebSockets?

Веб-сокеты позволяют установить длительное одно-сокетное TCP-соединение (протокол управления передачей) между клиентом и сервером, что позволяет мгновенно распределять двунаправленные полнодуплексные сообщения. Это делается с минимальными издержками, что приводит к соединению с низкой задержкой.

Реальные приложения для WebSockets бесконечны, в том числе приложения для чата, интернет вещей, многопользовательские онлайн-игры и просто любые приложения в реальном времени.

Что такое Channels?

Channels — это проект, который использует Django и расширяет его возможности за пределы HTTP — для обработки WebSockets, протоколов чата, IoT-протоколов и многого другого. Он построен на спецификации Python под названием ASGI.

Он делает это, беря ядро Django и размещая под ним полностью асинхронный слой, запуская сам Django в синхронном режиме, но обрабатывая соединения и сокеты асинхронно, и давая вам возможность писать в любом стиле.

 

Как подключить Channels?

В этой статье будет показан пример случая, когда вам может понадобиться постоянно отправлять данные с сервера клиенту.

Установка

  • Сначала создайте и активируйте виртуальную среду (pip или pipenv).
  • Установите Django.
  • Установите Channels и channels-redis в вашем виртуальном окружении, например:
    pip install channels
    pip install channels-redis

Если у вас современный макет проекта Django, например:

- my_proj/
   - manage.py
   - game/
   - my_proj/
      - __init__.py
      - settings.py
      - urls.py

Шаг 1:

Файлmy_proj/my_proj/settings.py

# Добавление Channels в приложения проекта
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'game',
    'channels',
.
.
.
.
# Конфигурация Channels
ASGI_APPLICATION = "olympia.routing.application"
CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'channels_redis.core.RedisChannelLayer',
        'CONFIG': {
            "hosts": [CHANNEL_REDIS_HOST],
            "symmetric_encryption_keys": [SECRET_KEY],
        },
    },
}

Шаг 2:

Каналы (Channels) предоставляют вам Потребителей (Consumers), богатую абстракцию, которая позволяет вам легко создавать приложения ASGI. Потребители, в частности, делают несколько вещей: структурируют ваш код как серию функций, которые будут вызываться всякий раз, когда происходит событие, вместо того, чтобы заставлять вас писать цикл события.

Потребитель - это класс, методы которого вы можете написать как обычные функции Python (синхронные) или как ожидаемые (асинхронные). Асинхронный код не должен смешиваться с синхронным кодом. Таким образом, есть функции преобразования для преобразования из асинхронного в синхронизацию и обратно. Помните, что части Django синхронны. Потребитель фактически является действующим приложением ASGI.

Итак, следующим шагом пишем my_proj/game/consumer.py.

# Встроенные импорты.
import json

# Импорты сторонних библиотек.
from channels.exceptions import DenyConnection
from channels.generic.websocket import AsyncWebsocketConsumer

# Импорты Django.
from django.core.exceptions import ObjectDoesNotExist
from django.contrib.auth.models import AnonymousUser

# Локальные импорты.
from my_proj.game.models import Game
from my_proj.game.utils import get_live_score_for_gameclass 

LiveScoreConsumer(AsyncWebsocketConsumer):
    async def connect(self):
       self.room_name = self.scope['url_route']['kwargs']['game_id']
       self.room_group_name = f'Game_{self.room_name}'

       if self.scope['user'] == AnonymousUser():
           raise DenyConnection("Такого пользователя не существует")

       await self.channel_layer.group_add(
           self.room_group_name,
           self.channel_name
       )

       # If invalid game id then deny the connection.
       try:
            self.game = Game.objects.get(pk=self.room_name)
       except ObjectDoesNotExist:
            raise DenyConnection("Неверный ID игры")

       await self.accept()

    async def receive(self, text_data):
       game_city = json.loads(text_data).get('game_city')

       await self.channel_layer.group_send(
            self.room_group_name,
            {
                'type': 'live_score',
                'game_id': self.room_name,
                'game_city': game_city
            }
        )

    async def live_score(self, event):
        city = event['game_city']
        # Вспомогательная функция, получающая счет игры из БД.
        await self.send(text_data=json.dumps({
                'score': get_live_score_for_game(self.game, city)
            }))

    async def websocket_disconnect(self, message):
        # Покинуть комнату группы
        await self.channel_layer.group_discard(
            self.room_group_name,
            self.channel_name
        )

Шаг 3:

Следующим шагом является настройка маршрутизаторов для потребителя. Маршрут, который мы задаем в файлах routing.py, используется веб-интерфейсом для фактического доступа к потребителю.

Как только соединение WebSocket установлено, браузер может отправлять или получать сообщения. Отправленное сообщение достигает маршрутизатора типа протокола, который определяет следующий обработчик маршрутизации на основе его транспортного протокола. Следовательно, вы можете определить маршрутизатор для HTTP и другой для сообщений WebSocket.

Файл: my_proj/game/routing.py

from django.urls import path
from channels.routing import ProtocolTypeRouter, URLRouter
from .consumers import LiveScoreConsumer

websockets = URLRouter([
    path(
        "ws/live-score/<int:game_id>", LiveScoreConsumer,
        name="live-score",
    ),
])

Файл: my_proj/routing.py

from channels.routing import ProtocolTypeRouter, URLRouter
from my_proj.game.routing import websockets

application = ProtocolTypeRouter({
    "websocket": websockets,
})

Шаг 4

Вы также можете добавить промежуточное программное обеспечение в ваши сокеты для проверки авторизации или других случаев.

Файл: my_proj/game/middlewares.py

# Импорты сторонних библиотек.
from urllib.parse import urlparse, parse_qs
from channels.auth import AuthMiddlewareStack

# Импорт DRF.
from rest_framework.authtoken.models import Token
from rest_framework.exceptions import AuthenticationFailed

# Импорты Django.
from django.contrib.auth.models import AnonymousUser
from django.db import close_old_connections

class TokenAuthMiddleware:
    """
    Промежуточный слой для токена авторизации
    """
    def __init__(self, inner):
        self.inner = inner    def __call__(self, scope):
        query_string = scope['query_string']
        if query_string:
            try:
                parsed_query = parse_qs(query_string)
                token_key = parsed_query[b'token'][0].decode()
                token_name = 'token'
                if token_name == 'token':
                     user, _ =  # Ваша функция аутентификации
                    scope['user'] = user
                    close_old_connections()
            except AuthenticationFailed:
                scope['user'] = AnonymousUser()
        else:
            scope['user'] = AnonymousUser()
        return self.inner(scope)

def TokenAuthMiddlewareStack(inner):
    return TokenAuthMiddleware(AuthMiddlewareStack(inner))

Если вы примените это промежуточное ПО, тогда ваш файл my_proj/routing.py будет:

from channels.routing import ProtocolTypeRouter, URLRouter
from my_proj.game.routing import websockets
from my_proj.game.middlewares import TokenAuthMiddlewareStack

application = ProtocolTypeRouter({
    "websocket": TokenAuthMiddlewareStack(websockets),
})

Тестирование

Теперь, когда вы успешно интегрировали каналы в свой проект, если вы хотите протестировать его, вы можете просто запустить сервер и открыть консоль Javascript, зайдя в браузер chrome и выбрав inspect.

lo = new WebSocket("ws://localhost:8000/ws/live-score/1?token=[YOUR TOKEN]");
lo.onmessage = (data) => console.log(data);
lo.onopen = () => {
    console.log("sending city");
    lo.send(JSON.stringify({"game_city": 1}));
}

Примечание: Так как в приведенном выше примере мы вызываем вспомогательную функцию для отправки текущего счета, после того, как соединение будет установлено, веб-интерфейсу необходимо будет отправлять запрос снова каждый раз, когда он хочет получить текущий результат в этот момент.

Вернуться на верх