Como faço para fechar uma conexão antes do tempo?

99

Estou tentando fazer uma chamada AJAX (via JQuery) que iniciará um processo bastante longo. Gostaria que o script simplesmente enviasse uma resposta indicando que o processo foi iniciado, mas o JQuery não retornará a resposta até que o script PHP termine de ser executado.

Eu tentei isso com um cabeçalho "close" (abaixo) e também com buffer de saída; nem parece funcionar. Qualquer suposição? ou é algo que preciso fazer em 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.' );

?>
Eric_WVGG
fonte
você liberou seu buffer de saída com ob_flush () e não funcionou?
Vinko Vrsalovic

Respostas:

87

A seguinte página de manual do PHP (incluindo notas do usuário) sugere várias instruções sobre como fechar a conexão TCP com o navegador sem encerrar o script PHP:

Supostamente, isso requer um pouco mais do que enviar um cabeçalho fechado.


OP então confirma: sim, isso funcionou: apontando para a nota do usuário nº 71172 (novembro de 2006) copiada aqui:

Fechar a conexão do navegador do usuário enquanto mantém seu script php rodando tem sido um problema desde [PHP] 4.1, quando o comportamento do register_shutdown_function()foi modificado para que não fechasse automaticamente a conexão do usuário.

sts at mail ponto xubion ponto hu Postou a solução original:

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

O que funciona bem até que você substitua, phpinfo()caso echo('text I want user to see');em que os cabeçalhos nunca são enviados!

A solução é desativar explicitamente o buffer de saída e limpar o buffer antes de enviar as informações do cabeçalho. Exemplo:

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

Acabei de passar 3 horas tentando descobrir isso, espero que ajude alguém :)

Testado em:

  • IE 7.5730.11
  • Mozilla Firefox 1.81

Mais tarde, em julho de 2010, em uma resposta relacionada, o Arctic Fire vinculou duas outras notas do usuário que eram acompanhamentos à anterior:

Joeri Sebrechts
fonte
1
Autor e @Timbo White, É possível fechar uma conexão antecipadamente sem saber o tamanho do conteúdo? Ou seja, sem ter que capturar o conteúdo antes do fechamento.
skibulk
3
Hackers e navegadores de baixa qualidade ainda podem ignorar o cabeçalho HTTP de fechamento de conexão e obter o resto da saída ... certifique-se de que o que vem a seguir não seja confidencial. talvez um ob_start (); para suprimir tudo: p
hanshenrik
3
Adicionando fastcgi_finish_request (); foi dito que fecha a conexão com sucesso quando o procedimento acima não funciona. No entanto, no meu caso, ele impediu que meu script continuasse em execução, portanto, use com cuidado.
Eric Dubé
@RichardSmith Porque o Connection: closecabeçalho pode ser sobrescrito por outro software na pilha, por exemplo, o proxy reverso no caso de um CGI (observei esse comportamento com o nginx). Veja a resposta de @hanshenrik sobre isso. Em geral, Connection: closeé executado no lado do cliente e não deve ser considerado uma resposta a esta pergunta. A conexão deve ser encerrada do lado do servidor .
7heo.tk
56

É necessário enviar estes 2 cabeçalhos:

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

Como você precisa saber o tamanho de sua saída, precisará armazená-la em buffer e, em seguida, descarregá-la no navegador:

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

Além disso, se o seu servidor web estiver usando compactação gzip automática na saída (ou seja, Apache com mod_deflate), isso não funcionará porque o tamanho real da saída foi alterado e o Content-Length não é mais preciso. Desative a compactação gzip do script específico.

Para obter mais detalhes, visite http://www.zulius.com/how-to/close-browser-connection-continue-execution

Timbó Branco
fonte
16
Se o seu servidor comprime a saída, você pode desabilitá-la com header("Content-Encoding: none\r\n");Assim o apache não a comprimirá.
GDmac de
1
@GDmac obrigado! Não consegui fazer isso funcionar por um tempo, mas desabilitar a compactação resolveu o problema.
Reagular de
O ob_flush()não é necessário e realmente causa um aviso failed to flush buffer. Eu tirei e funcionou muito bem.
Levi
2
Achei que a ob_flush()linha era necessária.
Deebster
21

Você pode usar Fast-CGI com PHP-FPM para usar a fastcgi_end_request()função . Dessa forma, você pode continuar a fazer algum processamento enquanto a resposta já foi enviada ao cliente.

Você encontra isso no manual do PHP aqui: FastCGI Process Manager (FPM) ; Mas essa função especificamente não está mais documentada no manual. Aqui está o trecho do PHP-FPM: Wiki do PHP FastCGI Process Manager :


fastcgi_finish_request ()

Escopo: função php

Categoria: Otimização

Este recurso permite que você acelere a implementação de algumas consultas php. A aceleração é possível quando há ações no processo de execução do script que não afetam a resposta do servidor. Por exemplo, salvar a sessão no memcached pode ocorrer após a página ser formada e passada para um servidor da web. fastcgi_finish_request()é um recurso php, que interrompe a saída de resposta. O servidor da Web imediatamente começa a transferir a resposta "lenta e tristemente" para o cliente, e o php ao mesmo tempo pode fazer muitas coisas úteis no contexto de uma consulta, como salvar a sessão, converter o vídeo baixado, lidar com todos os tipos de estatísticas, etc.

