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

Posted

技术标签:

【中文标题】如何在 Tornado 中创建多个 websocket 聊天?【英文标题】:How can I create several websocket chats in Tornado? 【发布时间】:2014-02-24 15:01:30 【问题描述】:

我正在尝试创建一个包含多个聊天的 Tornado 应用程序。聊天应该基于 html5 websocket。 Websocket 可以很好地通信,但我总是遇到每条消息都发布两次的问题。

应用程序使用四个类来处理聊天:

Chat 包含到目前为止的所有书面消息和一个包含所有应通知的waiters 的列表

ChatPool 用作新 Websocket 的查找 - 当没有人拥有所需的 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 我将 send_updates 方法从 Chat 更改了。不幸的是,它仍然返回双精度值。

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:我将我的代码与提供的示例进行了比较。在 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)

【问题讨论】:

【参考方案1】:

如果你想创建一个基于 Tornado 的多聊天应用程序,我建议你使用某种消息队列来分发新消息。这样,您将能够在 nginx 等负载均衡器后面启动多个应用程序进程。否则,您将仅受制于一个流程,因此在扩展方面受到严重限制。

我更新了我的旧 Tornado 聊天示例,以支持您要求的多房间聊天。查看存储库:

Tornado-Redis-Chat

Live Demo

这个简单的 Tornado 应用程序使用 Redis Pub/Sub 功能和 websocket 将聊天消息分发给客户端。只需将聊天室 ID 用作 Pub/Sub 频道,即可轻松扩展多房间功能。

【讨论】:

我希望我能为这个大卫奖励你更多的代表!您是否可以使用 wss 和正确的身份验证来更新您的 Redis-Chat。这对帮助凡人大有帮助。 非常感谢您的建议! redis 与数据库相比如何?我看了一下,没有查询这样的东西,如果我想只获取指定时间戳之后的最新消息,必须在程序代码中进行吗? 为了实现身份验证,我建议覆盖 WebSocketHandler 处理程序的 get 方法,@remudada。这将在套接字建立之前在 websocket 握手上调用。因此,这是验证身份验证令牌的正确位置。对于 WSS 支持,我建议使用 Nginx 之类的负载均衡器进行加密。 Tornado 可以处理 SSL,但 Nginx 更方便。 @V3ss0n:可以使用Redis的lrange command获取聊天的最后n条消息。【参考方案2】:

on_message 将消息发送到所有连接的 websocket,包括发送消息的 websocket。这就是问题所在:消息被回显给发件人吗?

【讨论】:

我在send_updates 添加了一个skip 子句-> 查看编辑。不幸的是,它仍然会产生两倍的值。 问题可能出在javascript方面吗?可能是一个错误的错误处理程序一次打开多个连接?

以上是关于如何在 Tornado 中创建多个 websocket 聊天?的主要内容,如果未能解决你的问题,请参考以下文章

Tornado 不处理 linux 上的 node-http-proxy websockes

如何在一个请求中在 Mongoose 中创建多个文档

如何在 Pentaho 中创建多个存储库

Tornado websocket 客户端:如何异步 on_message? (从未等待协程)

如何使用 Javascript 在 JSON 中创建多个对象? [复制]

如何在 HSQLDB 中创建多个目录