Продолжаю серию статей про Jii Framework. Сегодня настал момент релиза комета, о котором я и расскажу в этой статье.



Jii-comet — это масштабируемый, готовый к высоким нагрузкам и плохому интернету транспорт, реализующий постоянную связь между клиентом и сервером для мгновенного обмена данными.

Jii-comet предоставляет набор компонентов и классов, которые упрощают обмен сообщениями между каналами, подписки на них, обмена данными между серверами и так далее. Сам модуль не умеет доставлять сообщения на клиент и обратно, но в нем заложена абстракция, чтобы это можно было делать любой из существующих популярных библиотек (например, socket.io, sockjs), а так же чтобы это было надежно и масштабируемо.

Для тех, кто в первый раз слышит об этом фреймворке, рекомендую прочитать предыдущие статьи или посетить сайт. Если коротко, то
Jii — это фреймфорк, архитектура и API которого базируется на PHP фреймворке Yii 2.0, взяв из него лучшие стороны и сохраняя преимущества JavaScript.

Обзор


Для начала немного справки о том, что такое комет (wiki):
Comet (в веб-разработке) — любая модель работы веб-приложения, при которой постоянное HTTP-соединение позволяет веб-серверу отправлять (push) данные браузеру без дополнительного запроса со стороны браузера.

Возможности Jii-comet


Клиент:
  • Отправка и прием сообщений из каналов
  • Подписка на каналы
  • Вызов действий на сервере
  • Балансировка (случайный выбор сервера)
  • Возможность смены транспорта сообщений

Сервер:
  • Отправка и прием сообщений из каналов
  • Подписка на сообщения из канала
  • Подписывание соединения на канал
  • Отправка сообщений в соединение
  • Возможность смены транспорта сообщений, хаба и очереди
  • Масштабирование на несколько процессов и серверов
  • Режим listen-only (прослушивание каналов)
  • Запуск действий из очереди

Установка


Комет устанавливается как компонент приложения, на сервере и клиенте соответственно. На сервере комет открывает и «слушает» данные из указанного порта, а клиент при инициализации приложения подключается к этому порту и ждет информацию.

Рассмотрим пример приложения, где клиент подписывается на сервере на канал test и отправляет в него сообщение.
Сервер:

var Jii = require('jii');
require('jii-comet');

require('jii-workers')
    .application('comet', Jii.mergeConfigs(
        {
            application: {
                basePath: __dirname,
                components: {
                    comet: {
                        className: 'Jii.comet.server.Server',
                        port: 4401,

                        /**
                         *
                         * @param {Jii.comet.server.ConnectionEvent} event
                         */
                        'on addConnection': function(event) {
                            Jii.app.comet.subscribe(event.connection.id, 'test');
                        }
                    }
                }
            }
        },
        custom.comet || {}
    ));

Клиент:

require('jii/deps');
require('jii-comet/sockjs');

Jii.createWebApplication({
    application: {
        basePath: location.href,
        components: {
            comet: {
                className: 'Jii.comet.client.Client',
                serverUrl: 'http://localhost:4401/comet',

                /**
                 *
                 * @param {Jii.base.Event} event
                 */
                'on open': function(event) {
                    Jii.app.comet.send('test', {message: 'Hello World'});
                },

                /**
                 *
                 * @param {Jii.comet.ChannelEvent} event
                 */
                'on channel:test': function(event) {
                    console.log('Income message: ' + event.params.message);
                }
            }
        }
    }
}).start();

Далее делально рассмотрим серверные и клиентские компоненты.

Комет-сервер



Jii-comet предоставляет два класса для создания сервера:
  • Jii.comet.server.HubServer — сервер, настроенный только для прослушивания сообщений от соединений, при этом самих подключений у него нет, но он может отправить данные в подключение, находящееся в соседнем процессе. Т.е. в такой сервер напрямую (на порт) невозможно подключиться, однако сообщение можно отправить через другие процессы (к которым подключены клиенты).
  • Jii.comet.server.Server — полноценный сервер, унаследован он предыдущего. Хранит и поддерживает прямое соединение с клиентами, может обмениваться данными с клиентом.

Такое разделение необходимо для гибкого масштабирования приложения. Например, вы можете создать несколько процессов с компонентом Jii.comet.server.Server, которые будут только поддерживать соединение с клиентами, но не обрабатывать бизнес логику. И создать несколько процессов Jii.comet.server.HubServer, которые будут выполнять тяжелые вычисления. Это позволит сделать комет-канал более отзывчивым: если бы все операции были бы на одном сервере, то сложные операции затормаживали бы процесс, и все последующие запросы (даже легкие) были бы с задержкой.

Если у вас нет больших нагрузок или вы не знаете, что выбрать, выбирайте компонент Jii.comet.server.Server, так как он содержит в себе весь функционал.

