Случилось мне как-то иметь дело с тяжелым PHP-скриптом. Нужно было каким-то образом в браузере отображать прогресс выполнения задачи в то время, пока в достаточно длительном цикле на стороне PHP проводились расчёты. В таких случаях обычно прибегают к периодичному выводу строки вроде этой:

<script>document.getElementById('progress').style.width = '1%';</script>

Этот вариант меня не устраивал по нескольким причинам, к тому же мне в принципе не нравится такой подход.

Итераций у меня было порядка 3000—5000. Я прикинул, что великоват трафик для такой несложной затеи. Кроме того, мне такой вариант казался очень некрасивым с технической точки зрения, а внешний вид страницы и вовсе получался уродлив: футер дойдет еще не скоро — после последнего уведомления о 100% выполнении задачи.

Увернуться от проблемы некрасивой страницы большого труда не составляло, но остальные минусы заставили меня обрадоваться и приступить к поискам более изящного решения.

Несколько наводящих вопросов. Асинхронные HTTP-запросы возможны? — Да. Можно ли с помощью одного-единственного байта сообщить, что часть большой задачи выполнена? — Да. Можем ли мы постепенно (последовательно) получать и обрабатывать данные с помощью XMLHttpRequest.onreadystatechange? — Да. Мы даже можем воспользоваться заголовками HTTP для передачи предварительного уведомления об общей продолжительности выполняемой задачи (если это возможно в принципе).

Решение простое. Основанная страница — это пульт управления. С пульта можно запустить и остановить задачу. Эта страница инициирует XMLHttpRequest — стартует выполнение основной задачи. В процессе выполнения этой задачи (внутри основного цикла) скрипт отправляет клиенту один байт — символ пробела. На пульте в обработчике onreadystatechange мы, получая байт за байтом, сможем делать вывод о прогрессе выполнения задачи.

Схема такая. Скрипт операции:

<?php

set_time_limit(0);
for ($i = 0; $i < 50; $i++)	// допустим, что итераций будет 50
	{
	sleep(1);	// Тяжелая операция
	echo ' ';
	}


Обработчик XMLHttpRequest.onreadystatechange:

xhr.onreadystatechange = function()
	{
	if (this.readyState == 3)
		{
		var progress = this.responseText.length;
		document.getElementById('progress').style.width = progress + '%';
		}
	};

Однако, итераций всего 50. Об этом мы знаем, потому что сами определили их количество в файле скрипта. А если не знаем или количество может меняться? При readyState == 2 мы можем получить информацию из заголовков. Давайте этим и воспользуемся для определения количества итераций:

header('X-Progress-Max: 50');

А на пульте получим и запомним это значение:

var progressMax = 100;

xhr.onreadystatechange = function()
	{
	if (this.readyState == 2)
		{
		progressMax = +this.getResponseHeader('X-Progress-Max') || progressMax;
		}
	else if (this.readyState == 3)
		{
		var progress = 100 * this.responseText.length / progressMax;
		document.getElementById('progress').style.width = progress + '%';
		}
	};

Общая схема должна быть ясна. Поговорим теперь о подводных камнях.

Во-первых, если в PHP включена опция output_buffering, нужно это учесть. Здесь все просто: если она включена, то при запуске скрипта ob_get_level() будет больше 0. Нужно обойти буферизацию. Еще, если вы используете связку Nginx - FastCGI - PHP, нужно учесть, что и FastCGI и сам Nginx будут буферизовать вывод. Последний это будет делать в том случае, если собирается сжимать данные для отправки. Устраняется проблема просто:

header('Content-Encoding: none', true);

Если проблему с gzip можно решить внутри самого PHP-скрипта, то заставить FastCGI сразу передавать данные можно только поправив конфигурацию сервера:

fastcgi_keep_conn on;

Кроме того, то ли Nginx, то ли FastCGI, то ли сам Chrome считают, что инициировать прием-передачу тела ответа, которое содержит всего-навсего один байт — слишком расточительно. Поэтому нужно предварить всю операцию дополнительными байтами. Нужно договориться, скажем, что первые 20 пробелов вообще ничего не должны означать. На стороне PHP их нужно просто «выплюнуть» в вывод, а в обработчике onreadystatechange их нужно проигнорировать. На мой взгляд — раз уж вся конфигурационная составляющая передается в заголовках — то и это число игнорируемых пробелов тоже лучше передать в заголовке. Назовем это padding-ом.

<?php

header('X-Progress-Padding: 20', true);
echo str_repeat(' ', 20);
flush();

