Last Updated:

Write chat on Tornado, Backbone and web sockets

In this tutorial, we'll create a chat running on the Tornado server. Web sockets will be used to exchange messages between users and the server. As a database for storing messages, let's take MongoDB. 

 

Setting up your environment

 

First of all, we will open the unix-console and create a virtual environment for our chat.

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

If you do not yet have the virtualenv package installed, install it using your system's package manager. For example, in Ubuntu, virtualenv is installed as follows:

sudo apt-get install python-virtualenv

At Gentoo:

sudo emerge virtualenv  

Now let's install tornado web server in our virtual environment using the .pip

pip install tornado==4.3

To store chat messages, we will use the MongoDB database. Let's install Mongo itself and its python driver, pymongo.

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

Server side

Let's write a backend for our chat on Tornado. The message processing server will listen for connections on the 8888 port and add the IDs of all active clients to the list. When the server receives a message from the client, it is stored in the database. Notifications of the new message are then sent to all other participants in the conversation.WebSocketsPool

Thus, we will be able to achieve real-time chat updates for all users. In the directory, create a file with the following content:tornado-chatserver.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 template

The class is required to generate an HTML page with a list of messages and a form. There's nothing interesting about our template. Copy its source code from the repository on github to a .MainHandlerindex.html

Client-side application logic

Create a directory at the root of the project. There will be static files of our chat — pictures, styles and scripts.static

Our script will handle the form submission event and generate an object containing the author's nickname and the text of the message itself.

After that, the message will be transmitted to the server in JSON format. The server, in turn, will save it to the database and send notifications to all other participants of the conference.

When a new message is notified, an event from the . The message itself will be passed to clients, also in JSON format, and then converted to an object using the .onmessageWebSocketjavascriptJSON.parse

When an event is raised, the new message is added to the chat. To create a beautiful script architecture, we will use the Backbone.js library. If the reader is not yet familiar with Backbone, then it's time to get acquainted with this framework better.onmessage

Create a new directory and add a new .static/jsmain.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;
});

Appearance

 

To design the appearance of the page above you will need the CSS framework Twitter-Bootstrap. Download the latest version of the library and extract the archive to the directory of our project.static/lib

Create a file inside the . Take the contents of the style file from the repository on github.style.cssstatic/css

Launch of Tornado

In the project root, from the console, run the following commands:

source env/bin/activate
python server.py

The first command activates the virtual environment created earlier with virtualenv. At the very beginning, we already activated it. The author left this command here in case the reader restarted his shell, and accidentally forgot about the activation of the environment.

The second command starts the Tornado server, which ensures the operation of our chat.python server.py

Don't forget to start the MongoDB server if it didn't start automatically after installation. On Ubuntu/Debian, Mongo runs as follows:

sudo service mongodb start

At Gentoo:

sudo rc-service mongodb start

Now open the chat in a browser that supports web slicers: http://127.0.0.1:8888/.

Chart

You can download the archive with the chat sources from the repository on github.

UPD (November 13, 2016): Updated the chat code on hithub. I switched to python 3.5, injected an asynchronous Mongodb client and wrote for easier chat launch if you just want to take a look. Dockerfile