Перейти к содержимому

Pull изнутри - канал, подписка, доставка до браузера

Разбираем доставку по звеньям: сайт кладёт команду в очередь, отдельная служба держит соединение, браузер слушает свой канал. Дальше смотрим, где эта цепочка обрывается и почему сообщение пропадает без единой ошибки в журнале.

Механика

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

Адресат на сервере очередей называется каналом, и у каждого канала свой идентификатор. Это длинная случайная строка: сайт её выдаёт, а браузер предъявляет серверу очередей при подключении. Знание строки и есть право читать канал, поэтому она выглядит набором случайных символов.

Каналов у открытой страницы обычно два: личный канал пользователя и общий канал сайта. В адресе подписки они идут одной строкой через двоеточие, и это хорошо видно в журнале веб-сервера. Личный канал принадлежит одному пользователю, а общий раздаёт одно и то же всем подключённым страницам сразу.

Свой канал браузер узнаёт обычным запросом к сайту, а не угадывает его сам. Клиентская библиотека запрашивает конфигурацию и получает адреса сервера очередей вместе с идентификаторами каналов. Если этот запрос закрыт правами или отвечает ошибкой, соединение не поднимется вовсе.

Смена пользователя означает смену личного канала целиком, а не переподключение к прежнему. Библиотека перезапрашивает конфигурацию и подключается заново, а прежний канал остаётся на сервере очередей до истечения своего срока. Команды, отправленные предыдущему пользователю, в новый канал не переезжают никогда.

Подписка на чужую сущность устроена совсем иначе, чем выданный браузеру личный канал. Пользователь подписывается на тег - строку вида «заказ номер такой-то», а отправка уходит всем подписанным на этот тег. Личный канал знает только своего владельца, поэтому «все, кто смотрит заказ» собираются именно тегом.

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

Сайт подписывает пакет секретным ключом, а сервер очередей проверяет эту подпись перед раздачей. Расхождение ключей выглядит как полная тишина: соединение живо, публикация проходит, до браузера не доходит ровным счётом ничего.

Без настроенного сервера очередей модуль не ломается, а переходит в запасной режим опроса. Клиент обращается к сайту раз в минуту, при появлении данных интервал сокращается до десяти секунд, а затем на пустых ответах снова растёт. Работает такой режим честно, но мгновенным при интервале в минуту уже не выглядит.

Команда - это тройка: идентификатор модуля-отправителя, имя команды и объект произвольных параметров. Ровно эта тройка приходит на клиент, и обработчик получает параметры, служебные данные о доставке и имя команды. Канал живёт ограниченное время и продлевается сам, пока страница открыта и соединение живо.

Шаги

  1. Проверить, что клиентская библиотека подключена и объект подписки на странице действительно появился.
  2. Прочитать конфигурацию клиента: адрес сервера очередей, режим соединения и выданные странице идентификаторы каналов.
  3. Убедиться, что соединение установлено, а не свалилось в запасной режим опроса или в обрыв.
  4. Отправить команду самому себе и посмотреть, доходит ли она до потокового обработчика страницы.
  5. Сверить имена модуля и команды на сервере и на клиенте, а затем разобрать канал получателя.

Код

Подключаем клиентскую библиотеку в публичной части:

// шаблон сайта или свой компонент, до вывода разметки страницы
\Bitrix\Main\UI\Extension::load('pull.client'); // в мобильном веб - своё расширение
// без этой строки объект BX.PULL на странице не появится вовсе

Библиотека подключается расширением, а не ручным подключением файла из ядра. Без неё канал не запрашивается и соединение не поднимается.

Смотрим, какой канал выдан странице:

BX.PULL.getConfig(); // адреса сервера очередей и идентификаторы каналов
BX.PULL.getStatus(); // online означает установленное соединение
BX.PULL.getDebugInfo(); // подробное состояние подключения
BX.PULL.capturePullEvent(); // печатать в консоль все приходящие команды
// после смены пользователя конфигурацию запрашивают заново, канал уже другой

Конфигурация отвечает на вопрос, куда именно подключилась страница и какие каналы она слушает. Состояние соединения показывает, работает ли постоянный канал или запасной режим опроса.

Читаем запрос подписки в журнале веб-сервера:

GET /bitrix/sub/?CHANNEL_ID=6fb2a96a%3Ae37a3e5d&tag=&time= 200
# два идентификатора через двоеточие: личный канал и общий
POST /bitrix/pub/?CHANNEL_ID=6fb2a96a 200
# публикацию делает сам сайт, а не браузер посетителя

Запрос подписки висит долго и завершается только с приходом сообщения или по истечении срока ожидания.

Сверяем адреса и ключ на стороне сайта:

// /bitrix/.settings.php, секция pull
'pull' => ['value' => [
'path_to_listener' => '', // адрес подписки: сюда браузер идёт за сообщениями
'path_to_publish' => '', // адрес публикации: сюда сайт кладёт команду
'path_to_websocket' => '', // тот же канал, но постоянным соединением
'signature_key' => '', // обязан совпадать с ключом сервера очередей
]],

Адрес подписки и адрес публикации - разные концы одной трубы, и путать их нельзя. По первому ходит браузер, по второму публикует сам сайт.

Кладём команду в личный канал пользователя:

\Bitrix\Main\Loader::includeModule('pull');
\Bitrix\Pull\Event::add([$userId], [
'module_id' => 'vendor.shop', // имя модуля-отправителя
'command' => 'orderStatus', // имя команды
'params' => ['orderId' => 1234, 'status' => 'Отгружен'],
]);
// адресаты идут списком: команда ляжет в личный канал каждого из них
\Bitrix\Pull\Event::send(); // отправка накопленного за хит

Добавление события только копит команды, а на сервер очередей их отправляет отдельный вызов. Без него команда тихо остаётся в буфере хита и до службы очередей не доходит.

Собираем получателей тегом, а не списком:

// тег - произвольная строка, общая для всех смотрящих на одну сущность
CPullWatch::Add($userId, 'ORDER_' . $orderId); // подписка на сущность
CPullWatch::AddToStack('ORDER_' . $orderId, [
'module_id' => 'vendor.shop',
'command' => 'orderUpdated',
'params' => ['orderId' => $orderId],
]);
// подписка не вечна: пока страница открыта, клиент продлевает её сам

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

Принимаем одну именованную команду:

const off = BX.PULL.subscribe({
type: BX.PullClient.SubscriptionType.Server, // значение по умолчанию
moduleId: 'vendor.shop',
command: 'orderStatus',
callback: (params, extra, command) => { // параметры, служебные данные, имя
updateOrderRow(params.orderId, params.status);
},
});
off(); // возвращённая функция снимает подписку

Обработчик получает три аргумента: параметры команды, служебные данные о доставке и её собственное имя. Среди служебных данных приходит время с момента отправки команды сервером очередей.

Слушаем все команды своего модуля:

BX.PULL.subscribe({
moduleId: 'vendor.shop', // команду не указываем: ловим все подряд
// чужие модули в обработчик не попадают, фильтр по модулю остаётся
callback: (data) => console.log(data.command, data.params),
});
// в консоли видно реальные имена команд, приходящих в канал этой страницы

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

Выдаём гостю собственный канал:

// local/scripts/pull_hit.php, обработчик на прологе каждого хита
$guestId = -1; // стабильное отрицательное число на гостя
define('PULL_USER_ID', $guestId); // из этой константы берётся канал гостя

У неавторизованного посетителя личного канала нет, пока номер не выдан ему вручную явным образом.

Вешаем этот файл на пролог хита:

RegisterModuleDependences(
'main', 'OnProlog', 'main', '', '', 2,
'local/scripts/pull_hit.php' // файл, где вычисляется номер гостя
);
// константа задаётся до вывода страницы, позже канал уже не сменить

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

Ограничения

Канал не хранилище: сообщение живёт мгновение и никого не догоняет. Отправленное закрытой вкладке пропадает бесследно, и повторной попытки доставки платформа не предпринимает.

Срок жизни канала на сервере очередей ограничен и задаётся настройками самой службы. Долгая пауза - спящий ноутбук или закрытая крышка - переживает этот срок, и после пробуждения страница подключается уже новым каналом.

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

Гостевой режим доступен не всякой установке: он появился в версии 15.5.1 и не работает в редакциях «Старт» и «Стандарт». На младших редакциях команды гостям не доходят вовсе, и обойти это настройкой модуля нельзя.

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

Типичные проблемы

Сообщение уходит, а видно его только после обновления страницы.

Настроена только публикующая половина сервера очередей, а подписная осталась без конфигурации. Страница ничего не слушает, поэтому данные появляются с очередным обычным запросом.

В журнале веб-сервера повторяется отказ соединения с портом очередей.

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

Сервер очередей отвечает, что идентификатор канала слишком длинный.

Режим модуля в настройках сайта не совпадает с версией установленной рядом службы. Каналы старого и нового сервера очередей отличаются длиной идентификатора канала.

После перевода сайта на HTTPS соединение перестало устанавливаться.

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

Гости получают чужие сообщения или не получают ничего.

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

После долгого простоя вкладки сообщения перестают приходить.

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

Частые вопросы

Чем канал отличается от подписки на тег?

Личный канал привязан к одному пользователю: он выдаётся его браузеру и знает только своего владельца. Тег собирает тех, кто смотрит на одну сущность прямо сейчас: подписался на заказ, получил обновление по этому заказу. Личный канал удобен для персональных уведомлений, тег - когда получатели заранее неизвестны.

Как узнать идентификатор своего канала?

Служебной командой клиентской библиотеки в консоли браузера: она печатает адреса сервера очередей и выданные странице идентификаторы каналов. Там же видно состояние соединения и режим, в котором работает модуль. Те же идентификаторы попадают в журнал веб-сервера как параметр запроса подписки.

Почему сообщение не дошло тому, кто открыл страницу позже?

Потому что это не очередь с хранением: команда раздаётся только тем соединениям, которые открыты в момент отправки. Опоздавшего она не догоняет и нигде не ждёт. Поэтому страница при открытии запрашивает текущее состояние обычным запросом, а сообщениями ловит только последующие изменения.

Что происходит с каналом при смене пользователя?

Личный канал перевыпускается: библиотека перезапрашивает конфигурацию и подключается уже к новому каналу. Прежний канал остаётся на сервере очередей до истечения срока, но новому пользователю не достаётся. Команды, отправленные до входа, в новый канал не переезжают.

Прекратит ли модуль работу, если браузеры откажутся от Server Push?

Нет, это разные технологии с похожими названиями. Модуль доставляет команды веб-сокетом или долгим запросом к серверу очередей, а Server Push в HTTP/2 занимался предзагрузкой ресурсов страницы. Отказ браузеров от него на доставку сообщений никак не влияет.

Смежное

Первоисточник