Пример конфигурации для сервера выглядит так:

application: {
    components: {
        comet: {
            className: 'Jii.comet.server.Server',
            host: '0.0.0.0',
            port: 3100
        },

        // ...
    }
}

Полный список свойств, методов и событий представлен ниже.

Jii.comet.server.HubServer


Свойства


  • listenActions (boolean) — принимать ли запросы на вызов действий от клиентов. По умолчанию, true.
  • hub (object) — хаб, компонент или его конфигурация, имплементирующий интерфейс Jii.comet.server.hub.HubInterface для обмена сообщениями между процессами или серверами. По умолчанию создается компонент Jii.comet.server.hub.Redis.
  • queue (object) — очередь, компонент или его конфигурация, имплементирующий интерфейс Jii.comet.server.queue.QueueInterface для накопления очереди вызовов действий (actions). По умолчанию создается компонент Jii.comet.server.queue.Redis.

Методы


  • start() — открывает соединение для компонентов hub и queue, начинает слушать сообщения из каналов.
  • stop() — разрывает все соединения компонентов и перестает слушать информацию из вне.
  • sendToChannel(channel, data) — отправляет сообщение в канал в рамках хаба. Здесь channel — (string) название канала, а data — (string|object|*) данные для отправки.
  • sendToConnection(id, data) — как и предыдущий метод, отправляет данные, но не в канал, а напрямую в соединение. Здесь id (string) — это идентификатор соединения, взятый из Jii.comet.server.Connection, а data — (string|object|*) данные для отправки.
  • on(name, handler, data, isAppend) и off(name, handler) — методы для подписки и отписки на события. Список событий описан ниже, описание аргументов можно найти в разделе событий.
  • hasChannelHandlers(name) — Возвращает true, если на канал name (string) есть подписчики в рамках данного процесса.

События


  • channel — Событие запускается при любом входящем сообщении в любой канал. Первым аргументом обработчика будет передан экземпляр класса Jii.comet.ChannelEvent с параметрами channel (string) и message (string).
  • channel:%my_channel_name% — Событие запускается при любом входящем сообщении в канал, указанный после
Например, при вызове Jii.app.comet.on('channel:test', ...) происходит подписка на сообщения в канал test. Как и выше, в обработчик события первым аргументом будет передан экземпляр класса Jii.comet.ChannelEvent с параметрами channel (string) и message (string).
message — Событие запускается при любом входящем сообщении в хаб. Первым аргументом обработчика будет передан экземпляр класса Jii.comet.server.MessageEvent с параметром message (string).

Jii.comet.server.Server


Наследует вышеперечисленные свойства, методы и события и добавляет следующие:

Свойства


  • host (string) — хост, ожидающий входящие соединения от клиентов. По-умолчанию, 0.0.0.0.
  • port (number) — порт для входящих соединений. По-умолчанию, 4100.
  • transport (object) — компонент или его конфигурация, имплементирующий интерфейс Jii.comet.server.transport.TransportInterface для обмена сообщениями непосредственно с клиентами (браузером).

Методы


  • subscribe(connectionId, channel) и unsubscribe(connectionId, channel) — подписывает и отписывает соединение на заданный канал. Jii рекомендует делать подписку на канал именно на сервере, чтобы избежать гонки сообщений. В методах connectionId (string) — это идентификатор соединения, взятый из Jii.comet.server.Connection, а channel (string) — имя канала.

События


  • addConnection — Событие возникает при присоединении нового клиента. Первым аргументом обработчика будет передан экземпляр класса Jii.comet.server.ConnectionEvent с параметром connection (Jii.comet.server.Connection).
  • removeConnection — Событие возникает при потере соединения с клиентом. Аналогично предыдущему, первым аргументом обработчика будет передан экземпляр класса Jii.comet.server.ConnectionEvent.

Jii.comet.server.Connection


Содержит информацию о соединении с клиентом

Свойства


  • id (string) — идентификатор соединения. Используется для отправки сообщений напрямую в соединение.
  • request (Jii.comet.server.Request) — информация о соединении: заголовки, ip адрес, порт подключения и так далее.
  • originalConnection (object) — внутренний экземпляр соединения, полученный от транспорта. Специфичен для каждого транспорта.

Комет-клиент



На клиенте немного проще: есть один компонент Jii.comet.client.Client, который подключается к одному из серверов и обменивается данными.

application: {
    components: {
        comet: {
            className: 'Jii.comet.client.Client',
            serverUrl: 'http://localhost:4401/comet',
            autoOpen: true
        },

        // ...
    }
}

Полный список свойств, методов и событий представлен ниже.

Jii.comet.client.Client


