Pull to refresh

Асинхронный обмен данными поверх HTTP

Reading time4 min
Views11K
Не так давно на работе передо мной была поставлена задача реализовать механизм асинхронного обмена данными между веб-приложением на Java и веб фронт-эндом на стороне клиента. Задача заключалась в том, чтобы клиент получал апдейты с минимальной задержкой, при этом апдейты могли приходить со скоростью 100 апдейтов в секунду, так и 1 апдейд в минуту, т.е. желательно не слать лишних запросов со стороны клиента.

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


Кстати, схема по которой это работало:



А вот немного кода со стороны клиента(ничего сверхестественного: таймер + посылка JSON request):

function onLoad() 
{
	// Invoke request update each second
	intervalId = setInterval ( requestUpdate(), 1000 );
}
 
function requestUpdate()
{	
 
	$.getJSON(URL, JSONParams, function(data) {
 
	$.each(data, function(key, val) {				  
		// Process data
	})
 
	}).success(function(){
		// Succes handler					
	}).error(function(){
		// Error handler
	});
 
};
 
function onUnload() 
{
	clearInterval(intervalID);
};
 
$(document).ready(function() {
 
	// Disable caching for ajax
	$.ajaxSetup({ cache: false });		
 
});


И со стороны сервера(по этическим аспектам не могу разглашать код, отвечающий за бизнес-логику приложения, да смысл статьи не в нем):

public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
 
	//Get reqeust parameters 
	JSONObject jsonObj = JSONObject.fromObject(req.getParameterMap());
 
	//Some code
 
	//Get writer
	PrintWriter out = res.getWriter();
 
	//Create json object for response
	Map<String, String[]> map = retriever.getUpdates();
	JSONObject jsonObject = JSONObject.fromObject(map);
 
	//Send response
	out.println(jsonObject);
 
	//Finish response!
	out.close();
 
}


Первое, на что я наткнулся — были статьи about Long Polling (WebSockets я исключил сразу, потому как не у всех клиентов могут быть установлены современные браузеры). Меня очень заинтересовала статья на сайте IBM Developerworks, где подробно расписаны основные моменты организации асинхронного обмена данными между сервером и клиентом при помощи http протокола. В статье я обратил внимание на интересный пример. Советую и вам c ним ознакомиться(конечно если вам интересно).

Как оказалось далее в Tomcat'e, который используется у заказчиков, также есть поддержка Сomet(Pushing) механизма. Реализуется она имплементацией интерфейса CometProcessor. Подробный пример есть на сайте Tomcat, поэтому рекомендую обратиться к документации с примерами.

В итоге я решил имплементировать этот движок на строне сервера. Пример приводить не буду, потому как он мало чем отличается от приведенных выше (в ссылках на статьи).

Схема работы long polling соединения:



Главное отличие от схемы, которую вы видели выше это то, что сервер отправляет response не сразу, а ждет определенного события. Это достигается путем выставления в request header тега keep-alive. Данный тег заставляет сервер не рвать соединение раньше времени. После того как был отправлен response и клиент его получил, клиент снова отправляет ещё один запрос и ждет ответа. По сути здесь мы наблюдаем рекурсивный вызов.

Реализация long polling клиента на javascpirt:
function go(){
    var url = "your url"
    var request =  new XMLHttpRequest();
    request.open("GET", url, true);
    request.setRequestHeader("Content-Type","application/x-javascript;");
    request.onreadystatechange = function() {
		if (request.readyState == 4) {
			if (request.status == 200){
				if (request.responseText) {
					//Something
				}
			}
			go();
		}
    };
    request.send(null);
}


А так бы это могло выглядеть с использованием JQuery:
function poll(){
    $.ajax({ url: "your url", success: function(data){
        //Something
    }, dataType: "json", complete: poll, timeout: 30000 });
});


Схема работы stream соединения:



Здесь, как вы видите, клиент посылает только 1 запрос в самом начале. А далее сервер на каждое событие отправляет клиенту кусочек информации. Это достигается благодаря тому, что writer в response не закрывается, посылка выполняется только через метод flush(). Благодаря этому клиент продолжает вычитывать информацию из потока.

Реализация stream клиента на javascript:
function switchXHRState() {  
	switch (this.readyState) {  
		case 0: $("#messages").append("open() has not been called yet."); break;  
		case 1: $("#messages").append("send() has not been called yet."); break;  
		case 2:$("#messages").append("send() has been called, headers and status are available."); break;  
		case 3: if (this.status == 200) {Some lines of code} break;  
		case 4: $("#messages").append("Complete!"); break;  
	}  
};
 
function go() {
 
	var url = "comet";
	var request = new XMLHttpRequest();
	request.onreadystatechange = switchXHRState; 
 
	request.open("GET", url, true);
 
	request.setRequestHeader("Content-Type", "application/x-javascript;");
	request.setRequestHeader("Cache-Control", "no-cache");
	request.setRequestHeader("Cache-Control", "no-store");
	request.setRequestHeader("Cache-Control", "no-store");
	request.send(null);
 
}


Дополнительные материалы:
1. Статья в википедии о Push Technology
2. Статья на английском языке, откуда были позамствованы картинки
3. AJAX Patterns (Название говорит само за себя)
4. Спецификация на XMLHttpRequest

Tags:
Hubs:
+45
Comments46

Articles