Примечение переводчика: вебсокеты и Django — это довольно сложная тема, которая уже не раз поднималась на хабрахабре и основной идеей является написание параллельного бэкенда для вебсокетов. Автор же предлагает довольно лаконичное решение этой проблемы, которому правда еще предстоит проверка временем.
TL;DR — Я пришел к очень простому решению для работы с вебсокетами в Django. Все что вам нужно — это установить django-websocket-request, запустить скрипт, и теперь ваше приложение поддерживает вебсокеты! Это решение заставляет Django думать, будто он получает нормальный (в какой-то мере) HTTP-запрос, поэтому оно будет совместимо почти со всем вашим существующим кодом. Решение работает нормально как с Django Rest Framework, так и с обычными функциями-представлениями и представлениями, основанными на классах (Class Based Views).
Мы разрабатываем Blimp 2 — это говорит о том, что у нас постоянно множество изменений в коде и инфраструктуре, и о том, как мы решаем старые и новые проблемы. Одно из решений, которое мы сделали в отношении нашего приложения, существенно изменило взаимодействие фронтенда и бэкенда.
В данный момент Blimp работает на Django. Он обслуживает наш HTML-код, обрабатывает наше публичное и приватное API, всю бизнес-логику. До последнего времени большинство веб-приложений примерно так и строились, однако Blimp имеет толстый JavaScript-клиент. С обычными запросами все происходит примерно следующим образом: вы запрашиваете URL, происходит какая-то работа со стороны бэкенда — запросы к базе данных, кеширование, обработка данных, отрисовка HTML-страниц, подгрузка сторонних CSS и JavaScript-библиотек, загрузка нашего собственного JavaScript-приложения, еще немного обработки данных, и наконец отрисовка результата.
Через несколько месяцев практики и роста нашего проекта, мы обнаружили несколько ключевых улучшений, которые мы можем внедрить — новая версия бэкенда должна будет заниматься формированием JSON, но не отрисовкой HTML, а наше фронтенд-приложение, которое выполняется со стороны клиента, будет просто потреблять данные через API. Мы решили, что оно будет использовать вебсокеты там, где это возможно, и обычный XHR во всех остальных случаях.
Веб-фреймворк Django и ему подобные построены для работы с циклом HTTP-запрос/ответ, поэтому всё в них, и представления, и механизмы аутентификации принимают HTTP-запрос на входе и отдают HTTP-ответ на выходе. С другой стороны, сервер вебсокетов не знает ничего о таком цикле.
Нашими главными целями были:
1. Одни и те же механизмы сериализации и десериализации данных, как со стороны HTTP-бэкенда, так и со стороны API вебсокетов.
2. Одна и та же бизнес-логика для всех.
Первой мыслью, которая пришла в голову, было превращение всей бизнес логики в методы наших моделей. Таким образом мы могли бы написать код единожды и разделить его между двумя бэкендами. В тот момент это казалось правильным решением, но через несколько часов было доказано, что нет. Так как мы работаем поверх Django REST framework, мы были бы вынуждены унаследовать и доработать десятки его классов и примесей, что на самом деле звучало не так уж плохо, но мы были настроены достаточно скептически и решили поискать другие идеи.
Мы знали, что хотим единое REST API, которое бы работало по двум каналам: HTTP и вебсокеты. Самым лучшим вариантом стало бы избежание переписывания чего бы то ни было только для работы его с вебсокетами. В какой-то момент меня озарило. Я вспомнил о Sails.js, который делает нечто подобное с тем, чего мы хотим достичь.
Sails поддерживает непривязанный к конкретному механизму обмен данными, что позволит вашим обработчикам автоматически работать и с Socket.io, и с вебсокетами. В прошлом вам пришлось бы писать отдельный код, чтобы получить такой результат.
Говоря более простым языком, мы хотели внедрить непривязанный ни к чему конкретному механизм обмена данными, который бы позволил нам использовать все что есть из Django-цикла запрос/ответ, чтобы автоматически формировать вебсокет-сообщения.
WebSocketRequest оказался неожиданно простым решением. Это простой класс, который принимает JSON-строку, содержащую следующие ключи: method, url, data, и token. Ключ method может быть любым HTTP-методом: GET, POST, PUT, DELETE, PATCH, HEAD или OPTIONS. Ключ url является абсолютным URL без доменного имени. Ключ data — это необязательный параметр — словарь, содержащий данные. Ключ token также является необязательным — используется для воссоздания заголовка HTTP-авторизации, авторизации через JSON Web Token или для ваших собственных ключей. Вы можете посмотреть мою статью , чтобы узнать больше о JSON Web Token, а если вы пользуетесь Django REST framework, то вам наверное понравится django-rest-framework-jwt.
WebSocketRequest работает следующим образом:
Да-да, фабрика запросов, вы прочитали верно. Вы можете быть знакомы с ней, если когда-нибудь писали тесты для Django-приложений, но если нет, фабрика запросов занимается тем, что генерирует объект запроса, который может быть использован как аргумент для любого представления. Единственным минусом является то, что такой запрос не поддерживает механизм middleware, что для некоторых может стать проблемой.
Мне определенно хотелось бы услышать о возможных проблемах данного подхода. В чем он может быть улучшен? Что может сломаться? Что насчет промышленного использования?
Обратите внимание, что Django в этом примере не используется вообще. Tornado выдает статический HTML-файл и передает все вебсокет-запросы django-websocket-request, в котором уже и происходит вся магия.
Я установил демо-приложение на Heroku: http://dwr-example.herokuapp.com/. Будьте внимательны, данные периодически стираются. Если вы обнаружите какую-то ошибку, напишите мне в твиттере о ней.
Вы можете установить WebSocketRequest при помощи pip:
Исходный код:
https://github.com/GetBlimp/django-websocket-request
Демо-приложение:
https://github.com/GetBlimp/django-websocket-request-example
TL;DR — Я пришел к очень простому решению для работы с вебсокетами в Django. Все что вам нужно — это установить django-websocket-request, запустить скрипт, и теперь ваше приложение поддерживает вебсокеты! Это решение заставляет Django думать, будто он получает нормальный (в какой-то мере) HTTP-запрос, поэтому оно будет совместимо почти со всем вашим существующим кодом. Решение работает нормально как с Django Rest Framework, так и с обычными функциями-представлениями и представлениями, основанными на классах (Class Based Views).
Подробнее
Мы разрабатываем Blimp 2 — это говорит о том, что у нас постоянно множество изменений в коде и инфраструктуре, и о том, как мы решаем старые и новые проблемы. Одно из решений, которое мы сделали в отношении нашего приложения, существенно изменило взаимодействие фронтенда и бэкенда.
В данный момент Blimp работает на Django. Он обслуживает наш HTML-код, обрабатывает наше публичное и приватное API, всю бизнес-логику. До последнего времени большинство веб-приложений примерно так и строились, однако Blimp имеет толстый JavaScript-клиент. С обычными запросами все происходит примерно следующим образом: вы запрашиваете URL, происходит какая-то работа со стороны бэкенда — запросы к базе данных, кеширование, обработка данных, отрисовка HTML-страниц, подгрузка сторонних CSS и JavaScript-библиотек, загрузка нашего собственного JavaScript-приложения, еще немного обработки данных, и наконец отрисовка результата.
Через несколько месяцев практики и роста нашего проекта, мы обнаружили несколько ключевых улучшений, которые мы можем внедрить — новая версия бэкенда должна будет заниматься формированием JSON, но не отрисовкой HTML, а наше фронтенд-приложение, которое выполняется со стороны клиента, будет просто потреблять данные через API. Мы решили, что оно будет использовать вебсокеты там, где это возможно, и обычный XHR во всех остальных случаях.
Веб-фреймворк Django и ему подобные построены для работы с циклом HTTP-запрос/ответ, поэтому всё в них, и представления, и механизмы аутентификации принимают HTTP-запрос на входе и отдают HTTP-ответ на выходе. С другой стороны, сервер вебсокетов не знает ничего о таком цикле.
Нашими главными целями были:
1. Одни и те же механизмы сериализации и десериализации данных, как со стороны HTTP-бэкенда, так и со стороны API вебсокетов.
2. Одна и та же бизнес-логика для всех.
Первой мыслью, которая пришла в голову, было превращение всей бизнес логики в методы наших моделей. Таким образом мы могли бы написать код единожды и разделить его между двумя бэкендами. В тот момент это казалось правильным решением, но через несколько часов было доказано, что нет. Так как мы работаем поверх Django REST framework, мы были бы вынуждены унаследовать и доработать десятки его классов и примесей, что на самом деле звучало не так уж плохо, но мы были настроены достаточно скептически и решили поискать другие идеи.
Мы знали, что хотим единое REST API, которое бы работало по двум каналам: HTTP и вебсокеты. Самым лучшим вариантом стало бы избежание переписывания чего бы то ни было только для работы его с вебсокетами. В какой-то момент меня озарило. Я вспомнил о Sails.js, который делает нечто подобное с тем, чего мы хотим достичь.
Sails поддерживает непривязанный к конкретному механизму обмен данными, что позволит вашим обработчикам автоматически работать и с Socket.io, и с вебсокетами. В прошлом вам пришлось бы писать отдельный код, чтобы получить такой результат.
Говоря более простым языком, мы хотели внедрить непривязанный ни к чему конкретному механизм обмена данными, который бы позволил нам использовать все что есть из Django-цикла запрос/ответ, чтобы автоматически формировать вебсокет-сообщения.
Решение
WebSocketRequest оказался неожиданно простым решением. Это простой класс, который принимает JSON-строку, содержащую следующие ключи: method, url, data, и token. Ключ method может быть любым HTTP-методом: GET, POST, PUT, DELETE, PATCH, HEAD или OPTIONS. Ключ url является абсолютным URL без доменного имени. Ключ data — это необязательный параметр — словарь, содержащий данные. Ключ token также является необязательным — используется для воссоздания заголовка HTTP-авторизации, авторизации через JSON Web Token или для ваших собственных ключей. Вы можете посмотреть мою статью , чтобы узнать больше о JSON Web Token, а если вы пользуетесь Django REST framework, то вам наверное понравится django-rest-framework-jwt.
WebSocketRequest работает следующим образом:
- Проверяет пришедшую JSON-строку
- Создает экземпляр фабрики запросов (RequestFactory)
- Динамически вызывает один из методов фабрики запросов, который возвращает экземпляр WSGIRequest
- Находит соответствующее URL представление
- Запускает найденное представление вместе со всеми данными, которые пришли из URL.
Да-да, фабрика запросов, вы прочитали верно. Вы можете быть знакомы с ней, если когда-нибудь писали тесты для Django-приложений, но если нет, фабрика запросов занимается тем, что генерирует объект запроса, который может быть использован как аргумент для любого представления. Единственным минусом является то, что такой запрос не поддерживает механизм middleware, что для некоторых может стать проблемой.
Мне определенно хотелось бы услышать о возможных проблемах данного подхода. В чем он может быть улучшен? Что может сломаться? Что насчет промышленного использования?
Демо
Обратите внимание, что Django в этом примере не используется вообще. Tornado выдает статический HTML-файл и передает все вебсокет-запросы django-websocket-request, в котором уже и происходит вся магия.
Я установил демо-приложение на Heroku: http://dwr-example.herokuapp.com/. Будьте внимательны, данные периодически стираются. Если вы обнаружите какую-то ошибку, напишите мне в твиттере о ней.
Вы можете установить WebSocketRequest при помощи pip:
pip install django-websocket-request
Исходный код:
https://github.com/GetBlimp/django-websocket-request
Демо-приложение:
https://github.com/GetBlimp/django-websocket-request-example