Bagaimana cara menutup koneksi lebih awal?


99

Saya mencoba melakukan panggilan AJAX (melalui JQuery) yang akan memulai proses yang cukup lama. Saya ingin skrip hanya mengirim respons yang menunjukkan bahwa proses telah dimulai, tetapi JQuery tidak akan mengembalikan respons sampai skrip PHP selesai dijalankan.

Saya sudah mencoba ini dengan header "tutup" (di bawah), dan juga dengan buffering keluaran; sepertinya tidak ada yang berhasil. Ada tebakan? atau apakah ini sesuatu yang perlu saya lakukan di 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.' );

?>

apakah Anda membersihkan buffer keluaran dengan ob_flush () dan tidak berhasil?
Vinko Vrsalovic

Jawaban:


87

Halaman manual PHP berikut (termasuk catatan pengguna) menyarankan beberapa petunjuk tentang cara menutup koneksi TCP ke browser tanpa mengakhiri skrip PHP:

Seharusnya itu membutuhkan lebih dari sekedar mengirimkan header yang tertutup.


OP kemudian mengonfirmasi: ya, ini melakukan trik: menunjuk ke catatan pengguna # 71172 (Nov 2006) disalin di sini:

Menutup koneksi browser pengguna sambil tetap menjalankan skrip php Anda telah menjadi masalah sejak [PHP] 4.1, ketika perilaku register_shutdown_function() diubah sehingga tidak secara otomatis menutup koneksi pengguna.

sts at mail dot xubion dot hu Memposting solusi asli:

<?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");
?>

Yang bekerja dengan baik sampai Anda mengganti phpinfo()untukecho('text I want user to see'); dalam hal header tidak pernah dikirim!

Solusinya adalah dengan secara eksplisit menonaktifkan buffering keluaran dan menghapus buffer sebelum mengirimkan informasi header Anda. Contoh:

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

Baru saja menghabiskan 3 jam mencoba mencari tahu yang satu ini, semoga ini membantu seseorang :)

Diuji di:

  • IE 7.5730.11
  • Mozilla Firefox 1.81

Kemudian pada bulan Juli 2010 dalam jawaban terkait Arctic Fire kemudian menghubungkan dua catatan pengguna lebih lanjut yang merupakan tindak lanjut dari yang di atas:



1
Penulis dan @Timbo White, Apakah mungkin untuk menutup koneksi lebih awal tanpa mengetahui ukuran konten? IE, tanpa harus menangkap konten sebelum ditutup.
skibulk

3
Peretas dan peramban web yang buruk masih dapat mengabaikan header HTTP koneksi-tutup, dan mendapatkan keluaran lainnya .. pastikan apa yang muncul selanjutnya, tidak sensitif. mungkin ob_start (); untuk menekan segalanya: p
hanshenrik

3
Menambahkan fastcgi_finish_request (); telah dikatakan berhasil menutup koneksi ketika cara di atas tidak berfungsi. Namun, dalam kasus saya itu mencegah skrip saya untuk melanjutkan eksekusi, jadi gunakan dengan hati-hati.
Eric Dubé

@RichardSmith Karena Connection: closeheader dapat ditimpa oleh perangkat lunak lain di stack, misalnya, proxy terbalik dalam kasus CGI (saya mengamati perilaku itu dengan nginx). Lihat jawaban dari @hanshenrik tentang itu. Secara umum, Connection: closedijalankan di sisi klien, dan tidak boleh dianggap sebagai jawaban atas pertanyaan ini. Koneksi harus ditutup dari sisi server .
7heo.tk

56

Anda perlu mengirim 2 header ini:

Connection: close
Content-Length: n (n = size of output in bytes )

Karena Anda perlu mengetahui ukuran output Anda, Anda perlu buffer output Anda, lalu flush ke browser:

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

Selain itu, jika server web Anda menggunakan kompresi gzip otomatis pada keluaran (mis. Apache dengan mod_deflate), ini tidak akan berfungsi karena ukuran keluaran yang sebenarnya berubah, dan Panjang Konten tidak lagi akurat. Nonaktifkan kompresi gzip pada skrip tertentu.

Untuk detail lebih lanjut, kunjungi http://www.zulius.com/how-to/close-browser-connection-continue-execution


16
Jika server Anda memampatkan keluaran, Anda dapat menonaktifkannya dengan header("Content-Encoding: none\r\n");cara itu apache tidak akan mengompresnya.
GDmac

