Закрыть соединение рано
Я пытаюсь сделать AJAX-вызов (через JQuery), который инициирует довольно длительный процесс. Мне бы хотелось, чтобы скрипт просто отправлял ответ, указывающий, что процесс запущен, но JQuery не вернет ответ, пока не завершится выполнение скрипта PHP.
Я пробовал это с заголовком "close" (ниже), а также с буферизацией вывода; ни один из них не работает. Есть догадки? или это то, что мне нужно сделать в JQuery?
<?php
echo( "We'll email you as soon as this is done." );
header( "Connection: Close" );
// do some stuff that will take a while
mail( 'dude@thatplace.com', "okay I'm done", 'Yup, all done.' );
?>
20 ответов
На следующей странице справки по PHP (включая примечания пользователя) предлагается несколько инструкций о том, как закрыть TCP-соединение с браузером без завершения сценария PHP:
Предположительно, это требует немного больше, чем отправка закрытого заголовка.
Затем OP подтверждает: да, это сработало : указав на примечание пользователя № 71172 (ноябрь 2006 г.), скопированное сюда:
Закрытие соединения с браузером пользователей при сохранении работоспособности вашего php-скрипта было проблемой с [PHP] 4.1, когда поведение
register_shutdown_function()
был изменен так, что он не будет автоматически закрывать соединение пользователей.sts at mail dot xubion dot hu Выложено оригинальное решение:
<?php header("Connection: close"); ob_start(); phpinfo(); $size = ob_get_length(); header("Content-Length: $size"); ob_end_flush(); flush(); sleep(13); error_log("do something in the background"); ?>
Который работает нормально, пока вы не замените
phpinfo()
заecho('text I want user to see');
в этом случае заголовки никогда не отправляются!Решение состоит в том, чтобы явно отключить выходную буферизацию и очистить буфер перед отправкой информации вашего заголовка. Пример:
<?php ob_end_clean(); header("Connection: close"); ignore_user_abort(true); // just to be safe ob_start(); echo('Text the user will see'); $size = ob_get_length(); header("Content-Length: $size"); ob_end_flush(); // Strange behaviour, will not work flush(); // Unless both are called ! // Do processing here sleep(30); echo('Text user will never see'); ?>
Просто потратил 3 часа, пытаясь выяснить это, надеюсь, это поможет кому-то:)
Проверено в:
- IE 7.5730.11
- Mozilla Firefox 1.81
Позже, в июле 2010 года, в связанном ответе Arctic Fire затем связал еще две заметки пользователя, которые были продолжением предыдущей:
Необходимо отправить эти 2 заголовка:
Connection: close
Content-Length: n (n = size of output in bytes )
Так как вам нужно знать размер вашего вывода, вам нужно будет буферизовать ваш вывод, а затем сбросить его в браузер:
// buffer all upcoming output
ob_start();
echo "We'll email you as soon as this is done.";
// get the size of the output
$size = ob_get_length();
// send headers to tell the browser to close the connection
header("Content-Length: $size");
header('Connection: close');
// flush all output
ob_end_flush();
ob_flush();
flush();
// if you're using sessions, this prevents subsequent requests
// from hanging while the background process executes
if (session_id()) session_write_close();
/******** background process starts here ********/
Кроме того, если ваш веб-сервер использует автоматическое сжатие GZIP на выходе (например, Apache с mod_deflate), это не будет работать, потому что реальный размер вывода изменяется, а Content-Length больше не является точным. Отключить сжатие GZIP конкретного скрипта.
Для получения более подробной информации, посетите http://www.zulius.com/how-to/close-browser-connection-continue-execution/
Вы можете использовать Fast-CGI с PHP-FPM, чтобы использовать fastcgi_end_request()
функция Таким образом, вы можете продолжать выполнять некоторую обработку, пока ответ уже отправлен клиенту.
Вы найдете это в руководстве по PHP здесь: FastCGI Process Manager (FPM); Но эта функция специально не описана в руководстве. Вот выдержка из PHP-FPM: PHP FastCGI Process Manager Wiki:
fastcgi_finish_request ()
Область применения: функция php Категория: Оптимизация
Эта функция позволяет ускорить реализацию некоторых php-запросов. Ускорение возможно, когда в процессе выполнения скрипта есть действия, которые не влияют на реакцию сервера. Например, сохранение сеанса в memcached может произойти после того, как страница сформирована и передана на веб-сервер. fastcgi_finish_request()
функция php, которая останавливает вывод ответа Веб-сервер сразу начинает передавать ответ "медленно и грустно" клиенту, и в то же время php может сделать много полезных вещей в контексте запроса, таких как сохранение сеанса, преобразование загруженного видео, обработка всех видов. статистики и т. д.
fastcgi_finish_request()
может вызвать выполнение функции выключения.
Полная версия:
ignore_user_abort(true);//avoid apache to kill the php running
ob_start();//start buffer output
echo "show something to user";
session_write_close();//close session file on server side to avoid blocking other requests
header("Content-Encoding: none");//send header to avoid the browser side to take content as gzip format
header("Content-Length: ".ob_get_length());//send length header
header("Connection: close");//or redirect to some url: header('Location: http://www.google.com');
ob_end_flush();flush();//really send content, can't change the order:1.ob buffer to normal buffer, 2.normal buffer to output
//continue do something on server side
ob_start();
sleep(5);//the user won't wait for the 5 seconds
echo 'for diyism';//user can't see this
file_put_contents('/tmp/process.log', ob_get_contents());
ob_end_clean();
Лучшее решение - развить фоновый процесс. Это довольно просто на Unix/ Linux:
<?php
echo "We'll email you as soon as this is done.";
system("php somestuff.php dude@thatplace.com >/dev/null &");
?>
Вы должны посмотреть на этот вопрос для лучших примеров:
Предполагая, что у вас есть сервер Linux и root-доступ, попробуйте это. Это самое простое решение, которое я нашел.
Создайте новый каталог для следующих файлов и предоставьте ему полные права. (Мы можем сделать его более безопасным позже.)
mkdir test
chmod -R 777 test
cd test
Поместите это в файл с именем bgping
,
echo starting bgping
ping -c 15 www.google.com > dump.txt &
echo ending bgping
Обратите внимание &
, Команда ping будет выполняться в фоновом режиме, в то время как текущий процесс переходит к команде echo. Он будет пинговать www.google.com 15 раз, что займет около 15 секунд.
Сделайте это исполняемым.
chmod 777 bgping
Поместите это в файл с именем bgtest.php
,
<?php
echo "start bgtest.php\n";
exec('./bgping', $output, $result)."\n";
echo "output:".print_r($output,true)."\n";
echo "result:".print_r($result,true)."\n";
echo "end bgtest.php\n";
?>
Когда вы запрашиваете bgtest.php в своем браузере, вы должны быстро получить следующий ответ, не дожидаясь около 15 секунд до завершения команды ping.
start bgtest.php
output:Array
(
[0] => starting bgping
[1] => ending bgping
)
result:0
end bgtest.php
Теперь команда ping должна быть запущена на сервере. Вместо команды ping вы можете запустить скрипт PHP:
php -n -f largejob.php > dump.txt &
Надеюсь это поможет!
Вот модификация кода Тимбо, которая работает со сжатием gzip.
// buffer all upcoming output
if(!ob_start("ob_gzhandler")){
define('NO_GZ_BUFFER', true);
ob_start();
}
echo "We'll email you as soon as this is done.";
//Flush here before getting content length if ob_gzhandler was used.
if(!defined('NO_GZ_BUFFER')){
ob_end_flush();
}
// get the size of the output
$size = ob_get_length();
// send headers to tell the browser to close the connection
header("Content-Length: $size");
header('Connection: close');
// flush all output
ob_end_flush();
ob_flush();
flush();
// if you're using sessions, this prevents subsequent requests
// from hanging while the background process executes
if (session_id()) session_write_close();
/******** background process starts here ********/
Я на общем хосте и fastcgi_finish_request
это настройка для полного выхода из скриптов. Мне не нравится connection: close
решение тоже. Его использование вызывает отдельное соединение для последующих запросов, что требует дополнительных ресурсов сервера. Я прочитал Transfer-Encoding: cunked
Статья в Википедии и узнал, что 0\r\n\r\n
завершает ответ. Я не проверил это тщательно на всех версиях браузеров и устройствах, но он работает на всех 4 моих текущих браузерах.
// Disable automatic compression
// @ini_set('zlib.output_compression', 'Off');
// @ini_set('output_buffering', 'Off');
// @ini_set('output_handler', '');
// @apache_setenv('no-gzip', 1);
// Chunked Transfer-Encoding & Gzip Content-Encoding
function ob_chunked_gzhandler($buffer, $phase) {
if (!headers_sent()) header('Transfer-Encoding: chunked');
$buffer = ob_gzhandler($buffer, $phase);
return dechex(strlen($buffer))."\r\n$buffer\r\n";
}
ob_start('ob_chunked_gzhandler');
// First Chunk
echo "Hello World";
ob_flush();
// Second Chunk
echo ", Grand World";
ob_flush();
ob_end_clean();
// Terminating Chunk
echo "\x30\r\n\r\n";
ob_flush();
flush();
// Post Processing should not be displayed
for($i=0; $i<10; $i++) {
print("Post-Processing");
sleep(1);
}
Вы можете попробовать сделать многопоточность.
Вы можете создать сценарий, который выполняет системный вызов (используя shell_exec), который вызывает двоичный файл php со сценарием, чтобы выполнить вашу работу в качестве параметра. Но я не думаю, что это самый безопасный способ. Может быть, вы можете улучшить вещи, используя chroot-процесс php и другие вещи
Кроме того, в phpclasses есть класс, который делает это http://www.phpclasses.org/browse/package/3953.html. Но я не знаю специфики реализации
TL;DR Ответ:
ignore_user_abort(true); //Safety measure so that the user doesn't stop the script too early.
$content = 'Hello World!'; //The content that will be sent to the browser.
header('Content-Length: ' . strlen($content)); //The browser will close the connection when the size of the content reaches "Content-Length", in this case, immediately.
ob_start(); //Content past this point...
echo $content;
//...will be sent to the browser (the output buffer gets flushed) when this code executes.
ob_end_flush();
ob_flush();
flush();
if(session_id())
{
session_write_close(); //Closes writing to the output buffer.
}
//Anything past this point will be ran without involving the browser.
Функция Ответ:
ignore_user_abort(true);
function sendAndAbort($content)
{
header('Content-Length: ' . strlen($content));
ob_start();
echo $content;
ob_end_flush();
ob_flush();
flush();
}
sendAndAbort('Hello World!');
//Anything past this point will be ran without involving the browser.
Ваша проблема может быть решена путем параллельного программирования в php. Я задал вопрос об этом несколько недель назад здесь: как можно использовать многопоточность в приложениях PHP
И получил отличные ответы. Особенно мне понравился один. Автор ссылался на учебник " Простая параллельная обработка в PHP" (сентябрь 2008 г.; johnlim), который действительно может очень хорошо решить вашу проблему, так как я уже использовал его для решения аналогичной проблемы, которая возникла пару дней назад.
Ответ Joeri Sebrechts близок, но он уничтожает любой существующий контент, который может быть помещен в буфер, прежде чем вы захотите отключиться. Не звонит ignore_user_abort
правильно, позволяя сценарию завершаться преждевременно. ответ diyism хорош, но не применим вообще. Например, у человека может быть больше или меньше выходных буферов, которые этот ответ не обрабатывает, поэтому он может просто не работать в вашей ситуации, и вы не будете знать, почему.
Эта функция позволяет в любой момент отключиться (если заголовки еще не отправлены) и сохраняет созданный вами контент. Дополнительное время обработки не ограничено по умолчанию.
function disconnect_continue_processing($time_limit = null) {
ignore_user_abort(true);
session_write_close();
set_time_limit((int) $time_limit);//defaults to no limit
while (ob_get_level() > 1) {//only keep the last buffer if nested
ob_end_flush();
}
$last_buffer = ob_get_level();
$length = $last_buffer ? ob_get_length() : 0;
header("Content-Length: $length");
header('Connection: close');
if ($last_buffer) {
ob_end_flush();
}
flush();
}
Если вам нужна дополнительная память, выделите ее перед вызовом этой функции.
Это сработало для меня
//avoid apache to kill the php running
ignore_user_abort(true);
//start buffer output
ob_start();
echo "show something to user1";
//close session file on server side to avoid blocking other requests
session_write_close();
//send length header
header("Content-Length: ".ob_get_length());
header("Connection: close");
//really send content, can't change the order:
//1.ob buffer to normal buffer,
//2.normal buffer to output
ob_end_flush();
flush();
//continue do something on server side
ob_start();
//replace it with the background task
sleep(20);
Примечание для пользователей mod_fcgid (пожалуйста, используйте на свой страх и риск).
Быстрое решение
Принятый ответ Joeri Sebrechts действительно функциональный. Однако, если вы используете mod_fcgid, вы можете обнаружить, что это решение не работает само по себе. Другими словами, когда вызывается функция сброса, соединение с клиентом не закрывается.
FcgidOutputBufferSize
может быть виноват параметр конфигурации mod_fcgid. Я нашел этот совет в:
После прочтения вышеизложенного вы можете прийти к выводу, что быстрым решением будет добавить строку (см. "Пример виртуального хоста" в конце):
FcgidOutputBufferSize 0
в файле конфигурации Apache (например, httpd.conf), файле конфигурации FCGI (например, fcgid.conf) или в файле виртуальных хостов (например, httpd-vhosts.conf).
В (1) выше упоминается переменная с именем "OutputBufferSize". Это старое имя
FcgidOutputBufferSize
упоминается в (2) (см. примечания по обновлению на веб-странице Apache для mod_fcgid).
Детали и второе решение
Приведенное выше решение отключает буферизацию, выполняемую mod_fcgid, либо для всего сервера, либо для конкретного виртуального хоста. Это может привести к снижению производительности вашего веб-сайта. С другой стороны, это может быть совсем не так, поскольку PHP выполняет буферизацию самостоятельно.
Если вы не хотите отключать буферизацию mod_fcgid, есть другое решение... вы можете принудительно сбросить этот буфер.
Приведенный ниже код делает именно это, опираясь на решение, предложенное Joeri Sebrechts:
<?php
ob_end_clean();
header("Connection: close");
ignore_user_abort(true); // just to be safe
ob_start();
echo('Text the user will see');
echo(str_repeat(' ', 65537)); // [+] Line added: Fill up mod_fcgi's buffer.
$size = ob_get_length();
header("Content-Length: $size");
ob_end_flush(); // Strange behaviour, will not work
flush(); // Unless both are called !
// Do processing here
sleep(30);
echo('Text user will never see');
?>
По сути дела, добавленная строка кода заполняет буфер mod_fcgi, заставляя его очищаться. Число "65537" было выбрано потому, что значение по умолчанию FcgidOutputBufferSize
переменная "65536", как указано на веб-странице Apache для соответствующей директивы. Следовательно, вам может потребоваться изменить это значение соответствующим образом, если в вашей среде установлено другое значение.
Моя среда
- WampServer 2.5
- Apache 2.4.9
- PHP 5.5.19 VC11, x86, Non Thread Safe
- mod_fcgid / 2.3.9
- Windows 7 Профессиональная x64
Пример виртуального хоста
<VirtualHost *:80>
DocumentRoot "d:/wamp/www/example"
ServerName example.local
FcgidOutputBufferSize 0
<Directory "d:/wamp/www/example">
Require all granted
</Directory>
</VirtualHost>
Если flush()
функция не работает. Вы должны установить следующие параметры в php.ini, например:
output_buffering = Off
zlib.output_compression = Off
Последнее рабочее решение
// client can see outputs if any
ignore_user_abort(true);
ob_start();
echo "success";
$buffer_size = ob_get_length();
session_write_close();
header("Content-Encoding: none");
header("Content-Length: $buffer_size");
header("Connection: close");
ob_end_flush();
ob_flush();
flush();
sleep(2);
ob_start();
// client cannot see the result of code below
Итак, в основном способ, которым jQuery выполняет XHR-запрос, даже метод ob_flush не будет работать, потому что вы не можете запустить функцию для каждого onreadystatechange. JQuery проверяет состояние, а затем выбирает правильные действия (завершено, ошибка, успех, время ожидания). И хотя мне не удалось найти ссылку, я вспоминаю, что слышал, что это не работает со всеми реализациями XHR. Метод, который, я считаю, должен работать для вас, - это нечто среднее между опросом ob_flush и forever-frame.
<?php
function wrap($str)
{
return "<script>{$str}</script>";
};
ob_start(); // begin buffering output
echo wrap("console.log('test1');");
ob_flush(); // push current buffer
flush(); // this flush actually pushed to the browser
$t = time();
while($t > (time() - 3)) {} // wait 3 seconds
echo wrap("console.log('test2');");
?>
<html>
<body>
<iframe src="ob.php"></iframe>
</body>
</html>
А поскольку сценарии выполняются встроенными, так как буферы сбрасываются, вы получаете выполнение. Чтобы сделать это полезным, измените console.log на метод обратного вызова, определенный в настройках основного сценария, чтобы получать данные и действовать на них. Надеюсь это поможет. Ура, Морган.
Не удалось заставить что-либо из вышеперечисленного работать с IIS, но:
При всех своих ограничениях на помощь приходит встроенный веб-сервер PHP -S.
Сценарий вызывающего абонента (IIS)
//limit of length required!
<?php
$s = file_get_contents('http://127.0.0.1:8080/test.php',false,null,0,10);
echo $s;
Рабочий скрипт (встроенный в webserber @ 8080 — остерегайтесь одиночного потока):
ob_end_clean();
header("Connection: close");
ignore_user_abort(true);
ob_start();
echo 'Text the user will see';
$size = ob_get_length();
header("Content-Length: $size");
ob_end_flush(); // All output buffers must be flushed here
flush(); // Force output to client
// Do processing here
sleep(5);
file_put_contents('ts.txt',date('H:i:s',time()));
//echo('Text user will never see');
Достаточно некрасиво? :)
Попробовав множество различных решений из этой темы (после того, как ни одно из них не помогло мне), я нашел решение на официальной странице PHP.net:
function sendResponse($response) {
ob_end_clean();
header("Connection: close\r\n");
header("Content-Encoding: none\r\n");
ignore_user_abort(true);
ob_start();
echo $response; // Actual response that will be sent to the user
$size = ob_get_length();
header("Content-Length: $size");
ob_end_flush();
flush();
if (ob_get_contents()) {
ob_end_clean();
}
}
Альтернативное решение - добавить задание в очередь и создать скрипт cron, который проверяет наличие новых заданий и запускает их.
Недавно мне пришлось сделать это таким образом, чтобы обойти ограничения, налагаемые общим хостом - exec() и другие были отключены для PHP, запускаемого веб-сервером, но могли работать в сценарии оболочки.