fastcgi_finish_request() pode invocar a execução da função de desligamento.


Nota: fastcgi_finish_request() tem uma peculiaridade onde as chamadas para flush, printou echoirá encerrar o script inicial.

Para evitar esse problema, você pode ligar ignore_user_abort(true)antes ou depois da fastcgi_finish_requestligação:

ignore_user_abort(true);
fastcgi_finish_request();
Jorrit Schippers
fonte
3
ESTA É A RESPOSTA REAL!
Kirill Titov
2
se você estiver usando php-fpm - apenas use esta função - esqueça os cabeçalhos e tudo mais. Me economizou muito tempo!
Ross
17

Versão completa:

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();
diyism
fonte
completo em que sentido? Qual problema exigiu que você concluísse o script de respostas aceitas (qual?) E quais das diferenças de configuração tornaram isso necessário?
hakre
4
esta linha: header ("Content-Encoding: none"); -> muito importante.
Bobby Tables
2
Obrigado, esta é a única solução funcional nesta página. Isso deve ser aprovado como a resposta.
6

Uma solução melhor é fazer um fork de um processo em segundo plano. É bastante simples em unix / linux:

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

Você deve olhar para esta questão para obter melhores exemplos:

PHP executa um processo em segundo plano

Salman A
fonte
4

Supondo que você tenha um servidor Linux e acesso root, tente isto. É a solução mais simples que encontrei.

Crie um novo diretório para os arquivos a seguir e conceda a ele permissões totais. (Podemos torná-lo mais seguro mais tarde.)

mkdir test
chmod -R 777 test
cd test

Coloque isso em um arquivo chamado bgping.

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

Observe o &. O comando ping será executado em segundo plano enquanto o processo atual passa para o comando echo. Ele fará ping em www.google.com 15 vezes, o que levará cerca de 15 segundos.

Torne-o executável.

chmod 777 bgping

Coloque isso em um arquivo chamado 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";

?>

Ao solicitar bgtest.php em seu navegador, você deve obter a seguinte resposta rapidamente, sem esperar cerca de 15 segundos para que o comando ping seja concluído.

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

result:0
end bgtest.php

O comando ping agora deve estar em execução no servidor. Em vez do comando ping, você pode executar um script PHP:

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

Espero que isto ajude!

Liam
fonte
4

Aqui está uma modificação no código do Timbo que funciona com compactação 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 ********/
Andrew Winter
fonte
VOCÊ É UM DEUS. Estou trabalhando há 2 dias para tentar descobrir isso. Funcionou no meu dev local, mas não no host. Eu estava ensopado. VOCÊ ME SALVOU. OBRIGADO!!!!
Chad Caldwell
3

Estou em um host compartilhado e fastcgi_finish_requestconfigurado para sair dos scripts completamente. Também não gosto da connection: closesolução. Seu uso força uma conexão separada para solicitações subsequentes, custando recursos adicionais do servidor. Eu li o Transfer-Encoding: cunked artigo da Wikipedia e descobri que 0\r\n\r\numa resposta termina. Eu não testei completamente isso em todas as versões de navegadores e dispositivos, mas funciona em todos os 4 dos meus navegadores atuais.

// 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);
}
skibulk
fonte
Graças à sua boa resposta, percebi o quão estúpido (e desnecessário) é usar connection: close. Acho que alguns não estão familiarizados com as porcas e parafusos de seu servidor.
Justin
@ Justin eu escrevi isso há muito tempo. Olhando novamente, devo observar que pode ser necessário aumentar os pedaços para 4 KB. Parece que me lembro que alguns servidores não funcionam até atingirem esse mínimo.
skibulk
2

Você pode tentar fazer multithreading.

você poderia preparar um script que faz uma chamada de sistema (usando shell_exec ) que chama o binário php com o script para fazer seu trabalho como parâmetro. Mas não acho que seja a forma mais segura. Talvez você possa fortalecer as coisas fazendo chroot no processo php e outras coisas

Alternativamente, há uma classe em phpclasses que faz isso http://www.phpclasses.org/browse/package/3953.html . Mas eu não sei os detalhes da implementação

paan
fonte
E se você não quiser esperar que o processo termine, use o &caractere para rodar o processo em segundo plano.
Liam
2

Resposta do 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.

Resposta da função:

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.
FluorescentGreen5
fonte
1

Seu problema pode ser resolvido fazendo alguma programação paralela em php. Eu fiz uma pergunta sobre isso algumas semanas atrás aqui: Como alguém pode usar multi-threading em aplicativos PHP

E obteve ótimas respostas. Gostei muito de um em particular. O autor fez uma referência ao tutorial Easy Parallel Processing in PHP (Set 2008; by johnlim) que pode realmente resolver seu problema muito bem, pois já o usei para lidar com um problema semelhante que surgiu alguns dias atrás.

