Асинхронщина на web страницах
Представьте: Вам надо познакомить новичка в вебе с AJAX технологиями.
Надо дать обзор, показать? Как этим пользуются сейчас, типа «CookBook», и показать
основные паттерны.
В этой статье я собираюсь выложить последовательный экскурс в AJAX like техники.
Туда — Сюда
«Откуда проявляется инициатива соединения? C браузера или сервера&» — не злободневный сегодня вопрос. Раньше она всегда исходила с браузера, но сегодня развивается и обратаная сторона медали.
Для проявления инициативы с браузера используют AJAX.
Для проявления инициативы с сервера — некие COMET PUSH-технологии, которые лучше рассматривать в зависимости от ситуации.
Здесь мы кратко, но понятно рассмотрим и первые, и вторые.
Что такое AJAX?
Очень много букв тут (https://ru.wikipedia.org/wiki/AJAX). Просто надо прочитать, повторять тут не буду стандартные слова.
Как пользоваться этим «нативно»
На низком уровне реализован AJAX с помощью XMLHTTPRequest.
Если он реализован иначе, то «обертки драйвера» предоставят схожий API.
XMLHTTPRequest — основа AJAX. Отличается все только тем, как его использовать, как получать.
Непосредственно его самого можно использовать, что называется «руками» или «нативно».
Про это написано хорошо тут http://javascript.ru/ajax/transport/xmlhttprequest , коротко суть в следующем:
имеются методы
open(Method, Url, async) send(data) onreadystatechange
вызываются в таком порядке
req.open("GET", url, true) req.onreadystatechange = handler req.send(null)
можно поработать с заголовками запроса и ответа
setRequestHeader() getResponseHeader() getAllResponseHeaders()
так можно обрывать соединение
abort()
в обработчик приходит ответ сервера с полезными полями, Вот полезные поля:
responseText responseXML status statusText
Причем responseXML заполняется только в случае, когда Content-Type с сервера имеет значение text/xml (кроме overrideMimeType-метода, но он есть только в Firefox).
Пример использования XMLHTTPRequest:
function getXmlHttp(){ try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch (e) { try { return new ActiveXObject("Microsoft.XMLHTTP"); } catch (ee) { } } if (typeof XMLHttpRequest!='undefined') { return new XMLHttpRequest(); } } // Получить данные с url и вызывать cb - коллбэк c ответом сервера function getUrl(http://javascript.ru/forum/url, cb) { var xmlhttp = getXmlHttp(); // IE кэширует XMLHttpRequest запросы, так что добавляем случайный параметр к URL // (хотя можно обойтись правильными заголовками на сервере) xmlhttp.open("GET", url+'?r='+Math.random()); xmlhttp.onreadystatechange = function() { if (xmlhttp.readyState == 4) { cb( xmlhttp.status, xmlhttp.getAllResponseHeaders(), xmlhttp.responseText ); } } xmlhttp.send(null); }
углубляться дальше не будем — ведь есть про эту штуку целый сайт http://xmlhttprequest.ru/
Надо сказать, что в реальной жизни пользуются этим очень редко. Пользуются удобными обертками, о них и пойдет дальше речь.
Почему редко пользуются ? Просто реализовано в разных браузерах по-разному. Вот НЕПОЛНЫЙ пример «кроссбраузерного использования» (есть и лучше, но не будем тратить место):
function getXmlHttp(){ var xmlhttp; try { xmlhttp = new ActiveXObject("Msxml2.XMLHTTP"); } catch (e) { try { xmlhttp = new ActiveXObject("Microsoft.XMLHTTP"); } catch (E) { xmlhttp = false; } } if (!xmlhttp && typeof XMLHttpRequest!='undefined') { xmlhttp = new XMLHttpRequest(); } return xmlhttp; }
Prototype
prototype js api понимает такую постановку запроса
new Ajax.Request('/your/url', { onSuccess: function(response) { // Handle the response content... } });
обработка запроса проходит следующие фазы:
- Создание
- Инициализация
- Отправка запроса
- Прием ответа (может происходить несколько раз, если данные приходят порциями)
- Ответ принят, Запрос завершен
Каждую фазу можно обработать обработчиками. Prototype понимает различные виды «mime javascript» ответов и преобразует параметры вызова к объектам, возможна гибкая работа с заголовками.
Но в свое время prototype потерял популярность и уступил jQuery. Почему ? Ну, он немного расширял стандартные классы JS своими методами, а когда стандарт JS начал меняться, это стало проблемой. Мораль: не расширяй API стандарт. языка — делай свою либу сбоку (в назидание Ruby RoR).
YUI
Нельзя пройти мимо, ведь YUI исповедует необычный подход. У них есть модуль «io», и работа с ajax происходит как бы в процедурном стиле.
Это лучше всего показать на примере:
// Create a YUI instance using io module. YUI().use("io-base", function(Y) { /* * Create a function as the event handler for the event "io:complete". * * The function will receive the following arguments: * - The ID of the transaction * - Object containing the response data. * - Argument one defined when subscribing to the event(e.g., "foo"). * - Argument two defined when subscribing to the event(e.g., "bar"). */ function onComplete(transactionId, responseObject, arg1, arg2) { /* * The argument 'responseObject' is the response object. Its * properties are: * - status * - statusText * - getResponseHeader(headerName) * - getAllResponseHeaders * - responseText * - responseXML * * NOTE: In an XDR transaction, only the responseText or the responseXML property is defined. */ }; /* * Subscribe to the event "io:complete", using Y.on. * * - 'io:complete' : Subscribe to this io event. * - onComplete : The event handler to be subscribed to 'io:complete'. * - Y : The execution context of the event handler, in this case, the YUI sandbox. * since the doComplete is defined as a global function. * - 'foo' : The first argument received by the event handler. * - 'bar' : The second argument received by the event handler. * Additional arguments can be defined, as desired. */ Y.on('io:complete', onComplete, Y, "foo", "bar"); // Starts the transaction. var request = Y.io(uri); });
Видите ? Настраиваются реакции на start, complete, success, failure, end события и делается просто Y.io(uri);
Ну… Так тоже можно. По мне, так «вывих мозга».
Ext JS
Пример из 4.0 версии EXT JS
Ext.Ajax.request({ url: 'page.php', params: { id: 1 }, success: function(response){ var text = response.responseText; // process server response here } });
Это уже похоже на Jquery код — типичный представитель callback техники.
Параметров хватает для работы. В остальном ни хуже, ни лучше — просто нормально.
Ссылка на описание http://www.objis.com/formationextjs/lib/extjs-4.0.0/docs/api/Ext.Ajax.html
Dojo
DOJO — один из лучших в мире движков или библиотек. Что это конкретно — тут трудно разделить.
Он на голову выше стоящих рядом побратимов. Уже много лет подряд самые передовые фичи появляются сначала в DOJO, а уж потом в прочих библиотеках.
От остальных он отличается системностью подхода. И структура, и модульность, и степень декларативности, и прочие черты выделяют его. Он профессионален, несомненно.
Сборка, деплой, расширение, виджеты, работа с DOM, события, анимации — все это есть там с незапамятных времен из коробки, и это удобно, но к этому обилию нужно привыкать. Кроме того, такое «погружение» выдирает тебя из остального мира, ты будешь видеть только кишки dojo, его будет хватать. И не все готовы платить такую цену. Так что, полнота — это наверное и достоинсво, и недостаток.
Простой пример использования dojo для ajax:
(показана еще и разметка, например, для отображения состояния процесса запроса)
<style type=”text/css” > .success { background-color: #dfd; border: 2px solid #000; } .error { background-color: #fdd; border: 2px dotted #000; } < /style > < div id=”xhrget1” > < h2 > Simple xhrGet < /h2 > < p class=”content_found” > Loading... < /p > < p class=”content_not_found” > Loading... < /p > < /div >
и вот такой, например, код:
dojo.xhrGet({ url: ‘data.txt’, timeout: 5000, load: function(resp, io_args) { dojo.query(‘.content_found’, ‘xhrget1’) .addClass(‘success’) [0].innerHTML = resp; }, error: function(error, io_args) { dojo.query(‘.content_found’, ‘xhrget1’) .addClass(‘error’) [0].innerHTML = error.message; } });
Поддерживает кучу всего, например, Deferreds, таймауты и т.д.
Еще пример:
require(["dojo/request"], function(request){ request.post("post-content.php", { data: { color: "blue", answer: 42 }, headers: { "X-Something": "A value" } }).then(function(text){ console.log("The server returned: ", text); }); });
Здесь используется модульность dojo и deffered в короткой цепочке.
Код у dojo простой и лаконичный (кому как).
Вот пример, где модульность используется более оправдано:
require(["dojo/_base/xhr", "dojo/dom", "dojo/_base/array", "dojo/domReady!"], function(xhr, dom, arrayUtil) { // Keep hold of the container node var containerNode = dom.byId("newsContainerNode"); // Using xhr.get, as we simply want to retrieve information xhr.get({ // The URL of the request url: "get-news.php", // Handle the result as JSON data handleAs: "json", // The success handler load: function(jsonData) { // Create a local var to append content to var content = ""; // For every news item we received... arrayUtil.forEach(jsonData.newsItems, function(newsItem) { // Build data from the JSON content += "
«; content += » » + newsItem.summary + » «; }); // Set the content of the news node containerNode.innerHTML = content; }, // The error handler error: function() { containerNode.innerHTML = «News is not available at this time.»; } }); } );
Отмечу только, что все эти ООП фишки DOJO трудно использовать, потому что JS. (субъективно)
MooTools
Смотри сюда http://mootools.net/core/docs/1.5.2/Request/Request
Пример:
var myRequest = new Request({ url: 'image.jpg', onProgress: function(event, xhr){ var loaded = event.loaded, total = event.total; console.log(parseInt(loaded / total * 100, 10)); } }); myRequest.send();
Тоже хорошая юзабельная обертка, но JQ все же более популярен.
К Mootools стоит присмотреться из-за предлагаемого им способа ООП, ставшего популярным, Core, Сlass — вот , что в этой либе сделано не плохо.
JQUERY
Самый популярный сегодня в мире способ делать AJAX запросы.
Он вобрал в себя «все», а если этого нет, то можно подключить плагины, которых тысячи. Как он стал так популярен — вопрос, достойный отдельного ислледования.
Для AJAX основные методыфункций это:
- jQuery.ajax( url [, settings ] )
- jQuery.get( [settings ] )
- jQuery.post( url [, data ] [, success ] [, dataType ] )
Есть много других функций. Например, для работы с контентом
- .load( url [, data ] [, complete ] )
- пример использования:
$( "#result" ).load( "ajax/test.html" );
здесь в элемент DOM с id=»result» спустя время запишется html содержимое которое вернет запрос заданный параметрами в
load
Плный список методов функций категории AJAX http://api.jquery.com/category/ajax/
Про праметры можно прочитать в документации. Студентам же важно понять отличие GET и POST.
GET и POST — методы HTTP протокола. Лучше всего: просто прочитать спецификацию HTTP протокола и вопрос сам собой отпадет. Лучше прочитать еще и идеологию REST, которая расставляет акценты в HTTP и его использовании.
Коротко: get для получения контента, POST — для обновления, отправки. Вероятно, что GET запросы будут кешироваться по пути к конечному серверу, т.к. проксями рассатриваются как просто получение данных. Поэтому используйте POST, если запрос на сервере что-то делает. Стоит прочитать еще про спец. поля заголовка, про ETag, чтобы эффективней бороться с желанием посредников отдать вам ответ из кеша. Ссылка http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9
comet PUSH в браузер
Ооой, тяжкая тема. Трудно в двух словах. Это связано с хорошим и не хорошим опытом, но попробую.
Технически, бОльшая часть, темы раскрыта тут http://javascript.ru/ajax/comet. Прочитайте, полностью повторяться не охота. Дополню не раскрытые, на мой взгляд, пункты.
WS — Web Socket
Есть, например, WS = WebSocket. Каждый раз, когда я решал, что он готов к использованию, я попадал на грабли и видел, что зря я решил его использовать. Проблемы были, например, такие:
- Есть в нем фаза рукопожатий, а самого WS 100500 версий (drafts) и не понятно с какой версией протокола к вам прийдет клиент — значит, сервер должен их все уметь, а мне надо было сервер тогда делать на C++…. Это был ахтунг вообще! (клиет тоже был на плюсах)
- Версии, вроде, устаканились, но их все равно много. В итоге, не каждый сервер и не каждый клиент могут соединиться. Если клиент — браузер, то хоть половину проблем это «решает» (они не ваши), а выбор серверов сужается.
- Время жизни соединений. С виду WS соединение -это HTTP, но только с виду, и долгоживущее. Для проксей роутеров и маршрутизаторов итд это непривычно. Допустим, мы настроили наше ПО, настроили балансировщики, чтобы наши «постоянные соединения» они не рвали, НО ! все равно у закзачика ты не знаешь, какая будет сеть и как ходят пакеты. И не понятно, почему у него в рамках содинения ну никак не идет больше 100 Метров за коннект и почему соединения не живут больше трех дней. Никак это иногда не выяснить (заказчик и сам может не знать), и ping-pong не помогает. Приходится пересоединяться. А если так, то чем LongPooling хуже? Даже еще лучше — он хоть вообще родной для http.
- Например, java world плохо работает с асинхронщиной в вебе и с WS в частности. Для сервлетов это проблема, которую не сразу понятно, как решать. И многие one shot каркасы не умеют асинхроные WS транспорты (сейчас ситуация сильно поменялась).
И больше наступать на эти грабли нет никакого желания. 🙁
Long Poling
Суть такая http://javascript.ru/ajax/comet/long-poll.
Если транспорт нужно делать руками, то это лучший вариант, я считаю. Никогда не подводил.
Тупой «просто poling» рассматривать вообще не будем — не вариант.
text/event-stream
Такой mime (text/event-stream) есть в спецификации. А в JS есть EventSource.
Этим примерно так можно пользоваться https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events
Я попробовал. Оно хорошо работает, однако, не во всех браузерах. Не работает никогда в IE и в OperaMini. В Остальных браузерах — гуд.
Суть в том, что после ответа для этого mime браузер не будет закрывать сам соединение, а будет ждать новых частей ответа. Если ответ сделать как multipart, браузер эти части будет полностью передавать в обработчик сообщения целиком. Так можно «скармливать» браузеру сообщение за сообщением.
Поддержка с серверной стороны плохая из коробки, но особо и не нужна — вы можете сами это сделать элементарно, например, в tornado.
Socket.io
Это не метод IO, а либа (и сервис).
Идея в том, что у кого что доступно — и это низкий уровень этой либы, а на высоком уровне она просто дает тебе транспорт, решает проблему выбора и сводит выбор к одной только себе.
Ссылка http://socket.io/, лицензия MIT. github.
Интересна тем, что предоставляет связь не только клиент-сервер, но и клиент-клиет из коробки. Это позволяет не влезать в ее серверную часть, а просто развернуть ее у себя, как есть (nodejs). Меньше отладки на севере (ноль).
Прекрасная поддержка есть у Python Tornado (думаю, лучший событийный однопоточный движок), но для java только нашел кое что, но не пробовал, а интересно попробовать.
Поддержка tornado
-
TornadIO2 = Tornado + Socket.IO http://habrahabr.ru/post/133350/ с кодом, с примерами, все хорошо разжевано
Поддержка в java
… для java ни LP ни WebSocket не работают, я так думал, но оказалось, что
https://toster.ru/q/59730 их «полно» для socket.io, правда, не знаю, какого качества, но они есть, и, значит, тем более стоит попробовать.
Перечислены следующие варианты:
- https://github.com/sciworth/Socket.IO-Java
- https://github.com/mrniko/netty-socketio
- https://github.com/simranrkm/JavaSocketIO
- https://github.com/Gottox/socket.io-java-client-new
- https://github.com/Joshua-F/Socket.IO-Java-Client
- https://github.com/nkzawa/socket.io-client.java
- https://github.com/clwillingham/java-socket.io.client
- https://github.com/tadglines/Socket.IO-Java
- https://github.com/Atmosphere/atmosphere
В socket.io есть «комнаты» для общения клиентов. Это похоже на jabber\xmpp, только работает не бинарно, а в браузере. Подобную идеологию также продвигают иные движки, например, с тегом IOT (Internet Of Things), например, JetBus.
JetBus
ссылка: http://jetbus.io/
Не самый популярный, просто написал о нем, т.к. мы использовали его. Сервер у них есть на node.js и на моем любимом LUA — плюсик, кроме того, API имеет более высокий уровень. Короче, мы попробовали переписать один из проектов на нем (Presentator).
Не буду вдаваться в идеологию этой системы — у них на сайте все хорошо разжевано, но кое во что мы уперлись.
Структура значений там состоит из ключей и подключей итд. Так вот, мы ожидали, что порядок событий об обновлении будет совпадать с тем порядком, в котором мы значения меняли, НО порядок уведомлений не совпадает с порядком внесения изменений в состояние.
Эта проблема может вам помешать, помешала она и нам. В итоге, мы написали (нам пришлось) свой сервер jetbus на питоне, который схраняет порядок установки ключей.
Больше я его использовать не стану, тем более, что по концепции MQTT брокеры похожи на jetbus, выше по производительности и протокол стандартный, плюс много обёрток и библиотек для разных языков.
MQTT брокеры
Посмотрите этот бенч. http://www.scalagent.com/IMG/pdf/Benchmark_MQTT_servers-v1-1.pdf
От себя добавлю, что мы в KV сервисе всё же используем WS а не MQTT. Почему ?
При высоких нагрузках становится ценным каждое соединение и то, что в MQTT и WS в одном соединении может ходить масса запросов и ответов (фреймов) это хорошо.
Но MQTT PUB-SUB паттерн плохо ложится на message passing, в рамках которого легче описывать распределённые алгоритмы. А поскольку MQTT «data format agnostic» и WS тоже, то всё равно задача кодирования должна решаться пользователем и никакого преимущества тогда MQTT над WS не имеет.
Кроме того, WS (который поверх HTTP) проще балансировать к бекендам, чем это пришлось бы делать на MQTT через бинарный balancer, например, haproxy.