Frage

I am trying to create a Tornado application with several chats. The chats should be based on HTML5 websocket. The Websockets communicate nicely, but I always run into the problem that each message is posted twice.

The application uses four classes to handle the chat:

  • Chat contains all written messages so far and a list with all waiters which should be notified

  • ChatPool serves as a lookup for new Websockets - it creates a new chat when there is no one with the required scratch_id or returns an existing chat instance.

  • ScratchHandler is the entry point for all HTTP requests - it parses the base template and returns all details of client side.

  • ScratchWebSocket queries the database for user information, sets up the connection and notifies the chat instance if a new message has to be spread.

How can I prevent that the messages are posted several times? How can I build a multi chat application with tornado?

import uuid
import tornado.websocket
import tornado.web
import tornado.template

from site import models
from site.handler import auth_handler

class ChatPool(object):

    # contains all chats

    chats = {}

    @classmethod
    def get_or_create(cls, scratch_id):

        if scratch_id in cls.chats:
            return cls.chats[scratch_id]
        else:
            chat = Chat(scratch_id)
            cls.chats[scratch_id] = chat
            return chat


    @classmethod
    def remove_chat(cls, chat_id):

        if chat_id not in cls.chats: return
        del(cls.chats[chat_id])


class Chat(object):

    def __init__(self, scratch_id):

        self.scratch_id = scratch_id
        self.messages = []
        self.waiters = []

    def add_websocket(self, websocket):
        self.waiters.append(websocket)

    def send_updates(self, messages, sending_websocket):
        print "WAITERS", self.waiters   
        for waiter in self.waiters:
            waiter.write_message(messages)
        self.messages.append(messages)


class ScratchHandler(auth_handler.BaseHandler):

    @tornado.web.authenticated
    def get(self, scratch_id):

        chat = ChatPool.get_or_create(scratch_id)
        return self.render('scratch.html', messages=chat.messages,
                                           scratch_id=scratch_id)


class ScratchWebSocket(tornado.websocket.WebSocketHandler):

    def allow_draft76(self):
        # for iOS 5.0 Safari
        return True

    def open(self, scratch_id):

        self.scratch_id = scratch_id
        scratch = models.Scratch.objects.get(scratch_id=scratch_id)

        if not scratch:
            self.set_status(404)
            return

        self.scratch_id = scratch.scratch_id

        self.title = scratch.title
        self.description = scratch.description
        self.user = scratch.user

        self.chat = ChatPool.get_or_create(scratch_id)
        self.chat.add_websocket(self)        

    def on_close(self):
        # this is buggy - only remove the websocket from the chat.
        ChatPool.remove_chat(self.scratch_id)

    def on_message(self, message):
        print 'I got a message'
        parsed = tornado.escape.json_decode(message)
        chat = {
            "id": str(uuid.uuid4()),
            "body": parsed["body"],
            "from": self.user,
            }

        chat["html"] = tornado.escape.to_basestring(self.render_string("chat-message.html", message=chat))
        self.chat.send_updates(chat, self)

NOTE: After the feedback from @A. Jesse I changed the send_updates method from Chat. Unfortunately, it still returns double values.

class Chat(object):

    def __init__(self, scratch_id):

        self.scratch_id = scratch_id
        self.messages = []
        self.waiters = []

    def add_websocket(self, websocket):
        self.waiters.append(websocket)

    def send_updates(self, messages, sending_websocket):

        for waiter in self.waiters:
            if waiter == sending_websocket:
                continue
            waiter.write_message(messages)

         self.messages.append(messages)

2.EDIT: I compared my code with the example provided demos. In the websocket example a new message is spread to the waiters through the WebSocketHandler subclass and a class method. In my code, it is done with a separated object:

From the demos:

class ChatSocketHandler(tornado.websocket.WebSocketHandler):

    @classmethod
    def send_updates(cls, chat):
        logging.info("sending message to %d waiters", len(cls.waiters))

        for waiter in cls.waiters:
            try:
                waiter.write_message(chat)
            except:
                logging.error("Error sending message", exc_info=True)

My application using an object and no subclass of WebSocketHandler

class Chat(object):

    def send_updates(self, messages, sending_websocket):

        for waiter in self.waiters:
            if waiter == sending_websocket:
                continue
            waiter.write_message(messages)

        self.messages.append(messages)
War es hilfreich?

Lösung

If you want to create a multi-chat application based on Tornado I recommend you use some kind of message queue to distribute new message. This way you will be able to launch multiple application process behind a load balancer like nginx. Otherwise you will be stuck to one process only and thus be severely limited in scaling.

I updated my old Tornado Chat Example to support multi-room chats as you asked for. Have a look at the repository:

Tornado-Redis-Chat

Live Demo

This simple Tornado application uses Redis Pub/Sub feature and websockets to distribute chat messages to clients. It was very easy to extend the multi-room functionality by simply using the chat room ID as the Pub/Sub channel.

Andere Tipps

on_message sends the message to all connected websockets, including the websocket that sent the message. Is that the problem: that messages are echoed back to the sender?

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top