1
@GD terima kasih! Saya tidak bisa mendapatkan ini bekerja untuk sementara waktu, tetapi menonaktifkan kompresi berhasil.
Reactgular

Ini ob_flush()tidak perlu dan sebenarnya menyebabkan pemberitahuan failed to flush buffer. Saya mengeluarkannya dan ini bekerja dengan baik.
Levi

2
Saya menemukan bahwa ob_flush()garis itu perlu.
Deebster

21

Anda dapat menggunakan Fast-CGI dengan PHP-FPM untuk menggunakan fastcgi_end_request()fungsi tersebut . Dengan cara ini, Anda dapat terus melakukan beberapa pemrosesan sementara respons telah dikirim ke klien.

Anda menemukan ini di manual PHP di sini: FastCGI Process Manager (FPM) ; Tetapi fungsi itu secara khusus tidak didokumentasikan lebih lanjut dalam manual. Berikut kutipan dari PHP-FPM: PHP FastCGI Process Manager Wiki :


fastcgi_finish_request ()

Cakupan: fungsi php

Kategori: Optimasi

Fitur ini memungkinkan Anda untuk mempercepat implementasi beberapa query php. Akselerasi dimungkinkan jika ada tindakan dalam proses eksekusi skrip yang tidak memengaruhi respons server. Misalnya, menyimpan sesi di memcache dapat terjadi setelah halaman terbentuk dan diteruskan ke server web. fastcgi_finish_request()adalah fitur php, yang menghentikan keluaran respons. Server web segera mulai mentransfer respons "perlahan dan sedih" ke klien, dan php pada saat yang sama dapat melakukan banyak hal berguna dalam konteks kueri, seperti menyimpan sesi, mengubah video yang diunduh, menangani semua jenis statistik, dll.

fastcgi_finish_request() dapat memanggil menjalankan fungsi shutdown.


Catatan: fastcgi_finish_request() memiliki kekhasan mana panggilan ke flush, printatau echoakan mengakhiri script awal.

Untuk menghindari masalah itu, Anda dapat menelepon ignore_user_abort(true)tepat sebelum atau sesudah fastcgi_finish_requestpanggilan:

ignore_user_abort(true);
fastcgi_finish_request();

3
INI ADALAH JAWABAN SEBENARNYA!
Kirill Titov

2
jika Anda menggunakan php-fpm - cukup gunakan fungsi ini - lupakan header dan yang lainnya. Menghemat waktu saya!
Ross

17

Versi lengkap:

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

lengkap dalam arti apa? Masalah mana yang mengharuskan Anda menyelesaikan skrip jawaban yang diterima (yang mana?) Dan perbedaan konfigurasi mana yang membuatnya perlu?
hakre

4
baris ini: header ("Content-Encoding: none"); -> sangat penting.
Bobby Tables

2
Terima kasih, ini adalah satu-satunya solusi yang berfungsi di halaman ini. Ini harus disetujui sebagai jawabannya.

6

Solusi yang lebih baik adalah melakukan proses latar belakang. Ini cukup mudah di unix / linux:

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

Anda harus melihat pertanyaan ini untuk contoh yang lebih baik:

PHP menjalankan proses latar belakang


4

Dengan asumsi Anda memiliki server Linux dan akses root, coba ini. Ini adalah solusi paling sederhana yang saya temukan.

Buat direktori baru untuk file berikut dan berikan izin penuh. (Kita bisa membuatnya lebih aman nanti.)

mkdir test
chmod -R 777 test
cd test

Taruh ini di file bernama bgping.

echo starting bgping
ping -c 15 www.google.com > dump.txt &
echo ending bgping

Perhatikan &. Perintah ping akan berjalan di latar belakang sementara proses saat ini beralih ke perintah echo. Ini akan melakukan ping ke www.google.com 15 kali, yang akan memakan waktu sekitar 15 detik.

Buat itu bisa dieksekusi.

chmod 777 bgping

Taruh ini di file bernama 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";

?>

Saat Anda meminta bgtest.php di browser Anda, Anda akan mendapatkan respons berikut dengan cepat, tanpa menunggu sekitar 15 detik hingga perintah ping selesai.

start bgtest.php
output:Array
(
    [0] => starting bgping
    [1] => ending bgping
)

result:0
end bgtest.php

Perintah ping sekarang harus berjalan di server. Alih-alih perintah ping, Anda dapat menjalankan skrip PHP:

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

