Linha de Código – Artigos
Category Archives: ajax
Long Polling com #PHP
Olá Pessoal !
Como mostrei a forma de se fazer um Long Polling utilizando Node.JS ( Long Polling com Node.JS ) , irei mostrar agora a forma de se fazer com PHP.
Importante !
Funcionar funciona, apesar de não ser tão eficiente quanto ao long polling feito utilizando node.js, não foram testados os efeitos colaterais no servidor, tais como desempenho e concorrência de uso, afinal estamos utilizando PHP.
A Técnica
Bom, para começar eu gostaria que vocês lessem o post do Long Polling com Node.JS para entender um pouco mais da técnica utilizada, o que na verdade só é uma técnica e aplicando o que foi proposto por ela:
- Abrir a conexão e não deixar que esta seja fechada sem resposta
- Se a conexão for fechada por algum motivo, tentar reabrir
- Não fechar a conexão até ter uma resposta esperada
Vocês irão ter um Long Polling com sucesso, então vamos lá .!
Lógica, Codificação
Criei quatro arquivos { “index.php” , “data.txt” , “server.php” , “client.js” } o data.txt contém os dados que serão mostrados, mas antes disso será feita uma comparação lógica, que se baseia em NÃO emitir uma resposta se : ‘os dados do data.txt estiverem vazios, se o conteúdo do data.txt for igual ao conteúdo requisitado’
Conteúdo requisitado ? quando recebemos uma resposta do servidor, que obviamente só vamos receber se houver conteúdo, nos capturamos o que foi retornado e enviamos por query string de volta ; Por que ? para comparar se o conteúdo atual ou futuro do data.txt é diferente, pois não vamos responder ou perguntar algo que já sabemos, lembrando que a mesma coisa vale para um banco de dados.
Nosso código PHP ficaria assim:
<?php while ( true ) { $data = file_get_contents ( 'data.txt' ) ; $requested = isset ( $_GET [ 'content' ] ) ? $_GET [ 'content' ] : null ; if ( strlen ( $data ) > 0 && $requested !== $data ) { echo $data ; break ; } else { sleep ( 2 ) ; // vamos dar um intervalo ! continue ; } }
O que fizemos ? iniciamos um loop infinito, logicamente se ele não for parado a requisição nunca vai terminar, a mesma coisa vale pra quando entramos na página que contém esse código ( server.php ) , e então lemos o conteúdo do arquivo, e procuramos pelo índice content na query string , duas comparações simples ( se a quantidade de caracteres de $data for maior que zero e se $requested for diferente de $data que é o conteúdo do text, emitimos a resposta e paramos o loop , nesse caso a requisição é fechada, feito isso o resto é simples.
Agora, o lado do cliente ( jscript ) , basta criar uma função que é responsável por enviar a requisição para ‘server.php’, e quando esta for fechada ( Http Code 200 ) , criamos a requisição novamente .. e assim por diante ( recursividade )
$ ( document ).ready ( function ( ) { $ ( '#ready' ).on ( 'click' , function ( ) { startPolling = function ( content ) { content === undefined || $ ( '#response' ).html ( content ) ; var qs = { 'content' : content } ; $.get ( 'server.php' , qs , function ( data ) { startPolling ( data ) ; } ) ; } ; startPolling ( undefined ) ; } ) ; } ) ;
Só precisamos disso .. bem simples e direto , a unica coisa que tem de demais no código acima, é a recursividade, como eu disse que faz parte da técnica, a conexão precisa ser reaberta caso fechada, e ela só é fechada quando temos a resposta esperada , para testar basta criar um arquivo html e colocar o seguinte código
<html> <head> <script type="text/javascript" src="http://code.jquery.com/jquery.min.js"></script> <script type="text/javascript" src="client.js"></script> </head> <body> <input type="button" value="ready" id="ready"> <div id="response"></div> </body> </html>
Isso é tudo .. agora façam seus testes, não apliquem uma técnica dessa pra algo que possa ser resolvido de outra forma, estude outras maneiras e principalmente, avaliem o custo benefício de cada uma delas, o que foi demonstrado acima funciona, você evita ficar enviando requisições ao servidor quando ele está respondendo o que você não quer , me faz lembrar o burro do shrek .
Abraços, e boa sorte !
Long Polling com Node.JS
Bem, esse é um assunto muito interessante e bastante discutido, trata-se de aplicações em tempo-real ( comet ) , nesse caso uma conexão aberta com o servidor aguardando respostas. Enquanto não temos HTML5 em todos navegadores, para poder utilizar os WebSockets, temos que utilizar técnicas Comet para desenvolver aplicações em realtime, como podemos fazer isso !? Podemos utilizar a técnica Long Polling !
Long Polling
É uma técnica comet, que permite que a conexão fique aberta, aguardando uma resposta do servidor para esta ser fechada e reaberta novamente, a diferença do AJAX Polling, que fica enviando requisições mesmo o servidor respondendo ‘não tenho nenhuma resposta’ consecutivamente, isso acaba prejudicando a aplicação e criando um excesso de requisições no servidor, observa-se uma long polling em ação com a imagem abaixo
Na prática, o que é ?
A conexão deve ser sempre aberta, o que não pode acontecer é essa conexão ser fechada, e o cliente não tentar reabri-la, daí não estamos respondendo de volta. Com Node.JS, temos um Daemon ( Disk And Execution Monitor ) , que lida com todas as requisições recebidas e enviadas ou seja o serviço é independente tanto de resposta, quanto de recebimento, nesse caso temos handlers ( manipuladores ) para as requisições feitas, e dessa forma, manipulamos essas requisições, é aí que entra a mágica !
Com Node.JS, guardamos todos os manipuladores em um objeto de manipuladores, para que possam ser utilizados de acordo com as requisições feitas, logo seguinte trecho de código
var http = require ( 'http' ) ; // Módulo HTTP do Node.JS ( Nativo ) var handlers = new Object ( ) ; // Objeto de manipuladores
Dessa forma, ficam acessíveis a todos , a variável ‘handlers’ é global para todo código portanto, não se preocupem muito com o escopo, além do mais para o que vamos utiliza-la, não teremos esse tipo de problema, vamos criar o servidor HTTP (http.createServer)
var http = require ( 'http' ) ; // Módulo HTTP do Node.JS ( Nativo ) var handlers = new Object ( ) ; // Objeto de manipuladores var url = require ( 'url' ) ; var server = http.createServer ( function ( request , response ) { response.send = function ( httpCode , data , contentType ) { var $data ; if ( data instanceof Object ) { $data = new Buffer ( JSON.stringify ( data ) ) ; } else $data = data.toString ( ) ; this.writeHead ( parseInt ( httpCode ) , { 'Content-Type' : contentType.toString ( ) , 'Content-Length' : $data.length } ) ; this.end ( $data ) ; } ; var path = url.parse ( request.url ).pathname.toString ( ) ; if ( path !== null || path !== undefined ) { if ( path in handlers !== false ) { if ( typeof handlers [ path ] === 'function' ) { handlers [ path ] ( request , response ) ; } } } } ).listen ( 8080 , function ( ) { console.log ( 'Servidor iniciado !' ) ; } ) ;
Vamos precisar do modulo ‘url’ para fazer o parser de urls, ou seja as urls de requisição, é necessário o require desse modulo, logo depois, a adição de um método auxiliar para emitir respostas para o cliente, trata-se do método send no objeto response, que emite respostas de acordo com os parâmetros passados, contentTypecomo todos conhecem, o tipo do conteúdo que está sendo enviado, o código http e os dados, logo abaixo o parser do path da url, testando se este não é nulo ou indefinido , e testando também se este path está registrado como um handler, o que esperamos que seja.
Importante !
Os handlers são mapeados de acordo com o path, o que seria esse path ? é exatamente o que estará no lugar de [path] em ‘http://127.0.0.1:8080/[path]‘.
Mas porque o handler é mapeado dessa forma !? porque essa é a forma mais fácil, existem outras, podemos fazer por query string enviando o parâmetro handler com o nome do handler em questão.
Se esse handler for uma função, o que esperamos que seja, executamos ele passando os objetos request e response para poder manipular logo, não colocamos um monte de if’s dentro da closure ( fica feio não !? ), vamos adicionar cada handler necessário no objeto de forma diferente ! então, vamos ter o seguinte código
var messages = [ ] , callbacks = [ ] ; handlers [ '/message' ] = function ( request , response ) { var message = url.parse ( request.url , true ).query.text ; var $ = { text : message.toString ( ) , appendMessage : function ( callBack ) { messages.push ( this ) ; callBack ( this ) ; } }.appendMessage ( function ( message ) { while ( callbacks.length > 0 ) { callbacks.shift ( ).callBack ( [ message ] ) ; } } ) ; response.writeHead ( 200 , { } ) ; response.end ( null ) ; } ;
Primeira parte feita, criei um array para mensagens ( o que será armazenado nele ? todas as mensagens que são enviadas, ficam ali ), vocês podem colocar um setInterval para remover as mensagens a cada 1 minuto, pra não manter um array enorme de mensagens antigas , então crio um objeto que representa uma mensagem, nesse objeto, tem o método appendMessage que é executado após a criação do objeto, esse método adiciona a mensagem na lista (this) e executa o callBack passado por parâmetro, essa é a parte mais importante para tudo funcionar corretamente
O callBack .. o que é, e pra que serve nesse contexto ?
O callback é quem vai notificar todas as conexões abertas, e é exatamente por isso que ele é executado quando uma mensagem é adicionada, se uma mensagem é adicionada, algo mudou no servidor, temos então que notificar aos clientes que esperam esta mudança acontecer, se não notificarmos, o cliente vai ficar com a conexão aberta esperando uma resposta que sabemos que não será enviada. Portanto, chamamos o callback logo após a mensagem ser adicionada na lista
Temos então, a parte que envia a mensagem, deixando claro aqui também, lembre-se de emitir um HTTP-Code 200 para que a conexão não fique aberta, por isso emito o header e executo o método end pra enviar a resposta para o requisitante e fechar a conexão, agora a parte que faz o polling:
handlers [ '/polling' ] = function ( request , response ) { if ( messages.length === 0 ) { callbacks.push ( { callBack : function ( $messages ) { response.send ( 200 , { messages : $messages } , 'text/json' ) ; } } ) ; } else { response.send ( 200 , { 'messages' : messages } , 'text/json' ) ; messages = [ ] ; } } ;
Simples não !? verifico se o array de mensagens é vazio, se for adiciono um cliente esperando resposta na lista de callBacks, porque este deve manter a conexão, observem também que eu NÃO dou fim na requisição, a função callBack que é adicionada na lista só é chamada quando uma mensagem é enviada ( algo muda no servidor ) , então o próprio servidor já vai notificar quando houver novas mensagens, caso o array já tenha mensagens, respondo a requisição e mando as mensagens que estão no array, logo após limpo a lista de mensagens, no lado do cliente teríamos algo semelhante ao seguinte fragmento de código
$ ( document ).ready ( function ( ) { function startPolling ( data ) { if ( data instanceof Object ) { for ( var i in data.messages ) { // ... exibe as mensagens } } $.ajax ( { url : 'http://127.0.0.1:8080/polling' , method : 'GET' , success : function ( data ) { startPolling ( data ) ; } } ) ; } } ) ;
O que vai acontecer ? vamos requisitar novas mensagens quando houver, essas mensagens serão mostradas e vamos requisitar de novo procurando por novas, só que a conexão vai ficar aberta, até termos novas mensagens e enquanto a conexão estiver aberta uma nova não é criada, no console do google chrome aba network, mostra que não estamos trafegando dado algum, mas a requisição está pendente ou seja, esperando uma resposta
Demonstração rápida com cURL
Uma breve demonstração disto em ação, pode ser feita utilizando cURL, vou abrir 4 janelas do prompt-de-comando, em três delas, eu vou iniciar o polling, e na última, vou enviar uma mensagem, observem o resultado
-- Janela 1 C:>curl http://127.0.0.1:8080/polling {"messages":[{"text":"Hello!"}]} -- Janela 2 C:>curl http://127.0.0.1:8080/polling {"messages":[{"text":"Hello!"}]} -- Janela 3 C:>curl http://127.0.0.1:8080/polling {"messages":[{"text":"Hello!"}]} -- Janela 4 C:>curl http://127.0.0.1:8080/message?text=Hello!
Bem legal não é !? e você, sabe outra técnica ? alguma dica para melhorar o long-polling ? apresente-se !