20

I am building a realtime web application. I want to be able to send broadcast messages from the server-side implementation of my python application.

Here is the setup:

I can succesfully send socket.io messages from the client to the server. The server handles these and can send a response. In the following i will describe how i did that.

Current Setup and Code

First, we need to define a Connection which handles socket.io events:

class BaseConnection(tornadio2.SocketConnection):
    def on_message(self, message):
        pass

    # will be run if client uses socket.emit('connect', username)
    @event
    def connect(self, username):
        # send answer to client which will be handled by socket.on('log', function)
        self.emit('log', 'hello ' + username)

Starting the server is done by a Django management custom method:

class Command(BaseCommand):
    args = ''
    help = 'Starts the TornadIO2 server for handling socket.io connections'

    def handle(self, *args, **kwargs):
        autoreload.main(self.run, args, kwargs)

    def run(self, *args, **kwargs):
        port = settings.SOCKETIO_PORT

        router = tornadio2.TornadioRouter(BaseConnection)

        application = tornado.web.Application(
            router.urls,
            socket_io_port = port
        )

        print 'Starting socket.io server on port %s' % port
        server = SocketServer(application)

Very well, the server runs now. Let's add the client code:

<script type="text/javascript">    
    var sio = io.connect('localhost:9000');

    sio.on('connect', function(data) {
        console.log('connected');
        sio.emit('connect', '{{ user.username }}');
    });

    sio.on('log', function(data) {
        console.log("log: " + data);
    });
</script>

Obviously, {{ user.username }} will be replaced by the username of the currently logged in user, in this example the username is "alp".

Now, every time the page gets refreshed, the console output is:

connected
log: hello alp

Therefore, invoking messages and sending responses works. But now comes the tricky part.

Problems

The response "hello alp" is sent only to the invoker of the socket.io message. I want to broadcast a message to all connected clients, so that they can be informed in realtime if a new user joins the party (for example in a chat application).

So, here are my questions:

  1. How can i send a broadcast message to all connected clients?

  2. How can i send a broadcast message to multiple connected clients that are subscribed on a specific channel?

  3. How can i send a broadcast message anywhere in my python code (outside of the BaseConnection class)? Would this require some sort of Socket.IO client for python or is this builtin with TornadIO2?

All these broadcasts should be done in a reliable way, so i guess websockets are the best choice. But i am open to all good solutions.

Yuval Adam
  • 161,610
  • 92
  • 305
  • 395
Alp
  • 29,274
  • 27
  • 120
  • 198
  • I dont work with tornado but when I created app with similar functional on gevent. Gevent doesnt store pool of connected users, and I add all new incoming connections to list, and when needed send message to objects in this list )) – Denis Jun 08 '12 at 14:11
  • Ok, that would be one possibility. But as far as i know, socket.io supports broadcast messages. Maybe there is a way to utilize them? – Alp Jun 08 '12 at 14:14

3 Answers3

16

I've recently written a very similar application on a similar setup, so I do have several insights.

The proper way of doing what you need is to have a pub-sub backend. There's only so much you can do with simple ConnectionHandlers. Eventually, handling class-level sets of connections starts to get ugly (not to mention buggy).

Ideally, you'd want to use something like Redis, with async bindings to tornado (check out brukva). That way you don't have to mess with registering clients to specific channels - Redis has all that out of the box.

Essentially, you have something like this:

class ConnectionHandler(SockJSConnection):
    def __init__(self, *args, **kwargs):
        super(ConnectionHandler, self).__init__(*args, **kwargs)
        self.client = brukva.Client()
        self.client.connect()
        self.client.subscribe('some_channel')

    def on_open(self, info):
        self.client.listen(self.on_chan_message)

    def on_message(self, msg):
        # this is a message broadcast from the client
        # handle it as necessary (this implementation ignores them)
        pass

    def on_chan_message(self, msg):
        # this is a message received from redis
        # send it to the client
        self.send(msg.body)

    def on_close(self):
        self.client.unsubscribe('text_stream')
        self.client.disconnect()

Note that I used sockjs-tornado which I found to be much more stable than socket.io.

Anyway, once you have this sort of setup, sending messages from any other client (such as Django, in your case) is as easy as opening a Redis connection (redis-py is a safe bet) and publishing a message:

import redis
r = redis.Redis()
r.publish('text_channel', 'oh hai!')

