Front End

30 jan, 2012

Veja como o Long Polling pode te ajudar a desenvolver aplicações em tempo real

Publicidade

Um assunto muito interessante e bastante discutido é aplicações em tempo real (comet) – nesse caso, uma conexão aberta com o servidor aguardando respostas. Enquanto não temos HTML5 em todos os navegadores, para poder utilizar os WebSockets temos que fazer uso de técnicas Comet para desenvolver aplicações em tempo real. Para fazer isso, basta utilizar a técnica Long Polling.

Long Polling

Essa é uma técnica comet, que permite que a conexão fique aberta, aguardando uma resposta do servidor para possa ser fechada e, depois, reaberta. Ela funciona de forma diferente do AJAX Polling, que fica enviando requisições, mesmo que o servidor alegue, consecutivamente, não ter nenhuma resposta. Isso acaba prejudicando a aplicação e criando um excesso de requisições no servidor. Observe uma Long Polling em ação na imagem abaixo:

Na prática, o que é?

A conexão deve ser sempre aberta. Caso ela seja fechada e o cliente não tente reabri-la, não estaremos enviando uma resposta. Com o 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, podemos manipulá-las. É aí que entra a mágica!

Com o Node.JS, guardamos todos os manipuladores em um objeto de manipuladores, para que possam ser utilizados de acordo com as requisições feitas.

var http = require ( 'http' ) ; // Módulo HTTP do Node.JS ( Nativo )
var handlers = new Object ( ) ; // Objeto de manipuladores

A variável ‘handlers’ é global para todo código e fica acessível a todos, portanto, não se preocupem muito com o escopo. Além do mais, para o nosso objetivo, 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. Para isso é necessário o require desse modulo. Depois, adiciona-se 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 (contentType como 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 testa se ele não é nulo ou indefinido e testa, 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, que é exatamente o que estará no lugar de [path] em ‘http://127.0.0.1:8080/[path]‘. E o handler é mapeado dessa forma por ser a mais fácil. Existem outras, por meio do query string, por exemplo, 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, o executamos passando os objetos request e response como atributos para poder manipular logo. Assim, não colocamos um monte de ifs dentro da closure. Vamos adicionar no objeto cada handler necessário de forma diferente, e, 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 ) ;
} ;

Depois da primeira parte feita, eu criei um array para mensagens. Vocês podem colocar um setInterval para remover as mensagens a cada minuto, pra não manter um array enorme de mensagens antigas. Depois eu criei um objeto que representa uma mensagem. Nele eu coloquei 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 corretament.

callBack: o que é e pra quê serve dentro desse contexto?

O callback é o que 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, então, temos que notificar aos clientes que esperavam por essa mudança. Caso não os notifiquemos, eles vão ficar com a conexão aberta, esperando uma resposta – que não será enviada. Portanto, chamamos o callback logo após a mensagem ser adicionada na list.

Temos, então, a parte que envia a mensagem. 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 = [ ] ;
}
} ;

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 a requisição. A função callBack que é adicionada na lista só é chamada quando uma mensagem é enviada, 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. Depois 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, e elas serão mostradas. Depois vamos requisitar de novo, procurando por novas – só que a conexão vai ficar aberta até termos novas mensagens. Enquanto a conexão estiver aberta, uma nova não pode ser criada. No console do Google Chrome, na aba “network”, você vê 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 pode ser feita utilizando cURL. Vou abrir quatro 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?