Як я можу завчасно закрити зв’язок?


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.' );

?>

ви промили свій вихідний буфер з ob_flush (), і він не працював?
Вінко Врсалович

Відповіді:


87

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

Нібито для цього потрібно трохи більше, ніж надсилання закритого заголовка.


Тоді ОП підтверджує: так , це зробило трюк: вказуючи на копію користувачу-примітки № 71172 (листопад 2006 р.) :

Закриття підключення до веб-переглядача користувачів під час збереження запущеного сценарію php було проблемою з [PHP] 4.1, коли поведінку register_shutdown_function()було змінено, щоб воно не закривало автоматично з'єднання користувачів.

STS на пошті точка 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 року, у відповідній відповіді « Арктичний вогонь» потім пов’язав ще дві нотатки користувачів, які були подальшими запитами до вищезгаданого:


7
Так, це зробило трюк: php.net/manual/en/features.connection-handling.php#71172
Eric_WVGG

1
Автор та @Timbo White, чи можна припинити зв’язок рано, не знаючи розміру вмісту? IE, не потребуючи захоплення контенту перед закриттям.
skibulk

3
Хакери та шахрайські веб-браузери все ще можуть ігнорувати закритий HTTP-заголовок підключення та отримувати решту результатів. можливо, ob_start (); придушити все: p
hanshenrik

3
Додавання fastcgi_finish_request (); як стверджується, успішно припинити з'єднання, коли вищезгадане не працює. Однак у моєму випадку це заважало моєму сценарію продовжувати виконувати, тому використовуйте з обережністю.
Ерік Дубе

@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), це не працюватиме, оскільки фактичний розмір виводу змінюється, а довжина вмісту вже не точна. Вимкнути стиснення gzip певного сценарію.

Для отримання більш детальної інформації відвідайте http://www.zulius.com/how-to/close-browser-connection-continue-execution


16
Якщо ваш сервер стискає вихід, ви можете відключити його header("Content-Encoding: none\r\n");таким чином, щоб Apache не стискав його.
GDmac

1
@GDmac дякую! Я не міг змусити це працювати деякий час, але відключення стиснення зробило свою справу.
Реакційний

Це ob_flush()не є необхідним і насправді викликає повідомлення failed to flush buffer. Я вийняв це, і це чудово спрацювало.
Леві

2
Я виявив , що ob_flush()лінія була необхідна.
Дібстер

21

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

Ви знайдете це в посібнику PHP тут: FastCGI Process Manager (FPM) ; Але ця функція конкретно не надана в документі. Ось уривок з PHP-FPM: PHP FastCGI Manager Manager Wiki :


fastcgi_finish_request ()

Область застосування: функція php

Категорія: Оптимізація

Ця функція дозволяє пришвидшити реалізацію деяких php-запитів. Прискорення можливе, коли в процесі виконання сценарію є дії, які не впливають на реакцію сервера. Наприклад, збереження сеансу в запам’ятованому стані може відбутися після того, як сторінка сформована і передана веб-серверу.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();

в якому сенсі? Яка проблема вимагала, щоб ви заповнили скрипт прийнятих відповідей (який?) І які з ваших відмінностей у конфігурації зробили це необхідним?
хакре

4
цей рядок: заголовок ("Content-Encoding: none"); -> дуже важливо.
Столи Бобі

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-сервер та кореневий доступ, спробуйте це. Це найпростіше рішення, яке я знайшов.

Створіть новий каталог для наступних файлів і надайте йому повні дозволи. (Ми можемо зробити це більш безпечним пізніше.)

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

Завдяки вашій гарній відповіді я зрозумів, наскільки дурним (і непотрібним) є використання з'єднання: близько. Я думаю, деякі не знайомі з гайками та болтами їх сервера.
Джастін

@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.

1

Вашу проблему можна вирішити, виконавши паралельне програмування в php. Я поставив питання про це кілька тижнів тому тут: Як можна використовувати багатопотокові нитки в PHP-програмах

І отримали чудові відповіді. Мені він особливо сподобався. Автор написав посилання на Easy Parallel Processing в PHP (вересень 2008; johnlim) підручник, який насправді може дуже добре вирішити вашу проблему, оскільки я вже використовував це для вирішення подібної проблеми, яка виникла пару днів тому.


1

Відповідь Джорі Себрехтса близька, але вона руйнує будь-який існуючий вміст, який може бути завантажений до того, як ви захочете від’єднатися. Він не викликає ignore_user_abortналежним чином, що дозволяє сценарію передчасно припинити роботу. відповідь діаїзму хороша, але не застосовується в загальних рисах. Наприклад, у людини може бути більше або менше вихідних буферів, на які відповідь не відповідає, тому він може просто не працювати у вашій ситуації, і ви не знаєте чому.

Ця функція дозволяє відключити будь-який час (доки заголовки ще не надіслані) та зберігає створений вами вміст. Додатковий час обробки за замовчуванням необмежений.

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. ця відповідь Travers Carter та
  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 Professional 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.
Цунабе

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 не працюватиме, оскільки ви не в змозі запустити функцію на кожній зміненій державі. jQuery перевіряє стан, а потім вибирає належні дії, які слід вжити (завершення, помилка, успіх, час очікування). І хоча мені не вдалося знайти посилання, я пам'ятаю, почувши, що це не працює з усіма реалізаціями XHR. Метод, який, на мою думку, повинен працювати для вас - це перехрес між об'ємним і вічним опитуванням кадрів.

<?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 на метод зворотного виклику, визначений у вашій головній настройці сценарію, щоб отримувати дані та діяти на них. Сподіваюся, це допомагає. Ура, Морган.


0

Альтернативне рішення - додати завдання до черги та зробити cron script, який перевіряє наявність нових завдань та виконує їх.

Нещодавно мені довелося це зробити, щоб обійти обмеження, накладені спільним хостом - 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();
    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.