Semoga ini membantu!


4

Berikut modifikasi kode Timbo yang berfungsi dengan kompresi 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 ********/

ANDA ADALAH ALLAH. Saya telah bekerja selama 2 hari untuk mencoba dan mencari tahu. Ini berfungsi pada pengembang lokal saya tetapi tidak pada host. Saya disemprot. KAMU MENYELAMATKANKU. TERIMA KASIH!!!!
Chad Caldwell

3

Saya menggunakan host bersama dan siap fastcgi_finish_requestuntuk keluar dari skrip sepenuhnya. Saya juga tidak suka connection: closesolusinya. Menggunakannya memaksa koneksi terpisah untuk permintaan berikutnya, dengan biaya sumber daya server tambahan. Saya membaca Transfer-Encoding: cunked Artikel Wikipedia dan mengetahui bahwa itu 0\r\n\r\nmenghentikan tanggapan. Saya belum menguji ini secara menyeluruh di seluruh versi browser dan perangkat, tetapi berfungsi di semua 4 browser saya saat ini.

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

Berkat jawaban bagus Anda, saya menyadari betapa bodoh (dan tidak perlu) menggunakan koneksi: tutup. Saya kira beberapa tidak akrab dengan mur dan baut server mereka.
Justin

@Justin Saya menulis ini sejak lama. Melihatnya lagi, saya harus mencatat bahwa mungkin perlu untuk menambah potongan ke 4KB. Sepertinya saya ingat bahwa beberapa server tidak akan disiram hingga mencapai minimum itu.
skibulk

2

Anda bisa mencoba melakukan multithreading.

Anda dapat menyiapkan skrip yang membuat panggilan sistem (menggunakan shell_exec ) yang memanggil biner php dengan skrip untuk melakukan pekerjaan Anda sebagai parameter. Tetapi menurut saya itu bukan cara yang paling aman. Mungkin Anda dapat mempererat hal-hal dengan chroot proses php dan hal lainnya

Sebagai alternatif, ada kelas di phpclass yang melakukan itu http://www.phpclasses.org/browse/package/3953.html . Tapi saya tidak tahu secara spesifik implementasinya


Dan jika Anda tidak ingin menunggu proses selesai, gunakan &karakter untuk menjalankan proses di latar belakang.
Liam

2

TL; DR Jawaban:

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.

Fungsi Jawaban:

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

Masalah Anda dapat diselesaikan dengan melakukan beberapa pemrograman paralel di php. Saya mengajukan pertanyaan tentang itu beberapa minggu yang lalu di sini: Bagaimana seseorang dapat menggunakan multi threading dalam aplikasi PHP

Dan mendapat jawaban yang bagus. Saya sangat menyukainya. Penulis membuat referensi ke tutorial Pemrosesan Paralel Mudah di PHP (Sep 2008; oleh johnlim) yang sebenarnya dapat menyelesaikan masalah Anda dengan sangat baik karena saya telah menggunakannya untuk menangani masalah serupa yang muncul beberapa hari yang lalu.


1

Jawaban Joeri Sebrechts hampir saja, tetapi itu menghancurkan semua konten yang ada yang mungkin disangga sebelum Anda ingin memutuskan sambungan. Itu tidak memanggil ignore_user_abortdengan benar, memungkinkan skrip untuk dihentikan sebelum waktunya. Jawaban diyism bagus tetapi tidak berlaku secara umum. Misalnya, seseorang mungkin memiliki buffer keluaran yang lebih besar atau lebih sedikit yang tidak ditangani oleh jawaban itu, jadi itu mungkin tidak berfungsi dalam situasi Anda dan Anda tidak akan tahu mengapa.

Fungsi ini memungkinkan Anda memutuskan sambungan kapan saja (selama header belum dikirim) dan mempertahankan konten yang telah Anda buat sejauh ini. Waktu pemrosesan tambahan tidak terbatas secara default.

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

Jika Anda juga membutuhkan memori tambahan, alokasikan sebelum memanggil fungsi ini.


1

Catatan untuk pengguna mod_fcgid (mohon, gunakan dengan resiko Anda sendiri).

Solusi Cepat

Jawaban yang diterima dari Joeri Sebrechts memang fungsional. Namun, jika Anda menggunakan mod_fcgid, Anda mungkin menemukan bahwa solusi ini tidak berfungsi dengan sendirinya. Dengan kata lain, ketika fungsi flush dipanggil, koneksi ke klien tidak ditutup.

