Управление каналами Nginx Push Module с использованием Ruby

в 10:57, , рубрики: comet, nginx, ruby, Песочница, метки: ,

Приветствую всех! Многие разработчики вкурсе про замечательный модуль Nginx Push Module для веб-сервера Nginx. Многие его опробовали, пощупали.

Задача модуля – позволить веб-серверу Nginx выступать в качестве Comet-сервера.

Материала по использованию данного модулю достаточно: хороша официальная страница проекта, описание Basic HTTP Push Relay Protocol, а также многие статьи, например Nginx & Comet: Low Latency Server Push. Однако, во многих руководствах рассматривают лишь базовую конфигурацию модуля с использованием одного общедоступного канала всеми клиентами. Несмотря на огромную полезность, модуль не предоставляет разработчикам гибкое управление каналами, их защиту.

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

Задача

Что нам требуется?

  • cоздание нового канала
  • закрытие существующего канала
  • проверка существования канала
  • отправка данных в канал
  • отправка данных во все каналы

Вследствие чего, для каждого пользователя будет выделен свой уникальный канал (после прохождения авторизации, например).

Nginx Push Module — Secure

Nginx Push Module предоставляет нам некоторые директивы в конфиге nginx по настройке безопасности. Рассмотрим только те, которые применил я:

  • push_authorized_channels_only [ on | off ]
    on – позволить клиенту прослушивание конкретного канала только после его явного создания (отправка POST или PUT запроса в точку publisher). В противном случае при попытке прослушивания закрытого канала клиенту возвращается ответ 403.
    off – клиент может начать прослушивание закрытого канала.

  • push_max_channel_subscribers [ число ]
    Максимальное число одновременных слушателей канала.

Реализация

Итак, дадим имя нашему модулю – Channel. Разрабатывать его будем на Ruby (также будут иметь место небольшие вставки на Rails).
Для управления каналами (см. Basic HTTP Push Relay Protocol) нам необходим HTTP-клиент. Мне нравится Patron.
Массив открытых каналов будем хранить в массиве opened_channels. Id канала будем генерировать при помощи метода generate_channel_id.

Создание канала (метод open) осуществляется путём отправки PUT-запроса в publish-точку (у нас это просто /publish). При успешном создании нового канала (статус 200) сгенерированный id добавляем в массив opened_channels и возвращаем его.
Закрытие канала (метод close) осуществляется путём отправки DELETE-запроса в publish.
Проверка существования канала (метод exist?) осуществляется с помощью отправки GET-запроса в publish. Если сервер вернул 200 – канал открыт, иначе, удаляем канал из массива.
Отправка данных в канал (метод push) осуществляется посылкой POST-запроса в publish с указанием данных и content-type. Данные отправляем только в открытые каналы.

Все HTTP-запросы должны содержать параметр канала (у нас это channel). Естественно, publish-точку следует защитить.

Код модуля:

module Channel 
  @http_client = Patron::Session.new 
  @http_client.base_url = "http://localhost/publish" 

  @@opened_channels = [] 
  mattr_accessor :opened_channels 

  class << self 
    def open 
      id = generate_channel_id 
      resp = @http_client.put(build_request_for_channel(id), "") 
      if resp.status == 200 
        opened_channels << id 
        id 
      else 
        false 
      end 
    end 

    def close(id) 
      resp = @http_client.delete(build_request_for_channel(id)) 
      resp.status 
    end 

    def exist?(id) 
      resp = @http_client.get(build_request_for_channel id) 
      if resp.status == 200 
        true 
      else 
        opened_channels.delete id 
        false 
      end 
    end 

    def push(id, data, content_type) 
      if exist? id 
        puts "pushing to channel with id=#{id}..." 
        resp = @http_client.post(build_request_for_channel(id), data, {"Content-Type" => content_type}) 
        resp.status 
      end 
    end 

    def push_to_all_channels(data, content_type="application/json") 
      opened_channels.each { |c| push(c, data, content_type) } 
    end 

  private 
    def generate_channel_id 
      UUIDTools::UUID.timestamp_create.to_s 
    end 

    def build_request_for_channel(id) 
      "/?channel=#{id}" 
    end 
  end 
end

Открытие канала по запросу:

def subscribe 
  if channel_id = Channel::open 
    render text: channel_id 
  else 
    render nothing: true, status: 500 
  end 
end

Пример отправки данных:

user = current_user
channel_id = user.channel_id
msg = user.messages.last 
data = msg.to_json(only: [:created_at, :text]) 
status = Channel::push(channel_id, msg, "application/json")

Автор: yuryroot

* - обязательные к заполнению поля