Почему одного AJAX недостаточно: протокол WAMP

в 20:49, , рубрики: ajax, javascript, wamp, Веб-разработка, метки:

AJAX-вызовы вывели работу web на новый уровень. Уже не нужно перезагружать страницу в ответ на каждый ввод информации пользователем. Теперь возможно отправлять вызовы на сервер и обновлять страницу на основании полученных ответов. Это ускоряет работу интерактивного интерфейса.

А вот что AJAX не обеспечивает – так это обновления с сервера, которые необходимы для работы приложения в реальном времени. Это могут быть приложения, в которых пользователи одновременно редактируют один документ, или уведомления, рассылаемые миллионам читателей новостей. Необходим ещё один шаблон для рассылки сообщений, в дополнение к запросам AJAX, который бы работал в разных масштабах. Для этого традиционно используется шаблон PubSub («publish and subscribe», «публикация и подписка»).

Какую задачу решил AJAX

До появления AJAX интерактивные взаимодействия со страницей были тяжеловесными. Каждое из них требовало перезагрузки страницы, которая создавалась на сервере. В этой модели основной единицей взаимодействия была страница. Неважно, какой объём информации отправлялся из браузера на сервер – результатом была полностью обновлённая страница. Это была трата как трафика, так и серверных ресурсов. И это было медленно и неудобно для пользователей.

AJAX решил проблему, разбивая всё на части: стало возможным отправить данные, получить конкретный результат и обновить лишь часть страницы, имеющую к этому отношение. От вызова «дай мне новую страницу» мы перешли к конкретным запросам данных. У нас появилась возможность делать вызовы удалённых процедур (RPC).

Рассмотрим простой пример веб-голосования:

image

С использованием AJAX обработка щелчка по «Vote» сводится примерно к следующему:

var xhr = new XMLHttpRequest();
xhr.open('get', 'send-vote-data.php');

xhr.onreadystatechange = function() {
   if(xhr.readyState === 4) {
      if(xhr.status === 200) {

      // Обновить голосование на основании результата
      } else{
         alert('Error: '+xhr.status); // Ошибочка вышла
      }
   }
}

Затем нужно сменить только один счётчик голосования. От перерисовки страницы мы перешли к изменению одного элемента DOM.

У сервера меньше работы, и трафик уменьшился. А главное, интерфейс обновляется быстрее, улучшая привлекательность использования.

Чего не хватает

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

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

image

PubSub: обновления от одного ко многим

Устоявшимся шаблоном для таких задач служит PubSub. Клиент заявляет свой интерес в какой-то теме (подписывается) серверу. Когда клиент отправляет событие серверу (публикует), сервер распространяет его по всем подсоединённым клиентам.

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

Реализаций шаблона множество. На Node.js или Ruby можно использовать Faye. Если вам не хочется держать свой сервер, можно использовать веб-сервисы типа Pusher.

Два шаблона отправки сообщений, две технологии?

Довольно просто отыскать технологию PubSub, подходящую для нужд определённого приложения. Но даже в таких простых приложениях, как голосование, необходимо реализовывать и RPC и PubSub – и отправку данных, и запросы, и получение обновлений. Используя чистый PubSub, вам придётся использовать две разных технологии: AJAX and

У такого подхода есть минусы:
— организация двух разных стеков, возможно, двух серверов
— раздельные соединения приложения для двух шаблонов, большая нагрузка на сервер
— на сервере нужно интегрировать два стека в одном приложении и координировать их друг с другом
— то же на фронтенде

WAMP: RPC и PubSub

Web Application Messaging Protocol (WAMP) решает эти проблемы, интегрируя RPC и PubSub в один протокол. Одна библиотека, одно соединение и один API.

Протокол открыт, и для него есть открытая реализация на JavaScript (Autobahn|JS), работающая и в браузере и под Node.js. Для других языков также существуют реализации, так что можно использовать PHP, Java, Python или Erlang на сервере.

image

Библиотеки WAMP можно использовать не только на бэкенде, но и для нативных клиентов, позволяя сочетать web и клиентов, работающих на одном протоколе. Библиотека на C++ хорошо приспособлена для запуска WAMP-компонент на устройствах с ограниченными ресурсами.

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