Свойства


  • transport (object) — компонент или его конфигурация, имплементирующий интерфейс Jii.comet.client.transport.TransportInterface для обмена сообщениями с сервером.
  • plugins (object) — набор компонентов или их конфигурация, каждый из которых имплементирует интерфейс Jii.comet.client.plugin.PluginInterface для расширения возможностей комет клиента.
  • workersCount (number|null) — максимальное количество воркеров сервера. Эта настройка используется для балансировки нагрузки на клиенте. По-умолчанию, null — отключена.
  • autoOpen (boolean) — если указано true, то при инициализации приложения комет клиент автоматически будет подключаться к серверу. По-умолчанию, true.

События


  • open — Событие запускается при успешном открытии соединения.
  • close — Событие запускается при закрытии соединения.
  • beforeSend — Событие запускается при любом исходящем сообщении. Первым аргументом обработчика будет передан экземпляр класса Jii.comet.client.MessageEvent с параметром message. Вы можете изменить отправляемое сообщение, изменяя параметр message, на сервер отправятся данные из параметра message.
  • channel — Событие запускается при любом входящем сообщении в любой подписанный канал. Первым аргументом обработчика будет передан экземпляр класса Jii.comet.ChannelEvent с параметрами channel (string) и message (string).
  • channel:%my_channel_name% — Событие запускается при любом входящем сообщении в подписанный канал, указанный после :. Например, при вызове Jii.app.comet.on('channel:test', ...) происходит подписка на сообщения в канал test. Как и выше, в обработчик события первым аргументом будет передан экземпляр класса Jii.comet.ChannelEvent с параметрами channel (string) и message (string).
  • message — Событие запускается при любом входящем сообщении. Первым аргументом обработчика будет передан экземпляр класса Jii.comet.server.MessageEvent с параметром message (string).
  • beforeRequest — Событие запускается перед тем, как клиент пытается отправить на сервер запрос на выполнения действия. Первым аргументом обработчика будет передан экземпляр класса Jii.comet.client.RequestEvent с параметрами route (string) и params (object). Вы можете изменить параметры запроса в этом событии, на сервер отправятся данные из параметра params.
  • request — Событие запускается при ответе сервера на запрошенное ранее действие. Первым аргументом обработчика будет передан экземпляр класса Jii.comet.client.RequestEvent с параметрами route (string) и params (object).

Плагины


Плагины позволяют расширять возможности комет клиента. По-умолчанию, установлен плагин для автоматического восстановления соединения Jii.comet.client.plugin.AutoReconnect. Плагин очень прост в создании и подключении. Интерфейс плагина Jii.comet.client.plugin.PluginInterface не требует имплементации методов, а лишь предоставляет доступ к комет-клиенту через параметр comet, через который можно подписываться на события комета. Устанавливается и настраивается плагин через конфигурацию, путем добавления его в секцию plugins:

application: {
    components: {
        comet: {
            className: 'Jii.comet.client.Client',
            // ...
            plugins: {
                autoReconnect: {
                    enable: false
                },
                myPlugin: {
			        className: 'app.components.MyCometPlugin'
                }
            }
        },

        // ...
    }
}

Как это работает



С первого взгляда, jii-comet — это обертка над библиотекой комет-транспорта (sockjs, socket.io — на выбор), однако это не так. От библиотеки транспорта берется только функционал доставки сообщения на клиент и обратно. Реализация каналов, подписок, масштабирования и дополнительных фишек сделано именно в jii-comet. Архитектурно «под капотом» jii-comet разбит на следующие составляющие:

Сервер:
  • Транспорт (Jii.comet.server.transport.TransportInterface) — абстракция для обмена сообщениями между клиентом и сервером.
  • Хаб (Jii.comet.server.hub.HubInterface) — абстракция для обмена сообщениями между процессами и серверами.
  • Очередь (Jii.comet.server.queue.QueueInterface) — абстракция для накопления вызовов действий.
  • Соединение (Jii.comet.server.Connection) — экземпляр данного класса содержит информацию о соединении и доступен как компонент контекста.

Клиент:
  • Транспорт (Jii.comet.client.transport.TransportInterface) — абстракция для обмена сообщениями между клиентом и сервером.
  • Плагин (Jii.comet.client.plugin.PluginInterface) — абстракция для расширения возможностей комет клиента. По-умолчанию, установлен плагин для автоматического восстановления соединения Jii.comet.client.plugin.AutoReconnect.

Все эти абстракции могут настраиваться и переопределяться через конфигурацию приложения и контекста.

Примеры


  • несколько клиентов
  • несколько серверных процессов только для поддержания соединений (Jii.comet.server.Server и listenActions = false)
  • несколько серверных процессов для поддержания соединений и обработки действий (Jii.comet.server.Server и listenActions = true)
  • несколько серверных процессов только для обработки действий (Jii.comet.server.HubServer и listenActions = true)

Пример: Клиент отправляет сообщение в канал



