• Как аутентифицировать пользователя приложения при подключении к RabbitMQ по протоколу WEB-STOMP?

    denisbondar
    @denisbondar Автор вопроса
    Алексей Кузнецов, да. можно создать только fanout и отправлять в ту, которая закреплена за конкретным пользователем.

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

    Пример отправки тривиальный — создаешь сообщение и отправляешь в exchange. Всё просто.
    Под "сообщением" здесь понимается не просто текстовое сообщение, а какой-то объект (json) произвольной структуры, который можно обработать на фронтенде. Ну, напрмер, чтобы отделить всплывающие уведомления от других типов информации, передаваемых во фронт.

    Упрощенный пример кода отправки сообщения если есть только fanout-exchange с именем вроде fanout-exchange-name-12345

    $message = new AMQPMessage(json_encode($messageBody), ['content_type' => 'application/json']);
    $this->amqpChannel->basic_publish($message, 'fanout-exchange-name-' . $userId);
  • Как аутентифицировать пользователя приложения при подключении к RabbitMQ по протоколу WEB-STOMP?

    denisbondar
    @denisbondar Автор вопроса
    Алексей Кузнецов, выше пример фронта, в котором уже есть имя очереди. Чтобы получить имя очереди, а также получить имя пользователя и пароль для подключения к RabbitMQ, нужно выделить на бэкенде эндпоинт с аутентификацией, который создает очередь и возвращает имя очереди, имя пользователя и пароль в json во фронт. Помимо этого, этот эндпоинт, если это необходимо, должен создать все необходимые exchange (но это зависит от конкретной архитектуры).

    Простой пример сервиса, который все это длеает (и вызывается из АПИ-контроллера):
    public function createAutodeleteQueueForUserId(string $userId): string
    {
        $this->createDirectExchanges();
    
        $queueName = sprintf(
            'webstomp-%s-%s',
            $userId,
            $this->queueSuffixGenerator->generate()
        );
    
        $this->amqpChannel->queue_declare(
            $queueName,
            false,
            false,
            false,
            true,
            false,
            new AMQPTable(
                [
                    'x-expires' => 60000,
                    'x-message-ttl' => 20000,
                    'x-max-priority' => 10,
                ]
            )
        );
    
        $fanoutExchange = $this->createUserFanoutExchange($userId);
        $this->amqpChannel->queue_bind($queueName, $fanoutExchange);
    
        return $queueName;
    }


    Метод возвращает имя созданной очереди. Например, для пользователя с ID 12345 будет создана очередь с именем вроде webstomp-12345-SGVsbG8sIFdvcmxkISBIZWxsbywgSGFiciE. И это имя очереди нужно передать во фронтенд, чтобы фронтенд приложение подключилось к ней в течение x-expires мс.

    Архитектура моего push модуля такая:
    • Один Direct exchange для широковещательных сообщений (для всех пользоватлей)
    • Один Direct exchange для персональных сообщений конкретного пользователя
    • Множество Fanout exchange, создаваемых для каждого пользователя (выше в коде видно его создание в конце метода) — после создания этот exchange безусловно биндится с широковещательным direct-exchange, а также биндится с персональным direct-exchange, но при помощи ключа маршрутизации. На каждого пользователя создается один такой fanout exchange
    • Множество очередей, которые создаются приведенным выше кодом и биндятся на fanout-exchange этого пользователя. Для каждого запроса создается своя очередь. Это могут быть как разные вкладки браузера, так и другие каналы получения уведомлений (например, мобильные приложения)


    В итоге в системе получается по одному direct-exchange каждого типа, множество fanout-exchange по одному для каждого пользователя и множество очередей по одной на каждую вкладку браузера.

    После того, как очередь создана, есть 60 секунд чтобы фронтенд приложение подключилась к ней — иначе она будет автоудалена. Если фронтенд приложение по какой-то причине отключается от очереди — она также автоматически удаляется. Если фронтенд приложение не ответело на служебный ping (webstomp), то очередь также будет автоудалена.

    При такой архитектуре новая копия приложения (открыта в новой вкладке браузера) будет получать только новые уведомления параллельно (fanout-exchange распараллеливает все сообщения во все подключенные к нему очереди). Если нужно другое поведение (например, чтобы новая копия клиентского приложения получала вообще все уведомления за все время), то придется изменить архитектуру.
  • Как подружить Windows 10 + Docker + PhpStorm + Xdebug?

    Михаил Алексеев, видимо путь в значении Path to create validaton script не совсем правильный - ведет не туда, куда настроен root web-сервера. Сервер отвечает 404 - значит соединение установилось нормально, но запрашиваемого документа нет.
  • Как настроить Xdebug в docker?

    Спасибо! Проверил, всё работает отлично. Статью дополнил, репозиторий обновил.
  • Как подружить Windows 10 + Docker + PhpStorm + Xdebug?

    В Windows фиксированная подсеть как раз таки и не нужна. Там достаточно прописать
    XDEBUG_CONFIG: "remote_host=host.docker.internal remote_enable=1"

    Но у меня нет Windows, чтобы проверить это. Во всяком случае, должно работать.
  • Как аутентифицировать пользователя приложения при подключении к RabbitMQ по протоколу WEB-STOMP?

    denisbondar
    @denisbondar Автор вопроса
    serega_chem, Клиент (WEB-STOMP) сначала по АПИ запрашивает имя временной очереди (и настройки этой очереди). АПИ метод генерирует это имя случайным образом, например `push-subscription-13-sEQEccPHmM479GBw9Mg3PA` и выдает его в ответ клиенту. Здесь 13 - это ИД пользователя - проще обслуживать брокер, видя ИД пользователя. При этом на стороне сервера создается эта временная очередь, которая затем подключается к exchange этого конкретного пользователя. То есть у пользователя один exchange, куда попадают сообщения для него, но при этом может быть запущено несколько клиентов (несколько вкладок браузера, например) и каждому клиенту будет создана своя отдельная временная очередь.

    5c276c0a5f309469230071.png

    Временная очередь — автоудаляемая (установлен флаг autodelete), а также с установленным аргументом `x-expires` (у меня он равен 60 секундам). Это значит, что если клиент не подключится к очереди в течение этого времени - она автоматически удалится, а также если клиент отключится от очереди - она тоже автоматически удалится. Это решает проблему накопления зомби-очередей. Тут еще можно сказать о том, что протокол STOMP контролирует наличие клиента через heartbeat и, если клиент не ответил, то очередь также будет удалена.

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

    При обновлении страницы браузера происходит и перезапуск web-stomp клиента, что автоматически приводит к отключению клиента от временной очереди (она тут же удаляется) и затем созданию запроса на новую очередь и подключению к ней. То есть не нужно хранить имя очереди между перезапусками клиента (между обновлениями страницы) - в этом нет смысла. К тому же это упрощает схему и делает ее надежней.

    В реббите создаем пользователя stomp с паролем stomp и разрешаем ему доступ только к очередям:
    rabbitmqctl add_user stomp stomp
    rabbitmqctl set_permissions -p / stomp "^push-subscription-.*" "" "^push-subscription-.*"

    Вот такая реализация клиента (упрощенно, конечно же):
    // Тут сначала выполняются два АПИ запроса - на получение имени очереди и на получение настроек
    // Имя временной очереди получаем по API
    var queue_name = "push-subscription-2-ZWM3YzY1YjgyOTIwZGQzZDRlYzlkYzU1NmVkOTQ3MTI";
    
    // Эти настройки тоже получаем по API от сервера
    var broker_username = "stomp";
    var broker_password = "stomp";
    // Очередь с этими  параметрами изначально создается на сервере, но чтобы подключиться к ней клиентом, параметры этой очереди должны совпадать с теми, что на сервере.
    var queue_expires = 60000;  // Это время жизни очереди, отведенное на подключение клиента
    var queue_message_ttl = 20000;  // Это ТТЛ собощений в очереди по умолчанию. Через 20 секунд сообщения, которые не доставлены клиенту - будут удалены, так как теряется их актуальность. Для сообщений с другой актуальностью можно установить конкретный ТТЛ
    var queue_max_priority = 10;  // Приоритет по умолчанию. Для сообщений с другим приоритетом его можно выставить индивидуально
    
    // Далее код клиента
    var client = Stomp.client('ws://' + window.location.hostname + ':15674/ws');
    client.heartbeat.outgoing = 30000;  // 30 секунд - исходящий heartbeat
    client.heartbeat.incoming = 0;  // входящий отключаем (мы так решили для себя)
    client.reconnect_delay = 5000;  // ожидание соединения
    //client.debug = null;  // для отладки раскомментировать или установить в качестве значения какой-то ИД блока, куда будет выводиться отладка. По умолчанию - в консоль.
    
    var on_connect = function () {
        client.subscribe(queue_name, function (d) {
            $("#websocket").append("Получено из очереди: " + d.body + "<br />")
        }, {
            durable: false,
            exclusive: false,
            'x-expires': queue_expires,
            'x-message-ttl': queue_message_ttl,
            'x-max-priority': queue_max_priority
        });
        console.log('WEB STOMP Connected');
    };
    
    var on_error = function () {
        console.log('WEB STOMP error');
    };
    
    client.connect(broker_username, broker_password, on_connect, on_error, '/');
  • Как аутентифицировать пользователя приложения при подключении к RabbitMQ по протоколу WEB-STOMP?

    denisbondar
    @denisbondar Автор вопроса
    1. Я наткнулся сразу же на проблему автоуничтожения очередей. Оказалось, что все работает как нужно, если бекенд создает очередь типа autodelete, а фронтенд подключается к ней с параметрами: autodelete: true, durable: false, exclusive: false. В такой конфигурации очередь удаляется после отключения клиента или после истечения таймаута (stomp об этом заботится, как я понимаю). Как раз понял разницу между autodelete и exclusive. Нагрузка у нас будет не такой большой, чтобы отказаться от autodelete. Единственное, что я пока не решил, это то, каким образом подчищать очереди без консумеров. Теоретически их не должно быть, но на практике вполне могут появиться, например, когда клиент открыл приложение, запрос на создание очереди для его копии ушел в бекенд, но изза плохого соединения клиент отключился. В итоге очередь была создана, но подписываться на нее уже некому.
    2. Это само собой. Мы будем создавать по одной очереди на каждую копию клиента (вкладки браузера). Пока что идея только в голове, а не в реализации, так что, возможно, еще наткнемся на подводные камни. Но в целом она кажется нормальной. Вариант, когда очередь создается клиентом, а обменник сервером - стандартный из примеров web-stomp-examples. Именно с ним я и не разобрался в плане прав доступа и задал этот вопрос на тостере. Вариант, когда контроль за созданием очередей лежит на бекенде мне понравился больше.
    3. Спасибо. Обязательно воспользуемся nginx для проксирования вебсокетов.

    За Вашу статью отдельное огромное спасибо. Именно она помогла найти правильный подход. Я остановился на варианте с отдельным обменником fanout для каждого юзера + двумя обменниками direct, с которыми работает клиентский код. (Рис. 8)

    Я с удовольствием пообщался бы с Вами на тему RabbitMQ. Мне интересно всё, что касается этого брокера. Мы долго выбирали и решили остановиться именно на нем. Ну и вебсокеты тоже решили реализовать на нем, чтобы не придумывать еще что-то. Так что есть огромное желание изучить его работу досконально.

    Спасибо!
  • Нужна ли переустановка Windows в такой ситуации?

    Tomaszz, вариантов может быть множество. Если вы только что установили Windows и неудачно "активировали", то самый быстрый путь - переустановить. Это займет 20 минут. Тем более, что теперь у вас есть лицензия.
    Поиски вирусов и их вычленение из системы может занять куда больше времени.
  • Нужна ли переустановка Windows в такой ситуации?

    Пока ждете ответов - уже давно переустановили бы с чистого листа :)
  • Побитовые операции PHP. Что такое исключающее или?

    Числовые значения всегда представлены в двоичном виде. Вы можете отображать их значения для себя в любом удобном вам виде. По умолчанию значения отображаются в десятичном.
  • Как провайдеры интернета ограничиваю скорость доступа?

    АртемЪ,
    Заметьте речь идет про ограничение доступа к сети провайдера, а не доступа в интернет.

    Есть мнение, что автор имел ввиду именно интернет, так как изначально сообщил о своей некомпетентности в вопросе.
  • Почему в textarea окончания строк \n, а в POST уже \r\n?

    denisbondar
    @denisbondar Автор вопроса
    Дайте пожалуйста ссылочку. Мне важен первоисточник. Нужно обосновать дефект ПО.
  • Почему в textarea окончания строк \n, а в POST уже \r\n?

    denisbondar
    @denisbondar Автор вопроса
    Stalker_RED, попробуйте отправить форму на сервер. Поймете, о чем я говорю.
    Приведенный мной пример содержит JS для демонстрации. Я не придумал, как иначе показать проблему.
    Но проблема в следующем: пока текст находится внутри textarea, то окончания строк состоят из одного символа, но если засабмитить форму, то уже в post-запросе видно, что окончания строк состоят из двух символов. Это можно проверить без JS вообще.
  • Почему в textarea окончания строк \n, а в POST уже \r\n?

    denisbondar
    @denisbondar Автор вопроса
    Это код, демонстрирующий проблему.
    Внутри POST также приезжает \r\n на сервер.
  • Как разбить сложное представление на части?

    denisbondar
    @denisbondar Автор вопроса
    Если у кого-то возникнет такой же вопрос, как у меня, то решение здесь: https://github.com/denisbondar/yii2-hmvc-test