AJAX-вызовы вывели работу web на новый уровень. Уже не нужно перезагружать страницу в ответ на каждый ввод информации пользователем. Теперь возможно отправлять вызовы на сервер и обновлять страницу на основании полученных ответов. Это ускоряет работу интерактивного интерфейса.
А вот что AJAX не обеспечивает – так это обновления с сервера, которые необходимы для работы приложения в реальном времени. Это могут быть приложения, в которых пользователи одновременно редактируют один документ, или уведомления, рассылаемые миллионам читателей новостей. Необходим ещё один шаблон для рассылки сообщений, в дополнение к запросам AJAX, который бы работал в разных масштабах. Для этого традиционно используется шаблон PubSub («publish and subscribe», «публикация и подписка»).
Какую задачу решил AJAX
До появления AJAX интерактивные взаимодействия со страницей были тяжеловесными. Каждое из них требовало перезагрузки страницы, которая создавалась на сервере. В этой модели основной единицей взаимодействия была страница. Неважно, какой объём информации отправлялся из браузера на сервер – результатом была полностью обновлённая страница. Это была трата как трафика, так и серверных ресурсов. И это было медленно и неудобно для пользователей.
AJAX решил проблему, разбивая всё на части: стало возможным отправить данные, получить конкретный результат и обновить лишь часть страницы, имеющую к этому отношение. От вызова «дай мне новую страницу» мы перешли к конкретным запросам данных. У нас появилась возможность делать вызовы удалённых процедур (RPC).
Рассмотрим простой пример веб-голосования:
С использованием 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 обновляет страницы только в ответ на действия пользователя. Он не решает задачи обработки апдейтов, идущих с сервера. Он не предлагает способа делать то, что нам нужно: передавать информацию с сервера в браузер. Для этого необходим шаблон передачи сообщений, который отправляет обновления на клиент без участия пользователя и без необходимости для клиента постоянно опрашивать сервер.
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 на сервере.
Библиотеки 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