HowTo: Как подружить Django с WebSocket (socket.io)

от автора

Возникла у меня потребность атомарного обновления в реальном времени страницы у некоторого количества пользователей в зависимости от действий других пользователей (не гербалайф чат). Понятное дело, можно всё выкинуть в помойку и, по-молодецки, запилить с нуля на tornado/twisted.web, но явно не самый продуктивный путь (да и я не мо́лодец ни разу) когда всё что надо — уже работает на Django и нужно всего-то чуть-чуть… Естественным образом, по сути своей, сюда просится WebSocket. И всё бы ничего но Django WSGI приложение, а этот стандарт не предполагает таких выкрутасов даже близко (пока). Гугления интернетов навели, в очередной раз, на труд известного python-гуру kmike (это без сарказма, т.к. его работы выручали меня лично уже не однократно, за что нижайший ему поклон!).

Итак если вы хотите скрестить ваш Django проект с websocket посредством js библиотеки socket.io — вилькоммен!

Вступление

Давно хотелось попробовать чего-нибудь асинхронного, да всё не было хорошего повода, Тут появилась необходимость, а откуда стартовать было совершенно не ясно. Собственно здесь я попытаюсь создать эту самую актуальную (мной самим за отправную точку был взят упомянутый выше доку́мент но он довольно стар и уже появились некоторые усовершенствования) отправную точку для старта. Будет знакомый островок Django к которому я покажу как подпустить свежего ветерку…

Кстати из труда kmike пара функций использована без изменений, надеюсь автор не против.

Что получим

В результате мы получим асинхронный сервис, который крутится рядом с основным django сайтом, знает какой django пользователь посылает/получает запросы, и [сервис] может получать команды от django, выполняя на их основе какие-то действия в браузере юзера.

Пример

Возьмём для примера гипотетическую биржу. У неё есть модераторы и клиенты. Всё работало у вас нормально и тут понадобилось дать модераторам возможность в реальном времени видеть изменения позиций на бирже. При этом модераторы могут как-то оперировать с позициями на бирже и нельзя просто перезагружать страницу.

До этого у вас все дружно колбасили F5… И, в общем, highload, как таковой, нас интересует не особо.

Инструменты

Для работы нам понадобятся:

pip install redis tornadio2 tornado-redis 

А также библиотека socket.io

Теория

Для работы с socket.io будем использовать библиотеку tornadio2, которая, естественным образом основана на асинхронном фреймворке tornado. Запускаться это дело будет как manage команда django (привет supervisor). Особых проблем с исполнением джанговского когда в tornadio нет, а вот в обратку у нас небольшой затык, который решается PubSub возможностями Redis (вкратце это такие каналы или очереди сообщений в которые publisher‘ы пихают сообщения, а subscriber‘ы их получают).

Отмазка

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

Также см. отмазки kmike в документе, на который я всё время ссылаюсь.

Практика

Практика будет практичной, потому много объяснений в комментариях в исходниках.

service.py

Собственно сам сервис, который будет поддерживать соединения с браузером, получать команды из django, отправляя их клиентам (и аналогично в обратную сторону).

Метод on_message обязателен к реализации, но в приведённом примере он не нужен, т.к. всё реализуется на новомодной событийной модели.

# -*- coding: utf-8 -*-  import tornado import tornadoredis from tornadio2 import SocketConnection from tornadio2.conn import event import django from django.utils.importlib import import_module from django.conf import settings from django.utils import simplejson  # start of kmike's sources _engine = import_module(settings.SESSION_ENGINE)   def get_session(session_key):     return _engine.SessionStore(session_key)   def get_user(session):     class Dummy(object):         pass      django_request = Dummy()     django_request.session = session      return django.contrib.auth.get_user(django_request) # end of kmike's sources   # конфиг для подключения к redis можно хранить в настройках django ORDERS_REDIS_HOST = getattr(settings, 'ORDERS_REDIS_HOST', 'localhost') ORDERS_REDIS_PORT = getattr(settings, 'ORDERS_REDIS_PORT', 6379) ORDERS_REDIS_PASSWORD = getattr(settings, 'ORDERS_REDIS_PASSWORD', None) ORDERS_REDIS_DB = getattr(settings, 'ORDERS_REDIS_DB', None)  # немного удобства unjson = simplejson.loads json = simplejson.dumps   class Connection(SocketConnection):     def __init__(self, *args, **kwargs):         super(Connection, self).__init__(*args, **kwargs)         self.listen_redis()      @tornado.gen.engine     def listen_redis(self):         """         Вешаем подписчиков на каналы сообщений.         """         self.redis_client = tornadoredis.Client(                 host=ORDERS_REDIS_HOST,                 port=ORDERS_REDIS_PORT,                 password=ORDERS_REDIS_PASSWORD,                 selected_db=ORDERS_REDIS_DB             )         self.redis_client.connect()          yield tornado.gen.Task(self.redis_client.subscribe, [             'order_lock',             'order_done'         ])         self.redis_client.listen(self.on_redis_queue)  # при получении сообщения                            #  вызываем self.on_redis_queue      def on_open(self, info):         """         Определяем сессию django.         """         self.django_session = get_session(info.get_cookie('sessionid').value)      @event  # событие, произошедшее в браузере     def login(self):         """         Определение пользователя и его возможностей         """         # это просто для примера входящей команды, определять юзера можно и в on_open         self.user = get_user(self.django_session)         self.is_client = self.user.has_perm('order.lock')         self.is_moder = self.user.has_perm('order.delete')      def on_message(self):         """         Обязательный метод.         """         pass      def on_redis_queue(self, message):         """         Обновление в списке заказов         """         if message.kind == 'message':  # сообщения у редиса бывают разного типа,                             # много сервисных, нам нужны только эти             message_body = unjson(message.body)  # разворачиваем сабж, как вы                                      #  поняли я передаю данные в JSON              # в зависимости от канала получения распределяем сообщения             if message.channel == 'order_lock':                 self.on_lock(message_body)              if message.channel == 'order_done:                 self.on_done(message_body)      def on_lock(self, message):         """         Заказ закреплён         """         if message['user'] != self.user.pk:  # юзеру-источнику действия сообщать о нём не надо             self.emit('lock', message)      def on_done(self, message):         """         Заказ выполнен         """         if message['user'] != self.user.pk:             if self.is_client:                 message['action'] = 'hide'             else:                 message['action'] = 'highlight'              self.emit('done', json(message))      def on_close(self):         """         При закрытии соединения отписываемся от сообщений         """         self.redis_client.unsubscribe([             'order_lock',             'order_done'         ])         self.redis_client.disconnect() 

