segunda-feira, 2 de junho de 2014

Real time communications com Websocket e Node.js






É... Eu sei que você perdeu minha palestra no Seminário sobre o Ecossistema Javascript, do CISL, mas, como o assunto é muito interessante, resolvi postar aqui o código-fonte e um artigo sobre isso.
Veja como implementar comunicação de baixa latência entre os clientes e o Servidor usando apenas o padrão (HTML 5).

Sempre foi um sonho dos desenvolvedores Web poder enviar informações para as páginas. Isso é conhecido por "push". Na verdade, existe uma forma mais simples de fazer isso, por exemplo, usando o "meta" refresh. Isso é conhecido como "push simulado".

E podemos implementar isso até mesmo usando Ajax e "setInterval", porém, ainda estaremos agindo da mesma forma:

  • Usando o mesmo socket;
  • Usando o mesmo HTTP;
  • Sujeitos à mesma latência.

Latência???

 Sim! É todo o tempo gasto entre o envio da mensagem e seu recebimento, no destinatário. O HTTP é um protocolo complexo e existe alguma latência decorrente disso.

Considerando uma aplicação de Chat, temos uma página na qual o usuário digita a mensagem, e também pode receber respostas. Se a comunicação entre ambos os usuários for feita na base do HTTP, notaremos que existe um tempo, além do necessário, deste que digitamos a mensagem até que ela seja exibida na tela do segundo usuário.

Parte dessa "latência" é o próprio mecanismo que estamos usando para fazer a mensagem aparecer. Na verdade, o segundo usuário tem que perguntar ao Servidor, com determinada frequência, se há novas mensagens. Só isso implica em:
  • Esperar o intervalo de tempo;
  • Enviar mensagem ao Servidor;
  • Receber resposta.
É claro que podemos diminuir o intervalo de tempo em que a página fica "enchendo a paciência" do Servidor, mas isso gera um grande problema, pois, além de ficarmos com um intervalo muito pequeno no Cliente, podemos sobrecarregar o Servidor de pedidos inúteis.

Se nossa aplicação apenas envia mensagens, como um Chat, essa latência pode ser tolerável. Mas e se estivermos fazendo uma tarefa na qual o tempo é mais crítico, como controlar um "drone", por exemplo? O Drone vai se chocar com a parede, antes de receber o comando para virar!

É aí que entra o Websocket!

Websocket é um padrão e uma tecnologia para comunicação bidirecional em HTML, com Baixa latência e Bidirecional. Está implementado no HTML 5 e vários navegadores dão suporte. E, se não derem, podemos recorrer a um "fallback" usando Ajax.

Para saber o suporte a Websockets, podemos consultar o site "html5test.com":


Funciona inicialmente com HTTP e faz "upgrade" da conexão. Usa o mesmo Navegador e o mesmo Servidor Web, maior segurança e estabilidade. Por exemplo, podemos requisitar um Websocket utilizando o novo protocolo "ws", a partir do Navegador:

var connection = 

     new WebSocket('ws://server.example.com', 
                   ['soap', 'xmpp']);


Neste momento, o Navegador envia um request ao Servidor solicitando o "upgrade" para Websockets:

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: ...
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com


E o Servidor, caso suporte o "upgrade", responde dessa forma:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: ...
Sec-WebSocket-Protocol: chat


A partir daí, o Cliente é acionado sempre que alguma mensagem chegar via o Websocket, como se fosse um Servidor, sem necessidade de ficar "cutucando" o Servidor para saber se tem nova mensagem. O Servidor simplesmente usa o Websocket e envia a mensagem ao Cliente.

Um exemplo vale mais que 1000 palavras...

Ok. Eu fiz um exemplo bem legal que vai fazer cair o seu queixo! Antes de mais nada, veja só a imagem dele, com 4 navegadores rodando:






Ao digitar uma mensagem e clicar no botão, a mesma é enviada ao Servidor (usando um HTTP PUT via Ajax) e o Servidor a re-envia para todos os Websockets conectados. A resposta é quase instantânea.

Este projeto está no GitHub.


É uma aplicação Node.js / Express típica. Ao obter a página inicial, o Template Jade já armazena um UUID (identificador de sessão) enviado pelo servidor (arquivo: layout.jade):

