Простой comet

Всю неделю готовил эту статью, и вот наконец публикую.

Что же первое приходит в голову, когда видишь слово «comet», правильно — чистящее средство, потом комета, а оно почему-то означает технологию (хотя это, наверное, громко сказано, скорее паттерн) постоянного соединения с сервером. Все же в толстых книжках читали, что после того как сервер получил запрос и отдал ответ браузеру, он забывает, что к нему вообще кто-то обращался. А тут поседели умные люди и стали седыми. Нет, посидели и придумали идею, как заставить передавать браузеру информацию, изменившуюся на сервере.

UPD Вообще код примера полное гавно, смотрю и плачу что когдато считал его венцом творения. Пробовал починить примеры, но это требует настроек nginx (к которому у меня доступа нет), тестов php (беферизация всегда безбожно глючила) и нового js (учитывая requirejs). А вообще методика явно устарела и при создании нового продукта лучше взять чтото посвежее, так что я забил.

Вот собственно об этом мы с вами сегодня и поговорим, а собственно я как обычно буду высказывать свои ламерские мысли, а вы, как обычно, будете комментировать…

Comet

И так с чего же начать, начать надо с того что выберем серверным языком php а клиентским как это не странно — javascript. Для того чтобы соединение не разрывалось, php скрипт должен все время работать, но тут мы встречаем два ограничения. Первое вполне логичное у нас нет бесконечного скрипта который бы все время что-то делал, да это и не надо, достаточно включить в код бесконечный цикл. А вот собственно второе ограничение уже куда более неприятное: по умолчанию php скрипт может выполняться только 30 секунд, после чего сервер выдает ошибку о зависании скрипта. С этим мы будем бороться, но об этом потом.

Теперь когда мы имеем серверную часть нужно придумать как она будет контактировать с клиентской. Для этого нужно написать еще какую-то функцию на javascript, которую бы мог вызвать сервер, причем это должно работать параллельным потоком относительно основной страницы. Для реализации параллельного запроса идеально подходит AJAX, но, к сожалению IE и Opera, не умеют корректно обрабатывать событие onreadystatechange==3 , поэтому для реализации параллельного потока в них используют iframe, а я буду использовать его и для остальных браузеров.

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


// устанавливаем неограниченное время выполнения скрипта
ini_set('max_execution_time', 0);

// начинаем буферизацию
ob_start();
// ставим флаг отдавать буфер каждый раз как он больше не нужен
ob_implicit_flush(true);

// отдаем заголовки, чтобы избежать кэширования
header('Expires: Sut, 01 Jan 2000 00:00:00 GMT');
header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');

// выбрасываем в поток килобайт пробелов, 
// это нужно из-за того, что php первый раз меньше отдавать не хочет
echo str_repeat(chr(32), 1024);
ob_get_clean();   

// в бесконечном цикле описываем то передачу информации в браузер
while(true){
	echo ""
		."<script type='text/javascript'>\n"
		."parent.$(parent.document).trigger('comet".$_GET['sid']."',['".date("D, d M Y H:i:s O")."']);\n"
		."</script>\n";
	ob_get_clean();
	// ждем 1 секунду до следующего обновления
	sleep(1);
}

А вот и пример работы этого скрипта находиться сразу под кодом.

Hello from comet!

Теперь давайте разберем, клиентскую часть, она тоже весьма не большая. Как я уже говорил, ее задача в основном создать iframe и приаттачить нужные события. Событий может быть всего два: обновление с новыми данными и разрыв соединения с сервером. Оба события необязательные, потому что сервер и сам вполне может отдавать готовый javascript код, а разорванное соединение можно восстанавливать, а можно предположить, что все обновления с сервера уже пришли и новых больше не будет.


(function($) {

	// конструктор объекта
	var comets = function(options){
		this.init(options);
	}
	
	// собственно сам объект
	comets.prototype = {
		comets : [], // итератор соединений
		options : { // массив функций по умолчанию
			connect:function(sid){
				// функция, выполняющаяся при разрыве соединения с сервером
			},
			update:function(data){
				// функция, вызываемая сервером с новыми данными
			},
			url:"", // адрес бэкэнда
		},
		init : function(options){
			var self = this, 
				comet = {},
				sid = self.comets.push([]); // i++

			// создаем реальный массив параметров
			$.extend(comet,self.options,options)
			
			// создаем iframe и соединение
			$("<iframe/>")
				.appendTo("body")
				.attr({src:comet.url+sid})
				.bind("load", function(){comet.connect.apply(this,[sid]);})
				
			// аттачим события, к ифрейму аттачить нельзя, так как он еще не загрузился
			$(document).bind("comet"+sid, function(event,data){comet.update.apply(this,[data])})
		}
	}

	$.fn.comets = function(options){
		// создаем новый comet
		new comets(options);
		
		// не нарушаем цепочку
		return this;
	}
})(jQuery);

Теперь надо передать в этот плагин (да я как всегда все делаю плагином под jQuery) наши функции.


$().comets({
	// функция которая обновит такст часов
	update:function(data){
		$("#clock").text(data);
	},
	// функция которая востановит соединение с сервером
	connect:function(sid){
		$(this).attr({src:"clock.php?sid="+sid});
	},
	// адресс бэкэнда
	url:"clock.php?sid="
});

Как я говорил в начале я расскажу как бороться с временем выполнения скрипта если вам недоступна функция ini_set(); Для этого надо узнать сколько времени может выполняться скрипт и прерывать его работу незадолго до этого времени. Если передать браузеру команду восстановить соединение, то этого можно не указывать в параметрах в javascript. Вносим правки в алгоритм работы скрипта:


// узнаем время рабы и отнимаем 5 секунд запаса
$metime = ini_get('max_execution_time') - 5;

// узнаем время начала и конца работы скрипта
$start = $stop = array_sum(explode(" ",microtime()));

ob_start();
ob_implicit_flush(true);
header('Expires: Sut, 01 Jan 2000 00:00:00 GMT');
header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');

echo str_repeat(chr(32), 1024);
ob_get_clean();   

// по условию, что время начала больше чем время конца минус время исполнения прерываем процедуру
while($start > $stop-$metime){
	echo ""
		."<script type='text/javascript'>\n"
		."parent.$(parent.document).trigger('comet".$_GET['sid']."',['".date("D, d M Y H:i:s O")."']);\n"
		."</script>\n";
	ob_get_clean();
	$stop = array_sum(explode(" ",microtime()));
	sleep(1);
}

// echo "<script type='text/javascript'>window.location='".$_SERVER['PHP_SELF']."?sid=".$_GET['sid']."';</script>n";

Вот пример работы этого кода, только для наглядности я поставил обрыв соединения каждые 10 секунд.

Hello from comet!

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

14 Комментарии “Простой comet

  1. А как справится с постоянно надоедающим курсором загрузки (типа часики) в Opera и FireFox ?

  2. Если устраивает курсор типа pointer то можно применить его к body, если нет то наверное никак.

  3. хуйня это а не камет))

    пока браузеры не будут поддерживать, это все костыли. я не знал что это называется комет и юзал еще года 4 назад =)))))

    зы: маврик однозначно зачет за статью, пригодится кому нить все равно :)

  4. попробуй поставь :)

    тут может nginx проксирует, тогда да конечно, а нет нету у тебя нгинкса на фронте, должно работать с флашем

Комментарии закрыты