// ...

На стороне клиента это тоже нужно учесть:

var progressMax = 100,
	progressPadding = 0;

xhr.onreadystatechange = function()
	{
	if (this.readyState == 2)
		{
		progressMax = +this.getResponseHeader('X-Progress-Max') || progressMax;
		progressPadding = +this.getResponseHeader('X-Progress-Padding') || progressPadding;
		}
	else if (this.readyState == 3)
		{
		var progress = 100 * (this.responseText.length - progressPadding) / progressMax;
		document.getElementById('progress').style.width = progress + '%';
		}
	};

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

Кстати, насчет настройки PHP output_buffering. Если у вас сложная буферизация и вы не хотите ее нарушать, можно воспользоваться такой функцией:

function ob_ignore($data, $flush = false)
	{
	$ob = array();
	while (ob_get_level())
		{
		array_unshift($ob, ob_get_contents());
		ob_end_clean();
		}
	
	echo $data;
	if ($flush)
		flush();
	
	foreach ($ob as $ob_data)
		{
		ob_start();
		echo $ob_data;
		}
	return count($ob);
	}

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

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

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

progress-loader.js
function ProgressLoader(url, callbacks)
	{
	var _this = this;
	for (var k in callbacks)
		if (typeof callbacks[k] != 'function')
			callbacks[k] = false;
	delete k;
	
	function getXHR()
		{
		var xhr;
		try
			{
			xhr = new ActiveXObject("Msxml2.XMLHTTP");
			}
		catch (e)
			{
			try
				{
				xhr = new ActiveXObject("Microsoft.XMLHTTP");
				}
			catch (E)
				{
				xhr = false;
				}
			}
		if (!xhr && typeof XMLHttpRequest != 'undefined')
			xhr = new XMLHttpRequest();
		return xhr;
		}
	
	this.xhr = getXHR();
	this.xhr.open('GET', url, true);
	
	var contentLoading = false,
		progressPadding = 0,
		progressMax = -1,
		progress = 0,
		progressPerc = 0;
	
	this.xhr.onreadystatechange = function()
		{
		if (this.readyState == 2)
			{
			contentLoading = false;
			progressPadding = +this.getResponseHeader('X-Progress-Padding') || progressPadding;
			progressMax = +this.getResponseHeader('X-Progress-Max') || progressMax;
			if (callbacks.start)
				callbacks.start.call(_this, this.status);
			}
		else if (this.readyState == 3)
			{
			if (!contentLoading)
				contentLoading = !!this.responseText
					.replace(/^\s+/, '');	// .trimLeft() — медленнее О_о
			
			if (!contentLoading)
				{
				progress = this.responseText.length - progressPadding;
				progressPerc = progressMax > 0 ? progress / progressMax : -1;
				if (callbacks.progress)
					{
					callbacks.progress.call(_this,
						this.status,
						progress,
						progressPerc,
						progressMax
						);
					}
				}
			else if (callbacks.loading)
				callbacks.loading.call(_this, this.status, this.responseText);
			}
		else if (this.readyState == 4)
			{
			if (callbacks.end)
				callbacks.end.call(_this, this.status, this.responseText);
			}
		};
	if (callbacks.abort)
		this.xhr.onabort = callbacks.abort;
	
	this.xhr.send(null);
	
	this.abort = function()
		{
		return this.xhr.abort();
		};
	
	this.getProgress = function()
		{
		return progress;
		};
	
	this.getProgressMax = function()
		{
		return progressMax;
		};
	
	this.getProgressPerc = function()
		{
		return progressPerc;
		};
	
	return this;
	}

process.php
<?php

function ob_ignore($data, $flush = false)
	{
	$ob = array();
	while (ob_get_level())
		{
		array_unshift($ob, ob_get_contents());
		ob_end_clean();
		}
	
	echo $data;
	if ($flush)
		flush();
	
	foreach ($ob as $ob_data)
		{
		ob_start();
		echo $ob_data;
		}
	return count($ob);
	}

if (($work = @$_GET['work']) > 0)
	{
	header("X-Progress-Max: $work", true, 200);
	header("X-Progress-Padding: 20");
	ob_ignore(str_repeat(' ', 20), true);
	
	for ($i = 0; $i < $work; $i++)
		{
		usleep(rand(100000, 500000));
		ob_ignore(' ', true);
		}
	
	echo $work.' done!';
	die();
	}