jii-comet: Клиент отправляет сообщение в канал


  1. Клиент отправляет сообщение в канал test.
  2. Серверный процесс, поддерживающий соединение принимает это сообщение и отправляет в хаб (Jii.comet.server.hub.HubInterface).
  3. Хаб рассылает это сообщение всем серверным процессам, которые подписаны на данный канал (или подписаны их соединения).
  4. Серверные процессы получают сообщение и отправляют соединениям, которые на него подписаны.

Пример: Клиент вызывает действие на сервере



jii-comet: Клиент вызывает действие на сервере

  1. Клиент отправляет запрос на выполнение действия site/test.
  2. Серверный процесс, поддерживающий соединение принимает его и скидывает в очередь действий (Jii.comet.server.queue.QueueInterface), а так же через хаб (Jii.comet.server.hub.HubInterface) рассылает уведомление всем серверам, обрабатывающим действия (listenActions = true) о том, что очередь обновилась.
  3. Один из серверных процессов с listenActions = true вытаскивает запрос из очереди (методом lpop в имплементации Redis'а) и запускает у себя действие.
  4. После выполнения действия, серверный процесс отправляет результат действия (response) обратно клиенту. Поскольку данный серверный процесс не имеет прямого соединения с клиентом, он отправляет его в хаб (Jii.comet.server.hub.HubInterface).
  5. Хаб отправляет сообщение серверному процессу, подписанному на сообщения для этого соединения.
  6. Серверный процесс, поддерживающий это соединение получает сообщение и отправляет его непосредственно клиенту.

В третьем пункте запрос из очереди мог вытянуть любой серверный процесс с listenActions = true. Работает это по принципу «кто успел» и, как правило, успевать будет наименее нагруженный сервер. Таким образом, производится балансировка вызовов действий между серверами и их процессами.

Внутренний канал связи


Jii-comet имеет абстракцию, позволяющую использовать любую библиотеку в качестве внутреннего канала связи между клиентом и сервером. На данный момент сделан адаптер для библиотеки sockjs, в будущем появится и для socket.io.

В конфигурации транспорт объявляется в секции transport. По-умолчанию, используется класс Jii.comet.server.transport.Sockjs на севере и Jii.comet.client.transport.Sockjs на клиенте.
Транспорт можно конфигурировать и переопределять через конфигурацию:

comet: {
    className: 'Jii.comet.client.Client',
    transport: {
        className: 'Jii.comet.client.transport.Sockjs',
        transports: [
            'websocket',
            'xhr-streaming',
            'xdr-streaming',
            'jsonp-polling'
        ]
    }
}

The End


Сайт фреймворка — jiiframework.ru
GitHub — github.com/jiisoft
Пожелания и предложения отправляйте на affka@affka.ru

Нравится идея фреймворка? Ставь звезду на гитхабе!


Каким комет решением вы пользуетесь для node.js?

Проголосовало 45 человек. Воздержалось 26 человек.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Комментарии (7)


  1. SamDark
    14.10.2015 14:42

    Ну и зачем постить в блоге Yii?


    1. affka
      14.10.2015 15:30

      Убрал


  1. uLow
    15.10.2015 04:04
    -1

    Заглянул в исходники… навязался вопрос — а при чем тут Yii вообще?


    1. uLow
      15.10.2015 04:09

      Jii — это фреймфорк, архитектура и API которого базируется на PHP фреймворке Yii 2.0, взяв из него лучшие стороны и сохраняя преимущества JavaScript.

      Yii позаимствовал архитектуру у RoR (тогда уж Jails, а не Jii).
      А API Yii не используется ни коем разом.
      Так зачем вообще упоминать Yii? Пиар за чужой счет?


      1. auine
        15.10.2015 21:55

        Нравится человеку Yii, некоторые принципы пытается вынести на фронт. Решил созвучно назвать свой труд.
        Ваш ник uLow, пиаритесь за счет uCoz?


        1. uLow
          16.10.2015 00:46
          -1

          Фраза «базируется на ХХХ» подразумевает, что используется какая-то часть.
          К примеру о фреймворках — Silex и Symfony, Lumen и Laravel, Rails и Ruby…
          А Jii и Yii всего лишь созвучны. Это не Yii на Javascript. Это просто «yet another framework» для Node.js. Да, в чем-то есть привкус Yii, возможно разработчик думал о Yii в момент написания. Но Jii не является тем, чем его пытается выдать автор.


          1. affka
            16.10.2015 04:13

            Вы слова то не выкидывайте. «Архитектура и API» базируется. А как может архитектура использовать часть архитектуры?.. Она может только повторять.
            Мне кажется вы сами себе уже противоречите в формулировке: «Jii != Yii на JavaScript, а Jii = Yii для Node.Js»

            > Да, в чем-то есть привкус Yii,
            вы видимо вообще не смотрели фреймворк Jii, ваши суждение в корне неверны