В этом руководстве мы создадим чат, работающий под управлением сервера 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/.
Скачать архив с исходниками чата можно из репозитория на github.
UPD (13 ноября 2016): обновил на хитхабе код чата. Перешел на python 3.5, впилил асинхронный Mongodb клиент и написал Dockerfile
для более простого запуска чата, если хочется просто посмотреть.
Комментарии к статье: 17
Возможность комментировать эту статью отключена автором. Возможно, во всем виновата её провокационная тематика или большое обилие флейма от предыдущих комментаторов.
Если у вас есть вопросы по содержанию статьи, рекомендуем вам обратиться за помощью на наш форум.