Как я могу закрыть соединение рано?

Я пытаюсь выполнить вызов AJAX (через JQuery), который инициирует довольно длительный процесс. Я хотел бы, чтобы script просто отправил ответ, указывающий, что процесс запущен, но 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( '[email protected]', "okay I'm done", 'Yup, all done.' );

?>

Ответ 1

На следующей странице руководства PHP (включая примечания пользователя) предлагается несколько инструкций о том, как закрыть TCP-соединение с браузером, не заканчивая сценарий PHP:

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


OP затем подтверждает: yup, это сделало трюк: указав на примечание пользователя # 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 я 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

Необходимо отправить эти 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 конкретным script.

Подробнее см. http://www.zulius.com/how-to/close-browser-connection-continue-execution

Ответ 3

Вы можете использовать Fast-CGI с PHP-FPM для использования функции fastcgi_end_request(). Таким образом, вы можете продолжать выполнять некоторую обработку, пока ответ уже отправлен клиенту.

Здесь вы найдете это в руководстве по PHP: FastCGI Process Manager (FPM); Но эта функция специально не документирована в руководстве. Здесь выдержка из PHP-FPM: PHP FastCGI Process Manager Wiki:


fastcgi_finish_request()

Область: функция php Категория: Оптимизация

Эта функция позволяет ускорить реализацию некоторых php-запросов. Ускорение возможно, если в процессе выполнения script есть действия, которые не влияют на реакцию сервера. Например, сохранение сеанса в memcached может произойти после того, как страница была сформирована и передана на веб-сервер. fastcgi_finish_request() - это функция php, которая останавливает вывод ответа. Веб-сервер сразу начинает передавать ответ "медленно и грустно" клиенту, и php в то же время может делать много полезных вещей в контексте запроса, таких как сохранение сеанса, преобразование загруженного видео, обработка всех видов статистики и т.д.

fastcgi_finish_request() может вызывать выполнение функции выключения.

Ответ 4

Полная версия:

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();

Ответ 5

Предполагая, что у вас есть сервер 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. Он будет ping 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 script:

php -n -f largejob.php > dump.txt &

Надеюсь, это поможет!

Ответ 6

Здесь приведена модификация кода Timbo, которая работает с сжатием 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 ********/

Ответ 7

Лучшим решением является разветвление фонового процесса. Это довольно прямо на unix/linux:

<?php
echo "We'll email you as soon as this is done.";
system("php somestuff.php [email protected] >/dev/null &");
?>

Вы должны посмотреть на этот вопрос для более удобных примеров:

PHP выполняет фоновый процесс

Ответ 8

Я использую общий хост, а fastcgi_finish_request - это установка для полного завершения сценариев. Мне не нравится решение connection: close. С его помощью создается отдельное соединение для последующих запросов, стоимость дополнительных ресурсов сервера. Я прочитал Transfer-Encoding: cunked статью Википедии и узнал, что 0\r\n\r\n завершает ответ. Я не тестировал это в разных версиях браузеров и устройствах, но он работает во всех четырех моих текущих браузерах.

// 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);
}

Ответ 9

Вы можете попытаться выполнить многопоточность.

вы можете взломать script, который делает системный вызов (используя shell_exec), который вызывает двоичный код php с помощью script для выполнения вашей работы в качестве параметра. Но я не думаю, что это самый безопасный способ. Может быть, вы можете взломать материал, chrooting php-процесс и другие вещи.

Кроме того, существует класс в phpclasses, который делает это http://www.phpclasses.org/browse/package/3953.html. Но я не знаю специфики реализации

Ответ 10

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.

Ответ 11

Ваша проблема может быть решена путем выполнения параллельного программирования в php. Я задал вопрос об этом несколько недель назад здесь: Как использовать многопоточность в PHP-приложениях

И получил отличные ответы. Особенно мне понравилось. Автор сделал ссылку

Ответ 12

Ответ Joeri Sebrechts близок, но он уничтожает любой существующий контент, который может быть буферизирован, прежде чем вы хотите отключиться. Он не называет ignore_user_abort правильно, позволяя script завершаться преждевременно. ответ дийизма хорош, но не является общеприменимым. Например. у человека может быть больше или меньше выходных буферов, которые этот ответ не обрабатывает, поэтому он может просто не работать в вашей ситуации, и вы не будете знать почему.

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

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();
}

Если вам нужна дополнительная память, выделите ее перед вызовом этой функции.

Ответ 13

Примечание для пользователей 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, есть еще одно решение... вы можете заставить этот буфер сбрасывать.

Приведенный ниже код делает это, основываясь на решении, предложенном Джори Себрехтом:

<?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 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 Professional x64

Пример виртуального хоста

<VirtualHost *:80>
    DocumentRoot "d:/wamp/www/example"
    ServerName example.local

    FcgidOutputBufferSize 0

    <Directory "d:/wamp/www/example">
        Require all granted
    </Directory>
</VirtualHost>

Ответ 14

это сработало для меня

//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);

Ответ 15

Хорошо, так что в основном способ 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 на метод обратного вызова, определенный в вашей основной настройке script, чтобы получать данные и действовать на них. Надеюсь это поможет. Привет, Морган.

Ответ 16

Альтернативным решением является добавление задания в очередь и создание cron script, который проверяет новые задания и запускает их.

Мне пришлось сделать это так недавно, чтобы обойти ограничения, наложенные общим хостом - exec() et al был отключен для PHP, запущенного веб-сервером, но мог работать в оболочке script.

Ответ 17

Если функция flush() не работает. Вы должны установить следующие параметры в php.ini, например:

output_buffering = Off  
zlib.output_compression = Off  

Ответ 18

Последнее рабочее решение

    // 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

Ответ 19

Попробовав много разных решений из этого потока (после того, как никто из них не работал для меня), я нашел решение на официальной странице 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();
    }
}