Посмотрим, как решить нашу задачу с голосовалкой при помощи WAMP.

Живое обновление голосовалки: WebSockets и WAMP

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

Код для демки лежит на GitHub, вместе с инструкциями по запуску. В качестве роутера используется Crossbar.io.

Подключение библиотеки WAMP

Для начала подключим библиотеку Autobahn|JS.

В целях демонстрации её можно подключить так:

<script src="https://autobahn.s3.amazonaws.com/autobahnjs/latest/autobahn.min.jgz"></script>;
Устанавливаем соединение

var connection = new autobahn.Connection({
   url: "ws://example.com/wamprouter",
   realm: "votesapp"
});

Первый аргумент – URL роутера. Использована схема ws, поскольку WAMP использует WebSockets в качестве транспорта по умолчанию. Кроме того, при общении не передаются HTTP-заголовки, что уменьшает трафик. WebSockets поддерживаются во всех современных браузерах.

Вторым аргументом мы устанавливаем «realm», пространство, к которому присоединяется соединение. Пространства создают отдельные домены для роутинга на сервере – то есть сообщения передаются только внутри одного пространства.

Созданный объект позволяет прикрепить два обратных вызова – один для успешного соединения, а второй для неуспешного, и для момента, когда соединение прервётся.

Хэндлер onopen вызывается по установлению соединения, и получает объект session. Мы передаём это в функцию main, в которой содержится функциональность приложения.

connection.onopen = function (session, details) {
	main(session);
};

Далее необходимо запустить открытие соединения:

connection.open();
Регистрируем и вызываем процедуру

Фронтенд отправляет голоса, вызывая процедуру на бэкенде. Определим функцию обработки переданного голоса:

var submitVote = function(args) {
   var flavor = args[0];
   votes[flavor] += 1;

   return votes[flavor];
};

Она увеличивает количество голосов и возвращает это число.

Затем мы регистрируем её на роутере WAMP:

session.register('com.example.votedemo.vote', submitVote)

При этом мы назначаем ей уникальный идентификатор, использующийся для вызова. Для этого WAMP использует URI в виде пакетов Java.

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

session.call('com.example.votedemo.vote',[flavor]).then(onVoteSubmitted)

То, что возвращает submitVote, передаётся в хэндлер onVoteSubmitted.

Autobahn|JS делает это через обычные обратные вызовы, но с обещаниями: session.call сразу возвращает объект, который оформляется в момент возврата вызова оформляется, а затем выполняется хэндлер- функция.

Для простых случаев использования WAMP и Autobahn|JS вам не надо ничего знать про обещания. Можете считать их другой записью обратных вызовов.

Подписка и отправка обновлений

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

session.subscribe('com.example.votedemo.on_vote', updateVotes);

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

Осталось лишь настроить отправку обновлений с сервера. Создаём объект для отправки и публикации информации по нужной нам теме. Эту функциональность мы добавим в ранее зарегистрированную submitVote:

var submitVote = function(args, kwargs, details) {
   var flavor = args[0];
   votes[flavor] += 1;

   var res = {
      subject: flavor,
      votes: votes[flavor]
   };

   session.publish('com.example.votedemo.on_vote', [res]);

   return votes[flavor];
};

На этом всё: отправка голосов на бэкенд и обновления голосов для всех подсоединённых браузеров работают на одном протоколе.

Итог

WAMP унифицирует передачу сообщений. RPC и PubSub должно хватить для всех задач приложения. Работает протокол через WebSockets, быстрое, одиночное и двунаправленное соединение с сервером. Поскольку протокол WAMP открыт, и уже существуют его реализации для разных языков, вы вольны выбирать технологию для использования на бэкенде и даже писать приложения для нативных клиентов, а не только для web.

Примечания

Vote”, Crossbar.io – действующая версия голосовалки

Why WAMP?”, WAMP – пояснения по разработке протокола

Free Your Code: Backends in the Browser,” Alexander Gödde, Tavendo – статья на тему того, как симметрия протокола влияет на деплой

WebSockets: Why, What and Can I Use It?”, Alexander Gödde, Tavendo – обзор WebSockets

WAMP Compared”, WAMP – сравнение протокола с другими

Crossbar.io – введение в использования универсального роутера для приложений

Автор: SLY_G

Источник

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


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js