launcher.html
<!DOCTYPE html>
<html>
<head>
<title>ProgressLoader</title>
<script type="text/javascript" src="progress-loader.js"></script>
<style>
progress, button {
	display: inline-block;
	vertical-align: middle;
	padding: 0.4em 2em;
	margin-right: 2em;
}
</style>
</head>
<body>
<progress id="progressbar" value="0" max="0" style="display: none;"></progress>
<button id="start">Start/Stop</button>
<script>

var progressbar = document.getElementById('progressbar'),
	btnStart = document.getElementById('start'),
	worker = false;

btnStart.onclick = function()
	{
	if (!worker)
		{
		var url = 'process.php?work=42';
		worker = new ProgressLoader(url, {
			start: function(status)
				{
				progressbar.style.display = 'inline-block';
				},
			progress: function(status, progress, progressPerc, progressMax)
				{
				progressbar.value = +progressbar.max * progressPerc;
				},
			end: function(status, s)
				{
				progressbar.style.display = 'none';
				worker = false;
				},
			});
		}
	else
		{
		worker.abort();
		progressbar.style.display = 'none';
		worker = false;
		}
	};

</script>
</body>
</html>

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

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


  1. Strate
    30.04.2015 12:23
    -16

    Чего только люди не придумают, чтобы комет-сервер не использовать.


    1. crmMaster
      30.04.2015 13:00
      +20

      Ну да — давайте поставим комет и будем его поддерживать вместо того, чтобы просто в HTTP заголовке передать что нам нужно.


  1. yjurfdw
    30.04.2015 14:24
    +9

    $work = @$_GET['work'] 
    

    Берет за душу, поправьте, пожалуйста на
    $work = isset($_GET['work']) ?  $_GET['work'] : 0;
    


    1. Aiki
      30.04.2015 14:48
      +13

      Еще лучше на:

      $work = isset($_GET['work']) ? intval($_GET['work']) : 0;
      


      1. beono
        30.04.2015 16:09

        Еще лучше на:

        $work = filter_has_var(INPUT_GET, 'work') ? filter_input(INPUT_GET, 'work', FILTER_VALIDATE_INT) : 0


        1. baltazorbest
          30.04.2015 17:05
          +2

          А подскажите пожалуйста, в чем приемущество filter_has_var над $_GET?


          1. Aiki
            30.04.2015 17:45
            +1

            filter_has_var — функция, проверяющая на наличие требуемого ключа в массивах входных параметров скрипта. $_GET — один из таких массивов. В чем преимущество функции над массивом, затрудняюсь ответить…

            Преимущество filter_has_var(INPUT_GET, 'work') над isset($_GET['work']) в том, что filter_has_var() вернет false, если ключ «work» был задан программно, а не передан в качестве параметра запроса. То есть:

            <?php
            $_GET['test'] = 1;
            echo filter_has_var(INPUT_GET, 'test') ? 'Yes' : 'No';
            ?>
            выдаст «No», если в запросе к скрипту не будет ?test=some_value.


            1. Remper
              30.04.2015 19:17
              +1

              С точки зрения этой задачи — преимущество ваше ничего не значит.
              С точки зрения разработки софта в целом, это получается наоборот хуже. Получается вы привязывайтесь напрямую к семантике гет-параметров. А если вам надо будет в самом деле проэмулировать наличие гет-параметра где-то выше вашего кода? Не говоря уже тупо о тестировании без реального запроса.
              Так что ваш вариант намного длиннее, а плюсов совершенно ноль. Эта функция узкоспециализированна и её необходимо использовать только там где это действительно нужно, например, когда у вас побился $_GET и в обработчике исключения нужно выяснить что было изначально в запросе, чтобы в последствии продебажить.

              Касательно phpшных фильтров (впрочем как и $_ массивов) — не уверен что это то что вы хотите в целом использовать. Есть огромное количество библиотек, как независимых, так и в составе фреймворков, которые вам предложат гораздо более удобные средства по валидации входящих данных. Опять же вы завязываетесь на то, что данные вам приходят из HTTP запроса определённым образом (хедеры, GET, POST...). Сегодня у вас данные приходят в GET, завтра могут в POST, послезавтра могут и так и так, через месяц вас заставят сериализовывать все параметры в JSON и слать просто в message body.
              Первый параметр в filter_input сделает в данном случае вам много головной боли.

              Для этой конкретной задачи и ваш комент и мой, кстати, никакой ощутимой пользы не принесут. Если хочется сделать такую задачу «красиво», то делать её надо всё равно совершенно по-другому. Автор по сути заменил одно костыльное решение на другое чуть менее костыльное.


              1. Remper
                30.04.2015 19:31
                +2

                Правильное решение конечно же будет зависеть от вашей нагрузки и инфраструктуры, но в целом очень неплохое правило — тяжёлые задачи должны как минимум выполняться асинхронно в отдельном потоке. Запросил — сервер отдал ответ что принял задачу, как он дальше распланировал её выполнение не сильно интересно — миллион способов. Главное чтобы у клиента была возможность сделать запрос о статусе задачи, отменить, принять результат — вот это вот всё. Не надо бояться (как написал автор ниже) вешать таймер и переодически проверять выполнение.
                Костыльное решение здесь гораздо опасней: ваше соединение может разорваться, вы можете вылететь по таймауту у PHP, вы можете забить максимальное количество одновременных запросов и следующий пользователь будет ждать когда же этот долгий запрос у кого-нибудь посчитается… Много всякого нехорошего. Не устраивайте себе проблемы на ровном месте)


          1. beono
            30.04.2015 18:28

            Помимо того, что написал Aiki рекомендую почитать аргументы по ссылке: stackoverflow.com/questions/15102796/when-to-use-filter-input

            Не даром фреймворки используют свои обертки над глобальными переменными.


  1. AlexTest
    30.04.2015 14:36
    +12

    Ваша реализация сильно зависит от настроек конкретного веб-сервера, где устанавливаются размеры буферов ответа, причем это может быть даже в нескольких местах — не хочу вдаваться в подробности.
    Более «серверонезависимым» будет вариант когда клиент запускает одним ajax-запросом «тяжелый» php скрипт (назовем его process.php) и с помощью другого ajax-запроса в цикле проверяет прогресс, запрашивая второй php скрипт (назовем его status.php). Во время своей работы process.php с помощью curl (или любым другим способом) сообщает status.php свое состояние используя любой удобный вариант идентификации.
    Такой вариант мне кажется намного надежнее, чем «борьба» с буферами ответа.


    1. bBars Автор
      30.04.2015 15:30

      Такой вариант я тоже рассматривал, но мне сначала не понравилось, что параллельный ajax-запрос будет висеть на таймере. Еще не понравилось, что нужен какой-то механизм хранения состояния выполнения конкретного {process.php} (он может быть запущен одновременно несколькими клиентами несколько раз). То есть, к моменту начала выполнения нужно знать идентификатор процесса {process.php} для каждого XHR. Для каждого такого идентификатора нужно где-то хранить состояние (memcache?).
      В виду этих мелочей мне и захотелось сделать так, как я сделал.
      Да, с конфигурацией сервера и прилежащих частей есть загвоздки. Но большинство из них — те, которые я описал — не очень сильно преобразуют дефолтный рабочий конфиг.


      1. AlexTest
        30.04.2015 15:47

        Вам достаточно первый раз отдавая страницу launcher.html клиенту проставить в ней уникальный ID для процесса и использовать его при запуске процесса и при запросе статуса, например прямо в URL: /process.php?id=ID и /status.php?id=ID, свой статус process.php сообщает в status.php используя этот же ID, а это состояние status.php может хранить в своей сессии также используя этот же ID. Причем передавать можно гораздо больше информации о состоянии процесса, ну там текущий шаг к примеру и сколько осталось по этому шагу и т.д. + ничего в настройках сервера и php связанного с буферизацией вывода менять не придется. В вашем варианте надо всегда настраивать конкретный сервер, а это далеко не всегда можно легко и просто сделать.


        1. Blumfontein
          30.04.2015 17:40

          >> /process.php?id=ID и /status.php?id=ID

          По-моему проще просто в сессию все складывать, process в сессию пишет прогресс, а status оттуда его забирает


          1. AlexTest
            30.04.2015 20:14
            +1

            Не проще. В разных реализациях-настройках рнр сессий на различных веб-серверах возможны проблемы блокировки сессий в такой ситуации, и это надо будет как-то «разруливать» так как: Session data is usually stored after your script terminated.


            1. Blumfontein
              04.05.2015 14:32

              А, пардон. Я уж и забыл, что в php сессии блокируются.


        1. bBars Автор
          30.04.2015 21:41

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


      1. ScorpLeX
        30.04.2015 15:53

        Пользователь не сможет воспользоваться вашим скриптом имея плохое соединение, например из Китая или с мобильного интернета в деревне.


        1. bBars Автор
          30.04.2015 21:16

          Ну, при подобных обстоятельствах и запрашивать у сервера статус с интервалом — тоже будет проходить не без неприятностей.
          Я понял свою ошибку: я не указал область применения. Последний случай был таким: у меня была выборка (unbuffered query) из большой таблицы БД, а в процессе транспортировки данных проводилась дополнительная фильтрация, при которой примерно треть отсеивалась. После этого проводилась сравнительно быстрая шлифовка результата. Все, результат выводился. Весь процесс занимал от полминуты до получаса (и это считалось нормой, это для админки). То есть, да, отчасти и по этой причине я даже не рассматривал post-запросы в скрипте, как бы намекая, что get — он для того и get, только для получения, прерывание которого не приведёт к проблемам вроде «скрипт отработал только на половину».


    1. prishelec
      30.04.2015 16:58

      Приходилось прибегнуть к похожему решению. Главное не использовать сессии в скриптах: ajax.php и progress.php (это так: для справки).


      1. Blumfontein
        30.04.2015 17:33

        А в чем проблема сессий?


        1. DoctorX
          30.04.2015 19:08
          +1

          Старт сессии устанавливает блок.
          Если не закрыть сессию вы не сможете выполнить оба запроса параллельно.


  1. sunnybear
    30.04.2015 15:19
    +1

    1Кб на прием-передачу данных раз в 30 секунд — это много? Вы можете вычислить примерную скорость уже после 1 ответа сервера — с процентом выполнения. И дальше ее корректировать, а на клиенте показывать плавный индикатор. Там Google PageSpeed Insights делает. Вам точные проценты не нужны, можно просто реперные точки передавать.


  1. ScorpLeX
    30.04.2015 15:24
    +2

    Если юзер вдруг обновит страницу или произойдет отключение, то скрипт прекратить свою работу на половине задачи и возможно все поломается.

    Я бы использовал ignore_user_abort и заставил скрипт выводить в файл % выполнения. А на странице подгружал бы этот файл по таймауту, запросы на обычные файлы у сервера проходят в разы легче чем к скриптам.


    1. AlexTest
      30.04.2015 15:31

      Вас не смущает, что весь этот файловый мусор потом надо будет как-то убирать?


      1. ScorpLeX
        30.04.2015 15:36
        +1

        Смотря что за задача, если требуется 1 процесс, то будет 1 файл. Если нет, можно удалять файл самим же скриптом, а на клиенте прописать, что если файл был, а потом удалился, значит задача выполнена на 100%. Или передавать доп запрос в конце на удаление файла.

        Или же реализовать на скриптах, хранить в базе\памяти, зависит от задачи, требований к нагрузкам, времени которое можно уделить на решение.


    1. bBars Автор
      30.04.2015 15:33

      Для разных ситуаций — разные решения. А если как раз нужно остановить выполнение скрипта?


      1. ScorpLeX
        30.04.2015 15:41

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

        Кнопка «Стоп» передающая запрос в любом случаи будет лучше.

        Можно комментировать раз в 5 минут — несправедливость.


        1. bBars Автор
          30.04.2015 21:46

          Существует метод XMLHttpRequest.abort(), который я пробросил в своем этом классе-обертке. В примере это есть: кнопка старта играет и роль кнопки для остановки.


  1. thecoder
    30.04.2015 15:48
    +2

    Websockets вам в помощь. Как раз для коротких асинхронных сообщений с сервера.


    1. ScorpLeX
      30.04.2015 16:00
      -4

      Более высокая сложность реализации, доп ПО. А из плюсов небольшая экономия на заголовках пакетов и чуть более плавная индикация состояния. В данном случаи того не стоит, как по мне.


  1. savostin
    30.04.2015 15:54
    +2

    Попробуйте SSE


  1. Rhaps107
    30.04.2015 16:15

    readyState == 3

    Насколько это стало кроссбраузерно?
    Года 3 назад я заметил эту возможность у xmlhttprequest обрабатывать порции данных, но тогда именно вот этот ready state 2 не выдавали какие-то из популярных на то время браузеров.


    1. bBars Автор
      30.04.2015 21:56
      -1

      Уверены, что дело в самих браузерах было?

      Еще дополнение. Уже после публикации статьи я наткнулся на ситуацию в Хроме: событие onreadystatechange отрабатывало должным образом, только если Content-Type != text/plain. Если text/plain, то Хром, по видимому, складывал все в какой-то свой буфер, а уже после EOF показывал все полученные данные разом. Даже если поменять на text/unknown или text/something, проблема улетучивалась.


  1. ntfs1984
    30.04.2015 16:45

    Интересный подход, хотя мне кажется что вызов на каждую итерацию — расточительно.
    Гораздо логичнее будет заставить скрипт выводить свой собственный прогресс куда нибудь, например в файл /tmp/progress_id, а веб часть скрипта будет считывать с заданной периодичностью этот файл, и выводить полезную информацию (а может и не только прогресс) на экран. Если процесс быстрый — будут выведены проценты 1, 65, 99, 100. Если медленный — будут выведены все проценты от 1 и до 100.


    1. defuz
      30.04.2015 18:09

      … Потом мы понимаем, что в файл писать слишком накладно, да и задачу нужно разпаралелить и начинаем писать в какой-нибудь MySQL. А когда база данных начнет отказывать по timeout, мы перепишем все на Redis или запихнем таблицу в память целиком. А протом окажется что нам нужна бoльшая стабильность, задачи нужно балансировать на несколько серверов, а на одной странице может выполнятся сразу несолько задач, и мы наконец сделаем нормальное решение на базе message broker и websockets.


      1. ntfs1984
        30.04.2015 18:24

        Если человек асилил эту технологию — то конечно лучше сделать нормальное решение.
        Но если нет (например я) — приходится изобретать костыли, что поделать.


        1. defuz
          30.04.2015 18:50

          Как правило технологии осиливаются на своем горьком опыте, так что все ок, но за использоваине файлов я все-таки влеплю вам мысленный подзатыльник. Подумайте сами, что расточительнее по ресурсам: отправить 5000 байт клиенту или сделать запись в файл 5000 раз с полным сбросом буфера. HDD начнут взлетать от такой нагрузки, а SSD вы просто задолбаетесь менять.


      1. bBars Автор
        30.04.2015 23:30

        Если это нужно для пары несложных страниц на всю систему (которая, как часто бывает, живет под девизом «нужно приспосабливаться под текущие условия — так сложилось исторически»), то все эти домыслы не к месту.


  1. Elfet
    30.04.2015 17:16

    Мне кажется все зависит от того что за долгий скрипт собирает выполнятся. Я бы делал на ReactPHP (WebSokets).


  1. defuz
    30.04.2015 17:30
    +5

    Простите конечно, но хабрахабр не Stackoverflow. А ваша статья больше похожа на тот как делать не стоит, даже несмотря на просьбу сильно не пинать в конце.

    Кроме того, то ли Nginx, то ли FastCGI, то ли сам Chrome считают, что инициировать прием-передачу тела ответа, которое содержит всего-навсего один байт — слишком расточительно. Поэтому нужно предварить всю операцию дополнительными байтами. Нужно договориться, скажем, что первые 20 пробелов вообще ничего не должны означать.


    Можно бесконечно смотреть на огонь, воду и то как PHP-разработчики решают проблемы. Вы сделали очень грязный хак для вашей пары браузер-вебсервер, но вы точно не решили проблему, ведь в даже не знаете, что ее вызвало. Что если в каком-то другом случае 20ти пробелов недостаточно?


    1. TimsTims
      30.04.2015 20:05

      Присоединяюсь и добавлю, что хак в 20 байтов связан с наполнителем буфера, который помимо прочего содержит еще и заголовки сервера. т.е. мало того, что буфер у всех разный, так еще всё будет зависеть от количества заголовков. Автор, проведите эксперимент: добавьте еще пару лишних хэдеров, и посмотрите, нужны ли 20 пробелов. Результат в студию


    1. bBars Автор
      30.04.2015 23:05

      Не знаю — я так и написал.
      Эксперимент с дополнительными хедерами провалился: они ничего не дают.

      onreadystatechange срабатывает, но в responseText — пусто. Срабатывает столько раз, сколько пришло байт, но тело ответа пустое, а потом, когда тело становится длиннее волшебного числа 8 (в моем случае на самом деле не 20) — responseText моментально преобразуется в строку из 8 байт.

      У меня были догадки на этот счет, которые упираются в Apache и Nagle. Но после наблюдений они разошлись с догадками и я не стал умничать и вводить читателя в заблуждение, а спросил.

      Быть может, у вас есть точный ответ?


      1. defuz
        30.04.2015 23:51

        Точного ответа у меня нет, я могу только гадать. 8 байт натолкнули меня на мыcль: может стоит указать в хедерах явно кодировку ascii. Еще попробуйте поставить `Content-Type: application/octet-stream`.