Как мне закрыть соединение раньше времени?

99

Я пытаюсь выполнить вызов AJAX (через JQuery), который инициирует довольно длительный процесс. Я бы хотел, чтобы сценарий просто отправил ответ, указывающий на то, что процесс запущен, но JQuery не вернет ответ, пока сценарий PHP не будет запущен.

Я пробовал это с заголовком «закрыть» (см. Ниже), а также с буферизацией вывода; ни один из них не работает. Есть догадки? или это то, что мне нужно сделать в 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.' );

?>
Eric_WVGG
источник
вы очистили буфер вывода с помощью ob_flush (), и это не сработало?
Винко Врсалович

Ответы:

89

На следующей странице руководства 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 связал еще две пользовательские заметки, которые были дополнением к предыдущей:

Йери Себрехтс
источник
7
да, это сработало
Eric_WVGG
1
Автор и @Timbo White, можно ли закрыть соединение раньше времени, не зная размера содержимого? IE, без необходимости захватывать контент до закрытия.
skibulk
3
Хакеры и дрянные веб-браузеры по-прежнему могут игнорировать HTTP-заголовок закрытия соединения и получать остальную часть вывода ... убедитесь, что то, что будет дальше, нечувствительно. возможно, ob_start (); подавить все: p
hanshenrik 02
3
Добавление fastcgi_finish_request (); было сказано, чтобы успешно закрыть соединение, когда вышеперечисленное не работает. Однако в моем случае это помешало моему сценарию продолжить выполнение, поэтому используйте его с осторожностью.
Eric Dubé
@RichardSmith Поскольку Connection: closeзаголовок может быть перезаписан другим программным обеспечением в стеке, например, обратным прокси в случае CGI (я наблюдал такое поведение с nginx). См. Ответ @hanshenrik по этому поводу. В целом Connection: closeвыполняется на стороне клиента и не может рассматриваться как ответ на этот вопрос. Соединение должно быть закрыто со стороны сервера .
7heo.tk
56

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

Тимбо Уайт
источник
16
Если ваш сервер сжимает вывод, вы можете отключить его с помощью header("Content-Encoding: none\r\n");этого параметра, чтобы apache не сжимал его.
GDmac
1
@GDmac спасибо! Некоторое время я не мог заставить это работать, но отключение сжатия помогло.
Reactgular
Это ob_flush()не обязательно, и фактически вызывает уведомление failed to flush buffer. Я вынул его, и это отлично сработало.
Леви
2
Я обнаружил, что ob_flush()линия была необходима.
Deebster 04
21

Вы можете использовать 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() может вызывать выполнение функции выключения.


Примечание: fastcgi_finish_request() есть причуда , где вызовы flush, printили echoбудут прекратить сценарий рано.

Чтобы избежать этой проблемы, вы можете позвонить ignore_user_abort(true)прямо до или после fastcgi_finish_requestзвонка:

ignore_user_abort(true);
fastcgi_finish_request();
Йоррит Шипперс
источник
3
ЭТО АКТУАЛЬНЫЙ ОТВЕТ!
Кирилл Титов
2
если вы используете php-fpm - просто используйте эту функцию - забудьте о заголовках и всем остальном. Сэкономил мне столько времени!
Росс
17

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

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();
дийизм
источник
полный в каком смысле? Какая проблема потребовала от вас выполнить сценарий принятых ответов (какой?) И какие из ваших различий в конфигурации сделали это необходимым?
hakre
4
эта строка: заголовок ("Content-Encoding: none"); -> очень важно.
Bobby Tables
2
Спасибо, это единственное рабочее решение на этой странице. Это следует принять как ответ.
6

Лучшее решение - разветвить фоновый процесс. В unix / linux это довольно просто:

<?php
echo "We'll email you as soon as this is done.";
system("php somestuff.php dude@thatplace.com >/dev/null &");
?>

Для получения лучших примеров вам следует взглянуть на этот вопрос:

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

Салман А
источник
4

Предполагая, что у вас есть сервер 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 &

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

Лиам
источник
4

Вот модификация кода Тимбо, которая работает со сжатием 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 ********/
Эндрю Винтер
источник
ВЫ БОГ. Я работал 2 дня, чтобы разобраться в этом. Он работал на моем локальном разработчике, но не на хосте. Меня обливали из шланга. ТЫ СПАС МЕНЯ. СПАСИБО!!!!
Чад Колдуэлл,
3

Я нахожусь на общем хосте и 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);
}
скибулк
источник
Благодаря вашему хорошему ответу я понял, насколько глупо (и ненужно) использовать connection: close. Думаю, некоторые не знакомы с основами своего сервера.
Джастин
@Justin Я написал это очень давно. Посмотрев на это еще раз, я должен отметить, что может потребоваться увеличение фрагментов до 4 КБ. Я, кажется, помню, что некоторые серверы не сбрасываются, пока не достигнут этого минимума.
skibulk
2

Вы можете попробовать сделать многопоточность.

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

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

паан
источник
А если вы не хотите ждать завершения процесса, используйте &символ, чтобы запустить процесс в фоновом режиме.
Лиам,
2

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.
ФлуоресцентныйЗеленый5
источник
1

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

И получил отличные ответы. Одно мне особенно понравилось. Автор сослался на учебник « Простая параллельная обработка в PHP» (сентябрь 2008 г .; автор johnlim), который действительно может очень хорошо решить вашу проблему, поскольку я уже использовал его для решения аналогичной проблемы, возникшей пару дней назад.

Стив Оббайи
источник
1

Ответ Джоэри Себрехтса близок, но он уничтожает любой существующий контент, который может быть помещен в буфер, прежде чем вы захотите отключиться. Он не вызывается 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();
}

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

Вальф
источник
1

Примечание для пользователей mod_fcgid (пожалуйста, используйте на свой страх и риск).

Быстрое решение

Принятый ответ Джоэри Себрехтса действительно работает. Однако, если вы используете mod_fcgid, вы можете обнаружить, что это решение не работает само по себе. Другими словами, когда вызывается функция очистки, соединение с клиентом не закрывается.

FcgidOutputBufferSizeПараметр конфигурации mod_fcgid может быть виноват. Я нашел этот совет в:

  1. этот ответ Трэверс Картер и
  2. это сообщение в блоге Seumas Mackinnon .

Прочитав вышесказанное, вы можете прийти к выводу, что быстрым решением было бы добавить строку (см. «Пример виртуального хоста» в конце):

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'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, без защиты потоков
  • 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>
Тасос
источник
Я пробовал много решений. И это единственное решение, которое у меня работает с mod_fcgid.
Tsounabe
1

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

//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);
Виллием
источник
0

Хорошо, так что в основном способ, которым jQuery выполняет запрос XHR, даже метод ob_flush не будет работать, потому что вы не можете запустить функцию для каждого onreadystatechange. jQuery проверяет состояние, а затем выбирает необходимые действия (завершение, ошибка, успех, тайм-аут). И хотя мне не удалось найти ссылку, я помню, что слышал, что это работает не со всеми реализациями XHR. Метод, который, как я считаю, должен работать для вас, представляет собой нечто среднее между ob_flush и опросом с постоянным кадром.

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

Морган ARR Аллен
источник
0

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

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

Оле Хельгесен
источник
0

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

output_buffering = Off  
zlib.output_compression = Off  
Нир О.
источник
0

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

    // 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
Судхаршана С.Л. Шрихари
источник
0

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