models.py

Источник изменений. Пускай это будет модель.

# -*- coding: utf-8 -*-  import redis from django.conf import settings from django.db import models   ORDERS_FREE_LOCK_TIME = getattr(settings, 'ORDERS_FREE_LOCK_TIME', 0) ORDERS_REDIS_HOST = getattr(settings, 'ORDERS_REDIS_HOST', 'localhost') ORDERS_REDIS_PORT = getattr(settings, 'ORDERS_REDIS_PORT', 6379) ORDERS_REDIS_PASSWORD = getattr(settings, 'ORDERS_REDIS_PASSWORD', None) ORDERS_REDIS_DB = getattr(settings, 'ORDERS_REDIS_DB', 0)  # опять удобства service_queue = redis.StrictRedis(     host=ORDERS_REDIS_HOST,     port=ORDERS_REDIS_PORT,     db=ORDERS_REDIS_DB,     password=ORDERS_REDIS_PASSWORD ).publish json = simplejson.dumps  class Order(models.Model)     …      def lock(self):         """         Закрепление заказа         """         …          service_queue('order_lock', json({                 'user': self.client.pk,                 'order': self.pk,             }))      def done(self):         """         Завершение заказа         """         …          service_queue('order_done', json({                 'user': self.client.pk,                 'order': self.pk,             })) 

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

Т.е. действие выполнено пользователем по стандартной схеме: он кликнул ссылку/нажал кнопку, django отработал необходимые действия, послал уведомление в канал для рассылки через websocket и вернул юзеру классический ответ.

client.js

Не забывайте загрузить в html socket.io.js (ссылка в начале статьи)!

Собственно апофигей всего этого действа — работа на клиентской стороне.

    var socket = io.connect('http://' + window.location.host + ':8989');  // ваш порт для асинхронного сервиса     // при соединении вызываем событие login, которое будет выполнено на серверной стороне     socket.on('connect', function(){         socket.emit('login');     });      // при дисконнекте - пытаемся вернуть соединение     socket.on('disconnect', function() {         socket.socket.reconnect();     });      // при возникновении события "lock" вызываем "ws_order_lock" с полученным сообщением в качестве параметра     socket.on('lock', function(msg){         ws_order_lock(msg);     });      socket.on('done', function(msg){         ws_order_done(msg);     });  function ws_order_lock(msg){     msg = JSON.parse(msg);  // разворачиваем полученное сообщение в JSON      if (msg.action == 'highlight'){         $('.id_order_row__' + msg.order).addClass('order-row_is_locked');     }else{         $('.id_info_renew_orders').addClass('hidden');     } }  … 

async_server.py

Это manage команда, файл надо класть в папку myProject/orderApp/management/commands не забываем также, в каждой из подпапок файлик __init__.py.

 # -*- coding: utf-8 -*-  import tornado import tornadio2 as tornadio from django.core.management.base import NoArgsCommand  from myProject.order.tornado.service import Connection   class Command(NoArgsCommand):     def handle_noargs(self, **options):         router = tornadio.TornadioRouter(Connection)         app = tornado.web.Application(router.urls, socket_io_port=8989)  # ваш порт для асинхронного сервиса         tornadio.SocketServer(app) 

Теперь можно стартовать сервис python manage.py async_server.

ссылка на оригинал статьи http://habrahabr.ru/post/128562/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *