klen.github.io

in Blog

Создание сервера оповещений с использованием Tornado и Socket.IO

Настройка dnsmasq для локальной разработки (linux) Ctrl→
←Ctrl Простейшая реализация протокола JSONP


Структура современных веб-приложений зачастую включает в себя некие HTTP API с которыми работают подключенные к серверу клиенты. Обычно для них важно оперативно обновлять информацию о произошедших на сервере событиях.

Note

Например оповещения о сообщениях в чатах, события в онлайн-играх, поступление новых заказов, предложений в системах коммерции и тп.

В данный момент на большинстве сайтов это реализуется постоянными AJAX запросами к серверу с клиентской стороны. У этого способа существуют недостатки:

  • Создается лишняя нагрузка на сервер;
  • Информация может приходить с задержкой;
  • Увеличивается трафик на клиенте.

В данной статье рассматривается способ с использованием WebSocket и Comet технологий. Мы создадим простейший сервер оповещений с использованием SocketIO и Tornadio. С клиентской стороны будет создаваться постоянное подключение к нашему серверу и в нужный момент мы будем посылать оповещения о событиях.

Note

SocketIO — javascript библиотека предоставляющая единый интерфейс для связи с Comet сервером с использованием множества протоколов (WebSocket, Flashsocket, XHR multipart и тд.).

SocketIO уже поставляется с модулем для nodejs. С его помощью можно реализовать необходимую функциональность на сервере используя JavaScript. В данной статье рассматривается решение с использованием Python.

Note

Tornadio — python библиотека надстройка над Tornado для поддержки интерфейса SocketIO.

Note

Для дальнейшего чтения предполагается, что вы знакомы с Python, VirtualEnv и JavaScript.

Установка и настройка Tornadio

  1. Для начала создадим и активируем виртуальное окружение (VirtualEnv) для нашего сервера:
# Создаем директорию для сервера
mkdir -p ~/Projects/tornado_pushserver
cd ~/Projects/tornado_pushserver

# Создаем виртульное окружение
virtualenv .ve

# Активируем виртуальное окружение
source .ve/bin/activate
  1. Устанавливаем в созданное окружение Tornado и Tornadio:
easy_install tornadio

Создание приложения Tornado

  1. Напишем обработчик HTTP подключений к нашему серверу. Создадим и отредактируем файл handler.py:
import tornado.web


class BroadcastHandler(tornado.web.RequestHandler):

    def get(self):
        self.write('Hello from tornadio!')

Все, что он делает, это отдает строку 'Hello from tornadio!' при GET запросах.

  1. Создадим основной файл приложения app.py
import tornado.web
from tornadio import server

from handler import BroadcastHandler


urls = [ (r"/", BroadcastHandler) ]

application = tornado.web.Application( urls )

if __name__ == "__main__":
    server.SocketServer(application)

Здесь мы создали Tornado application использующее наш обработчик соединений.

Note

Код для текущего состояния нашего сервера, вы можете посмотреть по адресу: https://github.com/klen/example_tornadio_project/tree/0.1.0

Запустите сервер с помощью команды python app.py и откройте в браузере http://localhost:8001 вы должны увидеть ответ сервера. Если все в порядке остановите его из консоли нажав Ctrl+C.

Клиентская часть

  1. Создайте страницу для клиентов console.html:
<!DOCTYPE html>
<html>
    <script src="http://cdn.socket.io/stable/socket.io.js"></script>
    <script>
        window.onload = function(){
            var log = document.getElementById('log');

            var socket = new io.Socket(window.location.hostname, {
                port: 8001,
                rememberTransport: false
            });

            socket.connect();

            socket.addEvent('message', function(data) {
                log.innerHTML += '<p>' + data + '</p>';
            });
        };
    </script>
    <h2>Console client</h1>
    <div id='log'></div>
</html>

Разберем, что в ней происходит. Подключается SocketIO скрипт реализации протоколов. Создается объект socket с параметрами подключения к нашему серверу. Происходит подключение socket.connect. И все полученные от сервера сообщения выводятся в тело страницы.

  1. Теперь подключим этот шаблон на стороне сервера, изменив handler.py:
class BroadcastHandler(tornado.web.RequestHandler):
    def get(self):
        self.render('console.html')

Note

Код для текущего состояния нашего сервера, вы можете посмотреть по адресу: https://github.com/klen/example_tornadio_project/tree/0.2.0

Снова запустите сервер и подключитесь к нему в браузере: http://localhost:8001 Через какое то время в вы увидите ошибки в консоли. Они вызваны тем, что протоколы SocketIO у нас сейчас никто не обрабатывает. Выключите сервер и давайте создадим обработку подключений.

Обработка подключений

  1. Создайте файл connection.py:
import logging
from tornadio import SocketConnection


class ClientConnection(SocketConnection):
    clients = set()
    def on_open(self, *args, **kwargs):
        logging.warning('client connected')
        self.clients.add(self)

    def on_close(self):
        logging.warning('client disconnected')
        self.clients.remove(self)

    # **

Здесь мы создали класс для наших подключений. При подключении он сохраняет ссылку на себя в ClientConnection.client. При разрыве соединения удаляет.

  1. Подключим его в наше приложение app.py:
import tornado.web
from tornadio import server, get_router