The FcgidOutputBufferSizeparameter konfigurasi mod_fcgid mungkin menyalahkan. Saya telah menemukan tip ini di:

  1. balasan dari Travers Carter dan
  2. ini posting blog Seumas Mackinnon .

Setelah membaca penjelasan di atas, Anda mungkin sampai pada kesimpulan bahwa solusi cepat adalah menambahkan baris (lihat "Contoh Host Virtual" di bagian akhir):

FcgidOutputBufferSize 0

di file konfigurasi Apache Anda (misalnya, httpd.conf), file konfigurasi FCGI Anda (misalnya, fcgid.conf) atau di file host virtual Anda (misalnya, httpd-vhosts.conf).

Dalam (1) di atas, variabel bernama "OutputBufferSize" disebutkan. Ini adalah nama lama yang FcgidOutputBufferSizedisebutkan di (2) (lihat catatan pemutakhiran di halaman web Apache untuk mod_fcgid ).

Detail & Solusi Kedua

Solusi di atas menonaktifkan buffering yang dilakukan oleh mod_fcgid baik untuk seluruh server atau untuk host virtual tertentu. Ini dapat menyebabkan hukuman kinerja untuk situs web Anda. Di sisi lain, ini mungkin tidak terjadi karena PHP melakukan buffering sendiri.

Jika Anda tidak ingin menonaktifkan buffering mod_fcgid , ada solusi lain ... Anda dapat memaksa buffer ini untuk mengosongkan .

Kode di bawah ini melakukan hal itu dengan mengembangkan solusi yang diusulkan oleh 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');
?>

Apa yang pada dasarnya dilakukan oleh baris kode tambahan adalah mengisi buffer mod_fcgi , sehingga memaksanya untuk mengosongkan. Nomor "65537" dipilih karena nilai default FcgidOutputBufferSizevariabel adalah "65536", seperti yang disebutkan di halaman web Apache untuk petunjuk terkait . Oleh karena itu, Anda mungkin perlu menyesuaikan nilai ini jika nilai lain ditetapkan di lingkungan Anda.

Lingkungan Saya

  • WampServer 2.5
  • Apache 2.4.9
  • PHP 5.5.19 VC11, x86, Tanpa Thread Aman
  • mod_fcgid / 2.3.9
  • Windows 7 Professional x64

Contoh Host Virtual

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

    FcgidOutputBufferSize 0

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

Saya mencoba banyak solusi. Dan ini adalah satu-satunya solusi yang berfungsi untuk saya dengan mod_fcgid.
Tsounabe

1

ini berhasil untuk saya

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

Ok, jadi pada dasarnya cara jQuery melakukan permintaan XHR, bahkan metode ob_flush tidak akan berfungsi karena Anda tidak dapat menjalankan fungsi di setiap onreadystatechange. jQuery memeriksa status, lalu memilih tindakan yang tepat untuk diambil (lengkap, kesalahan, sukses, batas waktu). Dan meskipun saya tidak dapat menemukan referensi, saya ingat mendengar bahwa ini tidak berfungsi dengan semua implementasi XHR. Sebuah metode yang saya percaya akan bekerja untuk Anda adalah persilangan antara polling ob_flush dan 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>

Dan karena skrip dieksekusi sebaris, saat buffer dibilas, Anda mendapatkan eksekusi. Untuk membuat ini berguna, ubah console.log ke metode panggilan balik yang ditentukan dalam penyiapan skrip utama Anda untuk menerima data dan menindaklanjutinya. Semoga ini membantu. Salam, Morgan.


0

Solusi alternatifnya adalah menambahkan pekerjaan ke antrian dan membuat skrip cron yang memeriksa pekerjaan baru dan menjalankannya.

Saya harus melakukannya dengan cara itu baru-baru ini untuk menghindari batasan yang diberlakukan oleh host bersama - exec () dkk telah dinonaktifkan untuk PHP yang dijalankan oleh server web tetapi dapat dijalankan dalam skrip shell.


0

Jika flush()fungsi tidak berfungsi. Anda harus mengatur opsi berikutnya di php.ini seperti:

output_buffering = Off  
zlib.output_compression = Off  

0

Solusi Kerja Terbaru

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

Setelah mencoba banyak solusi berbeda dari utas ini (setelah tidak ada yang berhasil untuk saya), saya telah menemukan solusi di halaman resmi 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();
    }
}
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.