Приветствую всех! Многие разработчики вкурсе про замечательный модуль 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