This answer turned out pretty long, so I went the extra mile and made a blog post out of it: http://blog.y3xz.com/blog/2012/06/08/a-modern-python-stack-for-a-real-time-web-application/

Yuval Adam
  • 161,610
  • 92
  • 305
  • 395
  • Dude, you even read the question? This question: "Is there a tornado in the function which send message to all connected clients", but not about how to create backend for store connection objects. – Denis Jun 08 '12 at 14:32
  • 1
    @Denis, i think Yuval Adam answered exactly what i wanted to know. I am not 100% sure yet, but please consider undoing your downvote. – Alp Jun 08 '12 at 14:33
  • @Yuval Adam: Can you please explain why you found sockjs better than socket.io? I can still switch as i am not bound to a specific library – Alp Jun 08 '12 at 14:34
  • 5
    Sure. I started off with socket.io as well, and actually had a discussion with MrJoes (tornadio/sockjs-tornado maintainer) about it. He claimed that sockjs has 100% test coverage, and that socket.io is known to have some protocol bugs. I found this to be true, when moving into production sockjs indeed felt more solid (though we did have some other issues that weren't related to transport). – Yuval Adam Jun 08 '12 at 14:36
  • @Denis - the downvote is totally inappropriate. I gave a complete scenario for tornado messaging *done right*, which is based on a setup I use *in production* and is solid as a rock. – Yuval Adam Jun 08 '12 at 14:38
  • Thanks for your insight. What other issues did you have? I may have similar mechanisms and i'd like to avoid similar problems. – Alp Jun 08 '12 at 14:41
  • @YuvalAdam - I trust you, but real mean of question about function like tornado.send(all_chat_users, 'Hi !!!') and correct answer is: 'there is no such function'. And I upvote you right now. – Denis Jun 08 '12 at 14:46
  • I am always open for reasonable alternatives. I am now trying sockjs, brb :) – Alp Jun 08 '12 at 14:48
  • 2
    @Alp - for 'usual' applications this setup works like a charm. The app I built does *serious* messaging (dozens of messages per second, don't ask ;)) and it turns out Chrome really doesn't like that stuff. Chrome would crash after 1-5 minutes of such WebSocket usage. – Yuval Adam Jun 08 '12 at 14:50
  • @Yuval Adam my application could evolve to the same frequency of messages - or even worse. how do you deal with that problem? (maybe we can continue our discussion by mail. you can find it on my github page: https://github.com/alp82) – Alp Jun 08 '12 at 15:15
  • what does the line `self.client = brukva.Client()` mean? what's "brukva"? – Alp Jun 08 '12 at 16:36
  • Brukva is an async redis-tornado binding, there's a link (https://github.com/evilkost/brukva) in the answer – Yuval Adam Jun 08 '12 at 16:45
  • Python 3 solution with SockJS and tornado-redis: http://blog.kristian.io/post/47460001334/sockjs-and-tornado-for-python-real-time-web-projects/ – Alp Jan 17 '14 at 14:02
3

I write here, because it's difficult write in comments section. You can view examples for tornadoio2 in examples directory where you can find implementation of chat, and:

class ChatConnection(tornadio2.conn.SocketConnection):
    # Class level variable
    participants = set()

    def on_open(self, info):
        self.send("Welcome from the server.")
        self.participants.add(self)

    def on_message(self, message):
        # Pong message back
        for p in self.participants:
            p.send(message)

As you can see they implemented participants as set ))

Denis
  • 7,127
  • 8
  • 37
  • 58
  • That is a naive implementation. Anything more serious than that should use a proper pub-sub backend (see my answer). – Yuval Adam Jun 08 '12 at 14:24
  • thanks for showing how that works. but as Yuval Adam pointed out, using something like Redis could be the better choice as my application will be quite big and needs to scale well – Alp Jun 08 '12 at 14:35
  • @Alp If you really want create simple chat app, you dont need any of db backends. – Denis Jun 08 '12 at 14:40
  • chatting is just a small part of my web application, there is much more to be done in realtime – Alp Jun 08 '12 at 14:42
  • I bet this solution is faster than using Redis with Tornado. You'd want to use Redis if you're using a stateless backend, and not Tornado, which runs as a single process. – Ron Reiter Sep 29 '12 at 08:27
2

If you're already using django, why not have a look at gevent-socketio.

abourget
  • 2,351
  • 19
  • 17
  • i tried that, but it had problems with broadcasting too. i switched to sockjs + tornado + redis and it's working remarkably well – Alp Jun 09 '12 at 17:41