Bom, inicialmente quero deixar bem claro que PHP não é a melhor e mais adequada ferramenta para efetuar esse tipo de tarefa. Diferente do NodeJS (por exemplo), a cada requisição feita ao servidor que serve conteúdo com PHP, uma thread é criada e isso não é muito legal porque se temos várias requisições concorrentes ou ativas ao mesmo tempo, vai acabar gerando latência em ambos os lados (servidor e cliente).
No NodeJS, diferente do PHP, é possível manipular bastante conexões concorrentes em uma única thread, justamente porque, por padrão, o Node faz um servidor de thread única e de fato, é mais apropriado para manter várias conexões ativas, ou seja: efetuar o realtime com o cliente.
Mas não quer dizer que “o PHP é péssimo ou inútil” para fazer esse tipo de coisa, justamente porque nem sempre temos um cloud ou servidor dedicado de prontidão pra antender a demanda. Às vezes queremos fazer uma dinâmica melhor na aplicação, como o uso de realtime, mas o cliente não vai arcar com isso e não é estritamente necessário NodeJS para resolver o problema, portanto, o PHP pode ser a solução, não a melhor, mas será.
Entretanto, recentemente no meu desenvolvimento na web tenho utilizado a técnica long polling em algumas aplicações para entregar respostas em tempo real ao cliente, e usei alguns algoritmos para “quebrar o galho” com PHP, fiz bom uso deles e não tive nenhum problema tanto no cliente quanto no servidor, portanto, vamos lá!
1. Latência no servidor
É um problema; e é um dos piores.
A causa é porque gastamos muito processamento em um único processo e em vários clientes, ou seja, o cliente está pronto para receber respostas ao mesmo tempo que abrimos a conexão com o endpoint que faz a persistência, para entregar a resposta em tempo real, e na medida que outros clientes se conectam ao mesmo endpoint, vários processos são criados e esses processos podem estrapolar o tempo limite de execução do PHP que por padrão é de 30 segundos, e acabar gerando essa latência no servidor.
Mas, esses clientes já conectados não são atingidos, literalmente. O problema está nas novas conexões pois como vamos ter muito processamento e o servidor vai estar bastante ocupado com as requisições já abertas, não conseguimos responder a uma nova requisição em tempo hábil, mesmo que não seja diretamente ligada ao endpoint do long polling e isso é ruim pois, se alguém não consegue acessar a aplicação, ela é inútil.
A solução que eu usei pra resolver o problema foi técnicamente a mesma que o Facebook usa (sim, o Facebook também faz long polling usando PHP) que consiste em definir um tempo limite de vida para uma conexão, ou seja, se em X segundos não houver resposta, encerre a tarefa e considere uma resposta vazia e então incie novamente. Isso vai fazer com que um novo processo seja criado (abandonando o antigo), é equivalente a um “refresh”.
Normalmente, o tempo limite que eu defino é de 15 segundos.
Vejam abaixo:
echo str_pad( null , 1 ); // ecoa uma string vazia para testar a conexão flush(); // força o envio para o cliente da string vazia if( time() >= ( 15 + $_SERVER[ 'REQUEST_TIME' ] ) || connection_aborted() ) { die( json_encode( array() ) ); // para o script e envia uma resposta vazia }
2. Filas de conexão
Não é um problema, é uma otimização. Se você define um tempo limite nas conexões, quer dizer que após atingir um tempo limite e não tiver uma resposta, a conexão será abandonada com uma resposta vazia pro cliente. Aideia é assim que a requisição for feita, adicioná-la em uma fila e definir um limite de conexões nessa fila.
Quando essa fila atingir um certo limite, você faz um timeout de X segundos para limpar essa fila e iniciar uma nova sequência de requisições no servidor. Pra melhorar o algoritmo, você pode adicionar a requisição à essa fila somente se a requisição for fechada sem resposta, assim podemos considerar que provavelmente não há nenhum conteúdo recente para enviar ao cliente.
Isso ajuda o servidor a processar outras requisições com mais tranquilidade, já que alguns clientes serão inativos durante um tempo devido ao timeout que será aplicado. “Enquanto não tenho nada de imediato pra você, vou procurar algo pra outro cara”, e vice-versa.
O tempo que normalmente eu defino para esse timeout é de 30 segundos. O seguinte fragmento de código ilustra programaticamente as operações da fila:
var queue = ( [] || new Array() ); // [ ... ] if( queue.length < 5 ) { // [ ... ] queue.push( this ); // adiciona a requisição na fila } else { // define um timeout de 30 segundos para esvaziar a fila setTimeout( function() { queue = ( [] || new Array() ); // limpa a fila de requisições // [ ... ] } , ( 30 * 1000 ) ); }
3. Aplicação em uso
Também não é um problema, é mais uma otimização.
Bom, vamos afirmar que quando o usuário não está com o foco na janela do website, a aplicação não está sendo usada, portanto podemos parar nossa fila de requisições até que o usuário volte com o foco para a janela do website.
E isso ajuda bastante, porque nem sempre você mantém uma atividade/uso constante na aplicação, então não adianta fornecer resposta ou exibir algum conteúdo que não será visto imediatamente. Quando o usuário voltar com o foco pra janela uma nova requisição será disparada em busca do conteúdo, e se ele estiver disponível, a resposta será imediata, ou seja, vai ser como se estivessemos usando constantemente.
var isInactive = ( false ); $( window ).bind( { blur : function() { // [ ... ] isInactive = ( true ); }, focus : function() { // [ ... ] isInactive = ( false ); } } );
Fiz uma implementação de exemplo seguindo os algoritmos acima e está disponível no GitHub. Para utilização, basta criar o banco de dados e configurar os dados de conexão com ele no arquivo /ajax/endpoint.php.
Para testar, basta abrir a index e fazer insert’s na tabela de notificações:
INSERT INTO `notifications`( `nid`, `notification` ) VALUES( NULL, 'Hello World :)' );
É isso, abraços!
Qualquer dúvida, critíca, sugestão, por favor, não deixem de fazer!