from connection import ClientConnection
from handler import BroadcastHandler


urls = [ (r"/", BroadcastHandler), get_router(ClientConnection).route() ]

application = tornado.web.Application( urls )

if __name__ == "__main__":
    server.SocketServer(application)

Сейчас мы создали ресурс SocketIO для клиентских подключений.

  1. И доработаем на handler.py для отправки сообщений подключенным клиентам:
import tornado.web
from connection import ClientConnection


class BroadcastHandler(tornado.web.RequestHandler):

    def get(self):
        self.render('console.html')

    def post(self):
        message = self.get_argument('message')
        for client in ClientConnection.clients:
            client.send(message)
        self.write('message send.')

Мы добавили обработчик POST запросов, отправляющий сообщения нашим подключениям.

Note

Код для текущего состояния нашего сервера, вы можете посмотреть по адресу: https://github.com/klen/example_tornadio_project/tree/0.3.0

Запустите сервер и откройте несколько страниц по адресу http://localhost:8001 В консоли сервера вы должны увидеть сообщения о подключении. Давайте попробуем отправить сообщения подключенным клиентам с помощью POST запросов к нашему серверу.

curl localhost:8001 -d message=Ping
curl localhost:8001 -d message=Another_ping

Вы должны увидеть как сообщения появляются на открытых страницах. Сейчас самый простой вариант нашего сервера сообщений уже работает. Добавим в него возможность посылать сообщения конкретным клиентам. Остановите сервер.

Идентификация подключений

Есть несколько вариантов идентифицировать клиент. Например можно запрашивать пользовательскую сессию если она существует. Сейчас мы просто будем сообщать случайно генерированный ID серверу после подключения. Доработаем немного console.html:

<!DOCTYPE html>
<html>
    <script src="http://cdn.socket.io/stable/socket.io.js"></script>
    <script>
        window.onload = function(){

            var log = document.getElementById('log');

            var socket = new io.Socket(window.location.hostname, {
                port: 8001,
                rememberTransport: false
            });

            // register client
            socket.addEvent('connect', function(e){
                log.innerHTML += '<p>Connected.</p>';
                socket.send({
                    id: Math.floor(Math.random(1000) * 1000)
                });
            });

            socket.connect();

            socket.addEvent('message', function(data) {
                log.innerHTML += '<p>' + data + '</p>';
            });
        };
    </script>
    <h2>Console client</h1>
    <div id='log'></div>
</html>

Мы добавили реакцию на событие connect и отправляем на сервер информацию о нашем текущем ID.

  1. Добавим обработку идентификатора в наш класс подключений connection.py:
from tornadio import SocketConnection
import logging


class ClientConnection(SocketConnection):

    clients = set()

    def __init__(self, *args, **kwargs):
        self.id = None
        super(ClientConnection, self).__init__(*args, **kwargs)

    def on_open(self, *args, **kwargs):
        logging.warning('client connected')
        self.clients.add(self)

    def on_message(self, message):
        logging.warning(message)
        if not self.id:
            self.id = message.get('id', None)
        self.send('Hello client %s' % self.id)

    def on_close(self):
        logging.warning('client disconnected')
        self.clients.remove(self)

    # **

Теперь при получении сообщения, мы регистрируем идентификатор клиента.

  1. И добавим функциональности в handler.py:
import tornado.web
from connection import ClientConnection


class BroadcastHandler(tornado.web.RequestHandler):

    def get(self):
        self.render('console.html')

    def post(self):
        message = self.get_argument('message')
        key = self.get_argument('id', None)
        for client in ClientConnection.clients:
            if key and not key == client.id:
                continue
            client.send(message)
        self.write('message send.')

Сейчас мы проверяем запрос на наличие параметра id и в этом случае отправляем сообщение только конкретному подключенному клиенту.

Note

Код для текущего состояния нашего сервера, вы можете посмотреть по адресу: https://github.com/klen/example_tornadio_project/tree/0.4.0

Запустите сервер и откройте несколько соединений в браузере. Вы должны увидеть сообщения о подключении и ответы сервера с зарегистрированными ID:

Console client

Connected.

Hello client 63

Отправим несколько сообщений:

curl localhost:8001 -d message=Hello_all
curl localhost:8001 -d message=Hello_63&id=63

Note

Во-втором сообщении подставьте ID для одного из своих подключений

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

Использование

Итак сервер мы написали, каким образом его можно использовать? Например запустить на другом порту вашего основного домена и подключаться к нему с клиентских страниц. При этом если на основном сервере происходит какое то событие, делается POST запрос (в идеале асинхронный) к серверу оповещений который в свою очередь посылает сообщение клиенту. В качестве сообщения можно отправлять JSON с необходимой информацией.

Но стоит учитывать, что данная статья является ознакомительной и показывает создание простейшего сервера. В нем нет защиты от возможности злоумышленника рассылать собственные оповещения, не используются ресурсы SocketIO, нет автоматических пере-подключений с клиентской стороны при разрыве связи.

Это несложно реализовать и вы можете доработать сервер самостоятельно.

В заключении я рекомендую вам почитать документацию SocketIO, Tornadio и возможно Tornado.

Настройка dnsmasq для локальной разработки (linux) Ctrl→
←Ctrl Простейшая реализация протокола JSONP
alt