Пишем чат на Tornado, Backbone и веб-сокетах

17 комментариев

В этом руководстве мы создадим чат, работающий под управлением сервера Tornado. Для обмена сообщениями между пользователями и сервером будут использоваться веб-сокеты. В качестве базы данных для хранения сообщений возьмем MongoDB. Демонстрация работы чата.

Настройка окружения

Первым делом откроем unix-консоль и создадим виртуальную среду для нашего чата.

mkdir tornado-chat
cd tornado-chat/
virtualenv --no-site-packages ./env
source ./env/bin/activate

Если у вас еще не установлен пакет virtualenv — поставьте его с помощью пакетного менеджера вашей системы. Например, в Ubuntu установка virtualenv производится следующим образом:

sudo apt-get install python-virtualenv

В Gentoo:

sudo emerge virtualenv  

Теперь установим в нашу виртуальную среду web-сервер Tornado с помощью программы pip.

pip install tornado==4.3

Для хранения сообщений чата мы будем использовать базу данных MongoDB. Установим саму Mongo и ее драйвер для python — pymongo.

sudo apt-get install mongodb
pip install pymongo==3.2.1

Серверная часть

Напишем backend для нашего чата на Tornado. Сервер обработки сообщений будет слушать соединения на 8888-порту и добавлять идентификаторы всех активных клиентов в список WebSocketsPool. Когда сервер получает сообщение от клиента, оно сохраняется в базу. Затем рассылаются уведомления о новом сообщении всем остальным участникам беседы.

Таким образом, мы сможем достичь realtime-обновления чата у всех пользователей. В директории tornado-chat создайте файл server.py со следующим содержимым:

#!/usr/bin/env python
#!-*- coding: utf-8 -*-

import json

import tornado.web
import tornado.ioloop
import tornado.websocket

from tornado import template

import pymongo

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        db = self.application.db
        messages = db.chat.find()
        self.render('index.html', messages=messages)


class WebSocket(tornado.websocket.WebSocketHandler):
    def open(self):
        self.application.webSocketsPool.append(self)

    def on_message(self, message):
        db = self.application.db
        message_dict = json.loads(message);
        db.chat.insert(message_dict)
        for key, value in enumerate(self.application.webSocketsPool):
            if value != self:
                value.ws_connection.write_message(message)

    def on_close(self, message=None):
        for key, value in enumerate(self.application.webSocketsPool):
            if value == self:
                del self.application.webSocketsPool[key]

class Application(tornado.web.Application):
    def __init__(self):
        self.webSocketsPool = []

        settings = {
            'static_url_prefix': '/static/',
        }
        connection = pymongo.MongoClient('127.0.0.1', 27017)
        self.db = connection.chat
        handlers = (
            (r'/', MainHandler),
            (r'/websocket/?', WebSocket),
            (r'/static/(.*)', tornado.web.StaticFileHandler,
             {'path': 'static/'}),
        )

        tornado.web.Application.__init__(self, handlers)

application = Application()


if __name__ == '__main__':
    application.listen(8888)
    tornado.ioloop.IOLoop.instance().start()

Шаблон HTML

Класс MainHandler необходим для формирования HTML-страницы со списком сообщений и формой. В нашем шаблоне нет ничего интересного. Скопируйте его исходный код из репозитория на github в файл index.html.

Клиентская логика приложения

Создайте директорию static в корне проекта. Там будут находиться статические файлы нашего чата — картинки, стили и скрипты.

Наш скрипт будет обрабатывать событие отправки формы и формировать объект, содержащий ник автора и текст самого сообщения.

После чего сообщение будет передаваться серверу в формате JSON. Сервер же, в свою очередь сохранит его в базу данных и отправит уведомления всем остальным участникам конференции.

При уведомлении о новом сообщении будет вызываться событие onmessage из класса WebSocket. Само сообщение будет передаваться клиентам, также, в формате JSON, а затем преобразовываться в объект javascript с помощью функции JSON.parse.

При вызове события onmessage, новое сообщения добаляется в чат. Для создания красивой архитектуры скрипта мы воспользуемся библиотекой Backbone.js. Если читатель еще не знаком с Backbone, то самое время познакомиться с этим фреймворком поближе.

