📜 ⬆️ ⬇️

HowTo: How to make Django friends with WebSocket (socket.io, sockjs)

Version: 0.2

I have a need for an atomic update in real time of the page for a certain number of users depending on the actions of other users (not herbalife chat). Understandably, you can throw everything in the trash and, like a young man , gash from scratch to tornado / twisted.web , but obviously not the most productive way (and I am not a broker even once) when everything you need is already working on Django and you need just a little bit ... Naturally, in its essence, WebSocket is asking here. And all would be nothing but Django WSGI application, and this standard does not imply such frills even close (yet). Googling of the Internet put, once again, on the work of the famous python-guru kmike (this is without sarcasm, because his work helped me personally more than once, for which my deepest bow to him!).

So, if you want to cross your Django project with a websocket using the js library socket.io or sockjs - wilcomman!

About update


The first version of the article was devoted only to the variant with Socket.io. In the process of work, there was a strange feeling that the library does not always determine the fallen connection. Those. when dumping the socket for a long time, she did not try to cling back. I fully admit that I myself did something wrong. In the comments to the first version of the article, people recommended sockjs (on their own initiative, because eventually they switched to it) and I decided to check this option. As a result, either he keeps the connection with all his teeth, but from the server side a strange situation has arisen when he stops responding (also most likely due to our fault, but the reason is still in the process of finding out). In any case, I decided to add an article for completeness (not without the urgent request of a friend from GooglePlus). In general, I leave the choice of implementation for you.
')

Introduction


I have long wanted to try something asynchronous, but everything was not a good reason, There was a need, and where it was completely unclear from where to start. Actually here I will try to create this most relevant (I myself have taken the above-mentioned document as the starting point, but it is quite old and some improvements have already appeared) the starting point for the start. There will be a familiar island of Django to which I will show how to let out a fresh breeze ...

By the way, from the work of kmike, a couple of functions are used without changes, I hope the author does not mind.

What we get


As a result, we will get an asynchronous service that runs next to the main django site, knows which django user sends / receives requests, and [service] can receive commands from django, performing some actions based on them in the user's browser.

Example


Take for example the hypothetical exchange. She has moderators and clients. Everything worked fine for you and it was necessary to give the moderators the opportunity to see changes in positions on the exchange in real time. In this case, the moderators can somehow operate with positions on the exchange and you can not just reload the page.

Before that, you have all together F5 sausages ... And, in general, highload , as such, we are not particularly interested.

Instruments


For the work we need:
pip install redis tornado-redis  pip install tornadio2  pip install sockjs-tornado     . 

As well as the library socket.io or sockjs

Theory


To work with socket.io, we will use the tornadio2 library, and for sockjs, sockjs-tornado, which are naturally based on the asynchronous tornado framework. This business will be started as manage command django (hello supervisor ). There are no special problems with the execution of Djangovsky when there is no tornadio, but in return we have a small gag, which is solved by the PubSub capabilities of Redis (in short, these are channels or message queues to which the publisher 's push messages, and the subscriber ' s receive them).

Excuse


In the course of the play, an attentive reader may notice inconsistencies, such as using django functions that are inherently synchronous, but this is a small sacrifice for the sake of rapid development. In addition, speech about hayload initially does not go well, and this is not a comprehensive solution, but a starting point. So I am leaving you to have fun with the nuances of your implementation and the bottlenecks of your code, for which I beg you to forgive me generously ...

Also see the kmike excuses in the document I refer to all the time.

Practice


The practice will be practical, because there are many explanations in the comments in the source code.

service.py

Actually the service itself, which will maintain connections with the browser, receive commands from django, sending them to clients (and similarly in the opposite direction).

The on_message method on_message mandatory for implementation, but in the example above, it is not needed, because everything is implemented on a newfangled event model (for socket.io).

Implementation for socket.io

 # -*- 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', message) def on_close(self): """       """ self.redis_client.unsubscribe([ 'order_lock', 'order_done' ]) self.redis_client.disconnect() 

Implementation for sockjs

 # -*- coding: utf-8 -*- import tornado import tornadoredis from sockjs.tornado import SockJSConnection 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 send(self, msg_type, message): """  . """ return super(Connection, self).send({ 'type': msg_type, 'data': message, }) def on_open(self, info): """   django. """ self.django_session = get_session(info.get_cookie('sessionid').value) 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.send('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.send('done', message) def on_close(self): """       """ self.redis_client.unsubscribe([ 'order_lock', 'order_done' ]) self.redis_client.disconnect() 

models.py

Source of change. Let it be a model.

 # -*- 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, })) 

Actually here the lock and done methods after executing some business logic send messages with the necessary information. This information will be obtained by the above service, processed and sent to client browsers.

Those. The action was performed by the user according to the standard scheme: he clicked the link / pressed the button, django completed the necessary actions, sent a notification to the channel for sending via the websocket and returned the classic answer to the user.

client.js

Do not forget to load in html socket.io.js or sockjs.js depending on your choice (links at the beginning of the article)!

Actually, the apofigy of all this action is work on the client side.

Implementation for socket.io

  var socket = io.connect('http://' + window.location.host + ':8989'); //      //     login,       socket.on('connect', function(){ socket.emit('login'); }); //   -    socket.on('disconnect', function() { setTimeout(socket.socket.reconnect, 5000); }); //    "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){ if (msg.action == 'highlight'){ $('.id_order_row__' + msg.order).addClass('order-row_is_locked'); }else{ $('.id_info_renew_orders').addClass('hidden'); } } … 

Implementation for sockjs

 socket_connect(); function socket_connect() { socket = new SockJS('http://' + window.location.host + ':8989/orders'); //      //     login,       socket.onmessage = function(msg){ window['ws_order_' + msg.data.type](msg.data.data); // ,      } socket.onclose = function(e){ setTimeout(socket_connect, 5000); }; } function ws_order_lock(msg){ if (msg.action == 'highlight'){ $('.id_order_row__' + msg.order).addClass('order-row_is_locked'); }else{ $('.id_info_renew_orders').addClass('hidden'); } } … 

async_server.py

This manage command, the file must be placed in the myProject/orderApp/management/commands folder. Also, do not forget, in each of the subfolders, the __init__.py file.

Implementation for socket.io

 # -*- 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) 

Implementation for sockjs

 # -*- 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 = SockJSRouter(Connection, '/orders') # sockjs      :( app = tornado.web.Application(router.urls) app.listen(8989) tornado.ioloop.IOLoop.instance().start() 


Now you can start the python manage.py async_server .

Source: https://habr.com/ru/post/128562/


All Articles