如何在Tornado中创建多个WebSocket聊天?

3
我正在尝试创建一个具有几个聊天室的Tornado应用程序。 聊天应该基于HTML5 Websocket。 尽管Websockets之间可以通信,但我总是遇到每条消息都会被发布两次的问题。
该应用程序使用四个类来处理聊天:
- `Chat` 包含到目前为止所有编写的消息,以及一个列表与所有应该被通知的 `waiters` - `ChatPool` 用作新 Websockets 的查找 - 当没有具有所需 `scratch_id` 的用户时,它会创建一个新的聊天或返回现有聊天。 - `ScratchHandler` 是所有HTTP请求的入口点 - 它解析基本模板并返回客户端的所有详细信息。 - `ScratchWebSocket` 查询用户信息的数据库,建立连接,并在必要时通知聊天实例需要传播新消息。
如何防止多次发布消息? 如何使用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)

注意:根据 @A. Jesse 的反馈,我修改了 Chat 中的 send_updates 方法。不幸的是,它仍然返回双倍的值。

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.编辑: 我将我的代码与提供的演示示例进行了比较。在websocket示例中,通过WebSocketHandler子类和一个类方法向等待者传播新消息。在我的代码中,使用一个单独的对象完成:

来自示例:

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)

我的应用程序使用对象而不是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)
2个回答

9
如果你想基于Tornado创建一个多聊天应用程序,我建议你使用某种消息队列来分发新消息。这样,你就可以在负载均衡器(如nginx)后面启动多个应用程序进程。否则,你只能被限制在一个进程中,因此无法进行有效的扩展。
我更新了我的旧Tornado聊天示例,以支持你要求的多房间聊天。请查看存储库: Tornado-Redis-Chat 实时演示 该简单的Tornado应用程序使用Redis Pub/Sub功能和WebSockets将聊天消息分发给客户端。通过简单地使用聊天室ID作为Pub/Sub通道,很容易扩展多房间功能。

1
我希望我能为你这个davidn提供更多的声望奖励!你是否可以更新你的Redis-Chat,加入wss和适当的身份验证呢?这将在帮助普通人方面有很大的作用。 - remudada
非常感谢您的建议!Redis与数据库相比如何?我看了一下,似乎没有查询这样的东西,所以如果我想在指定时间戳之后仅获取最新的消息,必须在程序代码中完成吗? - Phyo Arkar Lwin
对于实现身份验证,我建议重写WebSocketHandler处理程序的get方法,@remudada。这将在建立套接字之前在websocket握手上调用。因此,这是验证身份验证令牌的正确位置。对于WSS支持,我建议像Nginx一样做负载均衡器加密。Tornado可以处理SSL,但Nginx更加方便。 - davidn
1
@V3ss0n:你可以使用Redis的lrange命令来获取聊天中最后n条消息。 - davidn

2

on_message方法会将消息发送到所有连接的Websockets,包括发送该消息的Websocket。问题在于消息会被回显给发送者吗?


我在 send_updates 中添加了一个跳过子句 -> 请查看编辑。 不幸的是,它仍然会输出两次值。 - Jon
1
问题可能出在 JavaScript 方面吗?也许是一个有缺陷的错误处理程序同时打开了多个连接? - Ben Darnell

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接