Steve Obbayi
fonte
1

A resposta de Joeri Sebrechts está fechada, mas destrói qualquer conteúdo existente que possa ser armazenado em buffer antes de você desejar se desconectar. Ele não chama ignore_user_abortcorretamente, permitindo que o script seja encerrado prematuramente. A resposta do diyism é boa, mas não é genericamente aplicável. Por exemplo, uma pessoa pode ter mais ou menos buffers de saída que aquela resposta não trata, então pode simplesmente não funcionar na sua situação e você não saberá por quê.

Esta função permite que você se desconecte a qualquer momento (desde que os cabeçalhos ainda não tenham sido enviados) e retém o conteúdo gerado até agora. O tempo extra de processamento é ilimitado por padrão.

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

Se você também precisar de memória extra, aloque-a antes de chamar esta função.

Walf
fonte
1

Nota para usuários mod_fcgid (por favor, use por sua própria conta e risco).

Solução Rápida

A resposta aceita de Joeri Sebrechts é de fato funcional. No entanto, se você usar o mod_fcgid, poderá descobrir que esta solução não funciona sozinha. Em outras palavras, quando a função flush é chamada, a conexão com o cliente não é fechada.

O FcgidOutputBufferSizeparâmetro de configuração de mod_fcgid pode ser o culpado. Encontrei esta dica em:

  1. esta resposta de Travers Carter e
  2. esta postagem do blog de Seumas Mackinnon .

Depois de ler o acima, você pode chegar à conclusão de que uma solução rápida seria adicionar a linha (consulte "Exemplo de Host Virtual" no final):

FcgidOutputBufferSize 0

em seu arquivo de configuração do Apache (por exemplo, httpd.conf), seu arquivo de configuração FCGI (por exemplo, fcgid.conf) ou em seu arquivo de hosts virtuais (por exemplo, httpd-vhosts.conf).

Em (1) acima, uma variável chamada "OutputBufferSize" é mencionada. Este é o nome antigo do FcgidOutputBufferSizemencionado em (2) (veja as notas de atualização na página da web do Apache para mod_fcgid ).

Detalhes e uma segunda solução

A solução acima desativa o buffering executado por mod_fcgid para todo o servidor ou para um host virtual específico. Isso pode levar a uma penalidade de desempenho para o seu site. Por outro lado, pode muito bem não ser o caso, já que o PHP realiza o buffer por conta própria.

Caso você não queira desabilitar o buffer de mod_fcgid , há outra solução ... você pode forçar o esvaziamento deste buffer .

O código abaixo faz exatamente isso com base na solução proposta por 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');
?>

O que a linha de código adicionada essencialmente faz é preencher o buffer de mod_fcgi , forçando-o a esvaziar. O número "65537" foi escolhido porque o valor padrão da FcgidOutputBufferSizevariável é "65536", conforme mencionado na página da web do Apache para a diretiva correspondente . Portanto, pode ser necessário ajustar esse valor adequadamente se outro valor for definido em seu ambiente.

Meu ambiente

  • 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

Exemplo de host virtual

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

    FcgidOutputBufferSize 0

    <Directory "d:/wamp/www/example">
        Require all granted
    </Directory>
</VirtualHost>
Tasos
fonte
Tentei muitas soluções. E esta é a única solução que funciona para mim com mod_fcgid.
Tsounabe
1

isso funcionou para mim

//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);
Williem
fonte
0

Ok, então basicamente da forma como o jQuery faz a solicitação XHR, até mesmo o método ob_flush não funcionará porque você não consegue executar uma função em cada onreadystatechange. jQuery verifica o estado e, em seguida, escolhe as ações adequadas a serem tomadas (completo, erro, sucesso, tempo limite). E embora eu não tenha conseguido encontrar uma referência, lembro-me de ouvir que isso não funciona com todas as implementações de XHR. Um método que acredito que deva funcionar para você é um cruzamento entre a votação ob_flush e a polling para sempre.

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

E como os scripts são executados em linha, à medida que os buffers são liberados, você obtém a execução. Para tornar isso útil, altere o console.log para um método de retorno de chamada definido na configuração do script principal para receber dados e agir sobre eles. Espero que isto ajude. Saúde, Morgan.

Morgan ARR Allen
fonte
0

Uma solução alternativa é adicionar o trabalho a uma fila e fazer um script cron que verifica se há novos trabalhos e os executa.

Tive que fazer isso recentemente para contornar os limites impostos por um host compartilhado - exec () et al foi desabilitado para PHP executado pelo servidor da web, mas podia ser executado em um script de shell.

Ole Helgesen
fonte
0

Se a flush()função não funcionar. Você deve definir as próximas opções no php.ini como:

output_buffering = Off  
zlib.output_compression = Off  
Nir O.
fonte
0

Última Solução de Trabalho

    // 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
Sudharshana SL Srihari
fonte
0

Depois de tentar muitas soluções diferentes deste tópico (depois que nenhuma delas funcionou para mim), encontrei a solução na página oficial do 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();
    }
}
Anjo Branco
fonte