doctype html
html
  head
    title= title
    link(rel='stylesheet', href='/stylesheets/style.css')
    script(type='text/javascript', src='http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js')
    script(type='text/javascript', src='/javascripts/index.js')
    script(type='text/javascript')
      | meuid = "#{uuid}";
  body
    block content

A página carrega o jQuery e um script meu "index.js", que intercepta o evento "onload" da página:

// Página de eventos do Cliente

var porta = 8080;
var websocket;
$(document).ready(function(){
   $("#btnsend").click(function(){
         $.ajax({
             url: "/msg/" + $("#textomsg").val(),
             type: 'PUT',
             fail:  function(jqXHR, textStatus) {
                 $("#mensagens")
                  .text("Erro ao postar mensagem: " 
                  + textStatus);
             }
         });      
   });
      websocket = new WebSocket("ws://localhost:9090/");
      websocket.onopen =  function() {
                  console.log('Websocket aberto'); 
                  websocket.send(meuid);
               };
      websocket.onerror =  function(e) {
                  console.log('Websocket erro: ' + e.data); 
               };             
      websocket.onmessage = function(e) { 
                  $('#mensagens').text(e.data)
                  console.log('mensagem: ' + e.data); 
               };

  
  });

 Este script modifica o clique do botão, para enviar um request REST PUT usando Ajax, com a nova mensagem. E também abre um Websocket com o Servidor, enviando o UUID recebido inicialmente. Ele cria um "callback" para quando chega uma mensagem do Servidor, neste caso, ele simplesmente exibe o conteúdo em uma DIV.

Não há necessidade de "refresh" e nem de "setInterval()". Simplesmente, o Cliente se comportará como um Servidor, com um socket bi-direcional aberto. Ele poderá receber mensagens sem ter enviado coisa alguma. Legal, não?

Do lado Servidor, eu simplesmente usei o módulo "ws" que possibilita o uso de Websocket com aplicações Node.js. No meu código app.js eu abro o Websocket, depois de registrar as Rotas do Express:

... 
app.get('/', routes.index);
app.put('/msg/:mensagem', routes.novamsg);

http.createServer(app).listen(app.get('port'), function(){
  console.log('Express server listening on port ' + app.get('port'));
});

// Websocket

wsserver = new WebSocketServer({port: 9090});
wsserver.on('connection', function(ws) {
    conexoes.push(ws);
    ws.on('message', function(message) {
        ws.uuid = message;
        console.log('WS identificado: ' + ws.uuid);
    });
});

Eu tenho duas rotas: uma, para baixar a página inicial, e outra para enviar uma mensagem. Logo após fixar as rotas, eu abro um Websocket servidor, na porta 9090. Quando chega uma mensagem, eu armazeno o UUID recebido. Quando o Cliente abre uma conexão websocket, a primeira coisa que ele faz é enviar o UUID. Assim, eu tenho um vetor de Websockets, cada um com seu UUID, e posso identificar quem enviou e para quem devo enviar a mensagem (embora eu não esteja usando isso nesse exemplo).

Bem, quando o Cliente envia uma mensagem usando o Ajax, o Servidor a re-envia para todos os Clientes (inclusive o autor da mensagem) usando Websockets. Eis o arquivo de rota "index.js" que faz isso:

var WebSocket = require('ws');
var uuid = require('node-uuid');

exports.index = function(req, res){
  var identificador = uuid.v1();
  res.render('index', { title: 'Msg app', 
                        'msg': mensagem.texto ,
                        'uuid' : identificador});
};

exports.novamsg = function(req, res){
  mensagem.texto = req.params.mensagem;
console.log('Conexoes: ' + conexoes.length);  
  for (var x=0;x<conexoes.length;x++) {
     console.log('Enviando msg para uuid: ' + conexoes[x].uuid);
     conexoes[x].send(mensagem.texto);
  }
  res.header("Content-Type", "application/json; charset=utf-8");
  res.json({ 'msg' : mensagem.texto });
};

Funciona muito bem! E note que estou misturando comunicação normal com Websocket sem problema algum. O Cliente poderia usar o próprio websocket para enviar mensagem, ou poderia enviar o UUID do Cliente que deveria recebê-la. Mas isso, eu deixo para você fazer.