Создайте новую директорию static/js и добавьте туда новый файл main.js.

$(function () {

    var Socket = {
        ws: null,

        init: function () {
            ws = new WebSocket('ws://' + document.location.host + '/websocket');
            ws.onopen = function () {
                console.log('Socket opened');
            };

            ws.onclose = function () {
                console.log('Socket close');
            };

            ws.onmessage = function (e) {
                var message = new Message(JSON.parse(e.data));
                App.addOne(message);
            };

            this.ws = ws;
        }
    };

    Socket.init();
    var socket = Socket.ws;

    var Message = Backbone.Model.extend({
        defaults: function () {
            return {
                user: null,
                text: null,
            };
        },
        save: function (options) {
            socket.send(JSON.stringify(this));
        }
    });

    var MessageList = Backbone.Collection.extend({

        model: Message,

    });

    var Messages = new MessageList;

    var MessageView = Backbone.View.extend({

        tagName: 'div',

        className: 'message',

        template: _.template($('#message-template').html()),

        render: function () {
            this.$el.html(this.template(this.model.toJSON()));
            return this;
        }
    });

    var AppView = Backbone.View.extend({

        el: $('#backbone-chat'),
        lastMessage: $('.message').last(),

        events: {
            'submit #chat-form': 'createOnSubmit'
        },

        initialize: function () {
            if (this.lastMessage.length) {
                this.lastMessage[0].scrollIntoView();
            }

            this.textInput = this.$('#id_text');
            this.userInput = this.$('#id_user');

            this.listenTo(Messages, 'add', this.addOne);
            this.listenTo(Messages, 'reset', this.addAll);
            this.listenTo(Messages, 'all', this.render);

            Messages.fetch();
        },

        addOne: function (message) {
            var view = new MessageView({
                model: message
            });

            this.$('#chat-messages').append(view.render().el);
            this.$('.message').last()[0].scrollIntoView();
        },

        addAll: function () {
            Messages.each(this.addOne, this);
        },

        createOnSubmit: function () {
            this.userInput.removeClass('error');
            this.textInput.removeClass('error');

            if (!this.userInput.val().trim()) {
                this.userInput.addClass('error');
                this.userInput.focus();
                return false;
            }

            if (!this.textInput.val().trim()) {
                this.textInput.addClass('error');
                this.textInput.focus();
                return false;
            }

            Messages.create({
                user: this.userInput.val(),
                text: this.textInput.val()
            });

            this.textInput.val('');
            return false;
        },

    });


    var App = new AppView;
});

Внешний вид

Для оформления внешнего вида страницы над понадобится CSS фреймворк Twitter-Bootstrap. Скачайте последнюю версию библиотеки и распакуйте архив в директорию static/lib нашего проекта.

Создайте файл style.css внутри директории static/css. Содержимое файла стилей возьмите из репозитория на github.

Запуск Tornado

В корне проекта из консоли запустите следующие команды:

source env/bin/activate
python server.py

Первая команда активирует виртуальное окружение, созданное ранее с помощью virtualenv. В самом начале мы уже активировали его. Автор оставил эту команду здесь на тот случай, если читатель перезапустил свой shell, и случайно забыл про активацию окружения.

Вторая команда — python server.py запускает сервер Tornado, который обеспечивает работу нашего чата.

Не забудьте про запуск сервера MongoDB, если он не стартовал автоматически после установки. В Ubuntu/Debian Mongo запускается следующим образом:

sudo service mongodb start

В Gentoo:

sudo rc-service mongodb start

Теперь откройте чат в браузере, который поддерживает вебсокеты: http://127.0.0.1:8888/.

Чат на Tornado, Backbone и WebSockets

Скачать архив с исходниками чата можно из репозитория на github.

UPD (13 ноября 2016): обновил на хитхабе код чата. Перешел на python 3.5, впилил асинхронный Mongodb клиент и написал Dockerfile для более простого запуска чата, если хочется просто посмотреть.

Комментарии к статье: 17

Подождите, загружаются комментарии...

Возможность комментировать эту статью отключена автором. Возможно, во всем виновата её провокационная тематика или большое обилие флейма от предыдущих комментаторов.

Если у вас есть вопросы по содержанию статьи, рекомендуем вам обратиться за помощью на наш форум.