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 !