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 Godde, Tavendo – статья на тему того, как симметрия протокола влияет на деплой

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

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

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

Комментарии (6)


  1. spmbt
    09.04.2015 04:33
    +15

    Автор Alexander Godde зачем-то показательно слеп и с первых строк проводит серию ложных утверждений, годных лишь для экскурсантов в музее, но не для ресурса со специалистами, знающих, что протокол — это не основание для ограничений на передачу чего-либо. Зачем? Так, что ли, проще объяснить появление нового протокола?

    AJAX-вызовы вывели работу web на новый уровень. Уже не нужно перезагружать страницу в ответ на каждый ввод информации пользователем.
    Где же упоминание про фреймы, при наличии которых тоже не нужно перезагружать страницу, а только фрейм, который может быть нулевых размеров и невидимым? Старые протоколы безопасности затем не мешали прочитать содержимое обновлённого фрейма из соседнего фрейма при монодоменных (важное ограничение) запросах.

    А вот что AJAX не обеспечивает – так это обновления с сервера...
    Но правда в том, что обновление ПО как по eval(), так и несколькими другими способами, возможно как и по AJAX, так и через фреймы.

    До появления AJAX интерактивные взаимодействия со страницей были тяжеловесными. Каждое из них требовало перезагрузки страницы, которая создавалась на сервере.
    Тяжеловесность заключалась лишь в лишнем нуль-фрейме, что отнимало не лишние ресурсы старых браузеров (например, в Netscape 4 рекомендовалось больше 100 фреймов не делать. Но обновляемая в нём страница могла быть просто скриптом. Более того, всегда работал способ подгрузки скриптов ( и стилей, и картинок) путём создания тега Script с src=URL_скрипта_с_любого_домена. Это позволяло подгружать и обновлять ПО без перезагрузки какой-либо страницы.Правда, совсем чисто и безошибочно браузеры это научились делать во времена примерно Firefox 1.0 или немногим ранее. А до него — следовало, всё же, обновлять страницу, например, с помощью document.write() (с обязательным document.close() в каком-то из браузеров).

    У нас появилась возможность делать вызовы удалённых процедур (RPC).
    Это можно, как оговорено выше, было делать и до AJAX, и не одним способом (а тремя). Не в протоколе, конечно, RPC, но по сути RPC (Remote Procedure Call).

    Итого, кому нужно было такое вступление? Если это — статья для студентов, то тут автор показал себя дремуче безграмотным. И как тогда доверять тем знаниям, которые он настрочил ниже?

    Пусть, если автор такой, что счёл нужным не объяснять детям тонкости истории (очень самонадеянно, всё равно, что говорить, что камень не годился как орудие, а вот когда изобрели топор, весь мир перевернулся), то почему переводчик не счёл нужным это заметить? И сказать: вот тут автор много новой крутой новизны излагает, давайте простим ему этот лепет вначале? Зато потом он много хорошего про WAMP пишет таким же сладким складным языком, и я, мол, ручаюсь за него, что это уж точно правда.


    1. spmbt
      09.04.2015 05:06
      +9

      О, да, прочитав чуть далее, я смутно начал догадываться, что и про Вебсокет он накосячит, не здесь, так дальше. Он столь же феерично и про Websocket пишет, не в этой статье, а по ссылке tavendo.com/blog/post/websocket-why-what-can-i-use-it/. Там он наглядно и красиво утверждает, что длительная передача от сервера к клиенту была невозможна (под рисунком), пока не придумали WebSocket. Не читайте его… Лучше, тут, к примеру, хотя бы грамотно вводят в курс про long polling: www.slideshare.net/ffdead/the-html5-websocket-api и ещё через Флеш работал заменитель вебсокетов, когда их не было. Кроме того, первый запрос на вебсокет, всё же, должен дать клиент. В презентации это всё прекрасно описано, в статье данного автора — нет («No more polling» в тексте и около).


      1. RubaXa
        09.04.2015 10:43

        Ммм, RTMP + LocalConnection, помню, помню, веселые были вермена ;]


  1. ksdaemon
    09.04.2015 11:38
    +1

    Ну коли пошла такая пьянка, скажу про 3 моих реализации WAMP'а:

    • Wapmy.js — JS клиент для браузера
    • Wiola — Роутер на Lua на базе Nginx
    • Loowy — клиент на LUA

    А еще есть слайды с моего выступления на MoscowJS: «Пара слов про WAMP».
    Вдруг кому-то пригодится.


  1. netslavehq
    09.04.2015 15:53

    Кому будет интересна интеграция WAMP с PHP, обратите внимание на
    Ratchet: WebSockets for PHP.
    Oн использует REACT PHP для IOLoop`а и WAMP в качестве протокола.


  1. StreetStrider
    10.04.2015 22:08

    Решаю похожую проблему в Booth. Это Node.js/CommonJS библиотека, поверх вебсокета предоставляет два интерфейса: PubSub (у меня это названо realtime) и request-response (названо requests). Реализация двусторонняя: как сервер, так и клиент могут выступать одновременно и в качестве источника и приёмника потоков данных, также обе стороны могут обращаться друг к дружке с запросами.

    Запросы имеют promise API, а реалтайм — callback. Есть планы по реализации FRP для реалтайма на основе какой-нибудь существующей библиотеки, например, симпатичной мне либы Highland.