Aplicações de mensageria em tempo real são aplicações cada vez mais comuns nas realidades das empresas e suas aplicações web. Inúmeras são as soluções de mercado que proveem esse tipo de recurso, mas a maioria delas são complexas, envolvem tecnologias server side, bem como bancos de dados e outras configurações mais. Neste artigo você verá como criar um web chat completo usando apenas jQuery, Socket.IO e suas abstrações dos WebSockets, no estilo UOL ou o antigo MIRC.

Criando um Chat com Node.js

É muito comum encontrarmos na internet artigos explicando a criação de sistemas de chats, mas a maioria faz uso de uma linguagem de servidor, algumas linhas de AJAX e um banco de dados, fora as inúmeras linhas de código. Normalmente esse tipo de projeto exige uma série de conhecimentos em linguagens de back-end, front-end e um pouco de AJAX.

Com a criação da plataforma Node.js, foi possível que surgissem também servidores web nos mais diversos protocolos de comunicação (HTTP, HTTPS, FTP, dentre vários outros) e dentre um deles, está o WebSocket, um protocolo de comunicação suportado por browsers exatamente com o propósito de estabelecer entre o navegador e o servidor uma comunicação bidirecional e em tempo real, possibilitando uma troca de mensagens mais ágil sem o refresh de página e um tempo de espera demorado.

Isso não lembra um pouco o AJAX? Infelizmente o WebSocket não é suportado por todas as versões dos browsers do mercado como o AJAX, mas as últimas versões do Internet Explorer, Firefox, Safari, Opera e Google Chrome já o suportam. No caso de navegadores mais antigos, seria necessário programar em protocolos similares como um plano B. Ao mesmo tempo, se desenvolvêssemos uma aplicação deste tipo puramente com o Node.js teríamos um trabalho muito maior.

Visando atingir a todos esses problemas, foi criado o módulo do Node.JS, que cuida de todos os protocolos de transporte que podem servir de plano B para os navegadores mais antigos. Que seriam os seguintes (nesta ordem): Adobe Flash Socket, AJAX long polling, AJAX multipart streaming, Forever iframe e o JSONP Polling. Opção é o que não falta, então o próprio modulo fica a cargo de realizar a comunicação com o servidor pelo protocolo de transporte que lhe for mais conveniente.

Saiba mais Série Programe com o Node.js

Portanto, neste artigo vamos criar um projeto para conhecermos o básico do Socket.IO e o que ele pode nos oferecer, sem o uso de banco de dados, AJAX e alguma outra linguagem back-end, usando apenas JavaScript, o Node.js e o jQuery.

Nosso Projeto

Antes de entrarmos nos detalhes técnicos, vamos entender o projeto que realmente vamos desenvolver no decorrer deste artigo. Trata-se de um aplicativo de salas de chat simples, igual ao antigo e popular serviço do MIRC e as salas de bate-papo dos antigos provedores de internet.

Os usuários primeiro irão se deparar com uma tela para inserir o apelido, assim que for inserido um válido (que não tenha nenhum outro usuário com o mesmo nome) será apresentada a tela de chat, com um campo de mensagem, um botão enviar, um painel onde aparecerão todas as mensagens e uma lista com o nome de cada um dos usuários.

Mas para não ficarmos dando rodeios em linhas intermináveis de instruções de código sem sentido, vamos realizar o projeto de forma iterativa, começando com uma aplicação bastante simples e funcional. Conforme as etapas forem completadas, vamos adicionar mais alguma funcionalidade, para assim termos um aprendizado onde acompanhamos o passo a passo do projeto já fazendo os testes de tudo.

Um pouco sobre Socket.io

Este módulo do Node.js traz uma forma de conexão direta do browser do cliente com o servidor de aplicação. A biblioteca funciona através de eventos, ou seja, o servidor ou o cliente irão disparar eventos para que haja respostas de uma das partes, veja uma exemplificação na Figura 1.

Troca de
mensagens entre cliente e servidor
Figura 1. Troca de mensagens entre cliente e servidor

De certa forma, vamos usar dois métodos muitos básicos do módulo, que são o emit e o on. Um serve para efetuar a emissão do evento e o outro para receber a resposta do mesmo. Cada um dos lados da aplicação, portanto, terão a biblioteca Socket.IO adicionada.

Além de permitir a troca direta de mensagens entre dois dispositivos, o Socket.IO também permite o broadcast de mensagens, o envio de um evento a todos os outros usuários conectados. O broadcast pode ser tanto do cliente quanto do servidor, conforme demonstrado na Figura 2.

Broadcast de mensagem enviada pelo servidor
Figura 2. Broadcast de mensagem enviada pelo servidor

Quando o usuário acessar a página, um socket é criado com o servidor e é através deste socket que é realizada a troca de mensagens entre um cliente e um servidor. Este, por sua vez, pode tanto emitir um evento para um único Socket como para todos os sockets conectados a ele, o que chamamos de broadcast de mensagem.

Preparando o ambiente

Caso você não tenha ainda o ambiente do Node.JS preparado, vamos então ver juntos nesta seção como deixar tudo pronto para começarmos a desenvolver aplicações do tipo. É um processo simples, já que vamos apenas instalar o Node.js, neste artigo não vamos precisar de banco de dados nem qualquer outra aplicação. Tudo se resumirá a um editor de texto simples e um terminal (ou prompt) do seu sistema operacional.

Windows/MAC

Acesse o site oficial do Node.js (vide seção Links) e baixe o arquivo de instalação de extensão msi para o seu tipo de Windows (32 ou 64 bits), ou o arquivo .pkg para Mac. Abra o arquivo e execute a instalação normalmente. Ao final acesse o terminal (ou o prompt de comando) e execute o comando node –v (igual ao mostrado na Figura 3) e se a resposta for a versão instalada do Node.js isso quer dizer que o ambiente já está pronto.

Linux

Para o Linux, nas distribuições Debian e Fedora, o processo é bastante simples: a instalação é realizada pelo repositório de aplicações. Vejamos primeiro os comandos a serem executados no terminal do Ubuntu (válidos também para distribuições Debian), como mostra a Listagem 1. Na Listagem 2 vemos os comandos para as distribuições Fedora.

Listagem 1. Comandos para instalação no Linux Ubuntu (Distribuições Debian).

  sudo apt-get update
  sudo apt-get install Node.js
  sudo apt-get install npm

Listagem 2. Comandos de instalação para distribuições Fedora

  sudo curl --silent --location https://rpm.nodesource.com/setup | bash -
  sudo yum -y install Node.js
  sudo yum –y install npm

Ao final do processo, basta acessar o terminal e digitar o comando Node.js –v: se a resposta for a versão instalada do Node.js então nosso ambiente já está pronto para começarmos. O segundo comando de instalação após o Node.js é referente ao NPM (Node.js Package Manager), um sistema para o gerenciamento de pacotes que é muito importante no desenvolvimento de aplicações Node.js.

Caso você tenha outra distribuição Linux, como OpenSuse, Gentoo, Arch, acesse a página de ajuda para instalação do Node.js no Linux (confira a seção Links). É importante também mencionar que no artigo colocamos o comando “node” em vez de “Node.js” por este ser o comando padrão do Windows. Tenha em mente que sempre que precisarmos inserir o comando node utilize o Node.js.

Conferindo a versão do Node.js
Figura 3. Conferindo a versão do Node.js

Começando o projeto

Para começar vamos criar um diretório chamado ChatJS onde lhe for mais conveniente, mas que seja de fácil acesso pelo prompt. É recomendável colocar o diretório do projeto no diretório /var/www/ (ou no C:\www\ para usuários Windows). Agora que já temos um diretório vamos criar um arquivo chamado app.js, que será o arquivo principal do nosso servidor.

Como primeira parte vamos criar um servidor bastante simples que só vai apresentar na tela do navegador uma mensagem de sucesso, como mostra a Listagem 3.

Listagem 3. Criando uma aplicação de servidor.

  var app = require('http').createServer(resposta);
  app.listen(3000);
  console.log("Aplicação está em execução...");
  function resposta (req, res) {
       res.writeHead(200);
       res.end("Ola, o servidor esta funcionando corretamente.");
  }

O script cria um servidor HTTP (que estará escutando a porta 3000) que tem como método principal a ser requisitado a função resposta, que tem dois parâmetros: req (de requisição) e res (de resposta). Na resposta definimos um código 200 de sucesso e finalizamos a mesma com uma string avisando que o servidor está ok.

Logo após, vamos rodar o comando a seguir, que irá executar nossa aplicação no prompt cmd:

node app.js

Repare que quando executar este código no prompt ele não irá imprimir nenhuma outra linha, isto indica que nossa aplicação está em execução no momento.

Neste momento temos apenas o nosso servidor Node.js rodando, inclusive o terminal apresentou o conteúdo da função console.log avisando que a aplicação está em execução, conforme apresentado na Figura 4. Se você acessar no browser o endereço http://localhost:3000/ ele só irá mostrar a mensagem que passamos no método end, como podemos observar na Figura 5.

Mensagem que passamos via console.log
Figura 4. Mensagem que passamos via console.log
Resposta da aplicação no browser
Figura 5. Resposta da aplicação no browser

Em seguida, vamos fazer nosso servidor apresentar uma resposta em HTML e que será a página principal do nosso chat. Para isso teremos de carregar o módulo do FileSystem, já que vamos navegar no diretório do projeto e abrir um arquivo. Portanto, vamos alterar o nosso app.js para que fique conforme a Listagem 4. Antes de realizar as alterações, vá até o prompt e pressione Ctrl + C (ou command + C) para terminar a execução da nossa aplicação no servidor.

Listagem 4. Apresentando uma página HTML.

  var app = require('http').createServer(resposta);
  var fs = require('fs');
   
  app.listen(3000);
  console.log("Aplicação está em execução...");
  function resposta (req, res) {
       fs.readFile(__dirname + '/index.html',
       function (err, data) {
           if (err) {
                res.writeHead(500);
                return res.end('Erro ao carregar o arquivo index.html');
           }
   
           res.writeHead(200);
           res.end(data);
       });
  }

Após estas alterações vamos novamente executar o comando node app.js no prompt. Ao acessarmos novamente o endereço http://localhost:3000/ vamos nos deparar com a mensagem “Erro ao carregar o arquivo index.html” (Figura 6) isso por que ainda não temos um arquivo index.html dentro do nosso projeto.

Mensagem de erro para arquivo HTML não encontrado
Figura 6. Mensagem de erro para arquivo HTML não encontrado

É importante lembrar também que o servidor que criamos, até então, não diferencia o caminho, ou seja, você pode passar depois de http://localhost:3000/ qualquer coisa que ele sempre irá responder da mesma forma porque não implementamos um modo de tratar estes caminhos. Logo, você pode muito bem chamar endereços como http://localhost:3000/chat, http://localhost:3000/erro, http://localhost:3000/batata, etc., que qualquer requisição que o servidor receber irá responder com o mesmo método (a função que chamamos de resposta, neste caso).

Vamos então criar uma interface bastante simples para o nosso chat. Crie um arquivo index.html dentro do diretório do projeto (diretório ChatJS). Neste arquivo insira um código igual ao demonstrado na Listagem 5.

Listagem 5. Código HTML da aplicação de chat.

  <!DOCTYPE html>
  <html>
  <head>
       <title>ChatJS - FrontEnd Magazine - DevMedia</title>
       <link rel="stylesheet" type="text/css" href="/css/style.css" />
  </head>
  <body>
       <div id="historico_mensagens"></div>
       <form id=’chat’>
           <input type='text' id='texto_mensagem' name='texto_mensagem' />
           <input type='submit' value='Enviar mensagem!' />    
       </form>
  </body>
  </html>

Nosso index, por enquanto, só vai contar com uma div chamada historico_mensagens que é onde estarão dispostas todas as mensagens trocadas no chat e logo depois um formulário com uma caixa de texto e o botão de envio de mensagem. Uma estrutura bastante simples de chat até o momento.

Entretanto, se você agora tentar acessar o endereço http://localhost:3000/ irá receber a mesma mensagem de erro. Isso acontece porque não reiniciamos nossa aplicação de servidor, então mais uma vez vamos até o prompt, pressionamos Ctrl + C e depois reexecutamos o comando node app.js.

Acostume-se com este procedimento sempre que realizar alterações de código no arquivo app.js. Já nos arquivos HTML e CSS não é preciso fazer isto porque eles atualizam com o refresh de página automaticamente. Agora que reiniciamos a aplicação de servidor a nossa página HTML está funcionando conforme a Figura 7.

Página HTML sem estilo CSS
Figura 7. Página HTML sem estilo CSS

Como você deve ter percebido, já deixamos uma tag link na tag <head> da nossa aplicação para carregarmos o nosso CSS. Vamos então criá-lo para que fique com um design mais próximo de um chat. Dentro do diretório do nosso projeto crie um outro diretório chamado css e dentro dele crie o arquivo style.css com o conteúdo igual ao demonstrado na Listagem 6.

Listagem 6. Conteúdo do arquivo style.css.

  html, body{
       font-family: Arial, Tahoma, sans-serif;
       margin: 0;
       padding: 0;
  }
  body{
       background:#302F31;
       padding:10px;
  }
   
  form{
       margin:15px 0;
  }
  form input[type='text']{
       border:2px solid #45C5BF;
       border-radius: 5px;
       padding:5px;
       width:75%;
  }
  form input[type='submit']{
       background: #45C5BF;
       border:none;
       border-radius: 5px;
       color:#FFF;
       cursor:pointer;
       font-weight: bold;
       padding:7px 5px;
       width:19%;
  }
  #historico_mensagens{
       background: #FFF;
       border:2px solid #45C5BF;
       height: 550px;
  }

Se reiniciarmos a aplicação Node.js, o estilo ainda não estará aplicado à página index. A razão disso é que o nosso app.js só trata de um path de requisição até o momento. Para resolver isso vamos alterar o nosso arquivo app.js para que ele carregue os arquivos que são passados na URL da solicitação, ao invés de colocarmos cada uma das URLs manualmente. Vamos conferir melhor as alterações apontadas na Listagem 7.

Listagem 7. Alterações de caminhos no app.js.

 
  var app = require('http').createServer(resposta);
  var fs = require('fs');
   
  app.listen(3000);
  console.log("Aplicação está em execução...");
   
  function resposta (req, res) {
       var arquivo = "";
       if(req.url == "/"){
           arquivo = __dirname + '/index.html';
       }else{
           arquivo = __dirname + req.url;
       }
       fs.readFile(arquivo,
           function (err, data) {
                if (err) {
                     res.writeHead(404);
                     return res.end('Página ou arquivo não encontrados');
                }
   
                res.writeHead(200);
                res.end(data);
           }
       );
  }

Se reiniciarmos a aplicação node, desta vez o nosso sistema reconhecerá o estilo CSS que criamos anteriormente, conforme mostra a Figura 8.

Layout da aplicação
Figura 8. Layout da aplicação

Enviando mensagens

Agora temos o servidor funcionando, o estilo CSS na nossa página e toda a estrutura HTML pronta. Vamos a partir de agora trabalhar na função de envio de mensagens. Nossa aplicação vai funcionar se comunicando com o servidor node através da biblioteca client-side do Socket.IO com o jQuery fazendo a interação com a página.

Para isso vamos alterar o arquivo app.js, como demonstrado na Listagem 8, e incluir uma linha de um comando require logo no começo do arquivo informando que estamos incluindo na aplicação o Socket.IO e que o módulo será armazenado na variável socket.

Listagem 8. Incluindo o módulo Socket.IO.

 
  var app = require('http').createServer(resposta);
  var fs = require('fs');
  var io = require('socket.io')(app);
  ...

Podemos ver que o require claramente chama o módulo socket.io e estamos passando a variável app (referente ao nosso servidor) no require do módulo a fim de facilitarmos uma parte do nosso desenvolvimento que veremos mais à frente.

Porém, para darmos um require em um módulo precisamos instalar o módulo na nossa aplicação. Para isso acessamos o terminal finalizando nossa aplicação Node.js com o Ctrl + C (ou command + C) e inserindo o seguinte código:

npm install socket.io

Logo depois de instalar rode o comando para iniciar nossa aplicação novamente e acesse o endereço http://localhost:3000/ para verificar se está tudo ok com a aplicação. Por enquanto nada estará funcionando porque não criamos nenhuma funcionalidade. Adicione também, antes do fechamento da tag de body do arquivo index.js, duas tags <script>, conforme a Listagem 9, com as nossas bibliotecas que realizarão as principais funções do chat. O jQuery facilitará o nosso processo de desenvolvimento e o Socket.IO para o client-side.

Listagem 9. Importando bibliotecas para nossa aplicação client-side

  …    
  <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
       <script type="text/javascript" src="/socket.io/socket.io.js"></script>
  </body>
  </html>

Você deve estar se perguntando o porquê de colocarmos uma tag script com o caminho para o Socket.IO que não existe, mas não se preocupe, pois a biblioteca Socket.IO vai entender este caminho automaticamente e irá trazer para nossa aplicação a biblioteca client-side por si só.

Lembre-se que já passamos antes aquela variável app no nosso require, exatamente para que a nossa aplicação se integre melhor com o módulo. Agora que está tudo pronto vamos ao envio e recebimento de mensagens. Primeiramente, vamos abrir uma tag script (desta vez sem o atributo src) onde estará todo o nosso código do lado cliente da nossa aplicação. Para começar temos que programar um evento de envio de mensagem, criando assim uma função que será atrelada ao submit do formulário de mensagem, como mostram as alterações na Listagem 10.

Listagem 10. Evento de envio de mensagens.

  ...
  <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
  <script type="text/javascript" src="/socket.io/socket.io.js"></script>
  <script type="text/javascript">
           var socket = io.connect();
   
           $("form#chat").submit(function(e){
                e.preventDefault();
                socket.emit("enviar mensagem", $(this).find("#texto_mensagem").val(), function(){
                     $("form#chat #texto_mensagem").val("");
                });
           });
   
  </script>
  </body>
  </html>

No código JavaScript da página declaramos uma variável socket que é referente à biblioteca Socket.IO, que será responsável por todas as funcionalidades do socket. A seguir declaramos um evento submit do nosso formulário em jQuery e passamos um preventDefault< para que o formulário não prossiga ao action do formulário, já que nós é quem vamos cuidar da resposta do formulário.

Em seguida, podemos ver que é invocado o método emit da biblioteca, no qual passamos como parâmetros três coisas: o nome do evento (isso será útil no servidor), os dados que estamos emitindo (no caso só estamos enviando o conteúdo do campo mensagem) e por último o call-back, uma função que vai ser executada assim que o evento for emitido. Este último, em específico, servirá apenas para limpar o campo de mensagem, assim o usuário não tem que ficar excluindo a mensagem depois que mandá-la.

Se testarmos agora nossa aplicação (reiniciando-a e acessando o http://localhost:3000) o envio de mensagens não vai funcionar, nem mesmo o call-back para limpar o campo de mensagem porque ainda não colocamos a funcionalidade do que o servidor tem que fazer assim que receber este evento. Para isso, edite o arquivo app.js colocando o código mostrado na Listagem 11 no fim do mesmo.

Listagem 11. Recebendo mensagens do cliente.

  ...
  io.on("connection", function(socket){
       socket.on("enviar mensagem", function(mensagem_enviada, callback){
           mensagem_enviada = "[ " + pegarDataAtual() + " ]: " + mensagem_enviada;
   
           io.sockets.emit("atualizar mensagens", mensagem_enviada);
           callback();
       });
  });
  function pegarDataAtual(){
    var dataAtual = new Date();
    var dia = (dataAtual.getDate()<10 ? '0' : '') + dataAtual.getDate();
    var mes = ((dataAtual.getMonth() + 1)<10 ? '0' : '') + (dataAtual.getMonth() + 1);
    var ano = dataAtual.getFullYear();
    var hora = (dataAtual.getHours()<10 ? '0' : '') + dataAtual.getHours();
    var minuto = (dataAtual.getMinutes()<10 ? '0' : '') + dataAtual.getMinutes();
    var segundo = (dataAtual.getSeconds()<10 ? '0' : '') + dataAtual.getSeconds();
   
    var dataFormatada = dia + "/" + mes + "/" + ano + " " + hora + ":" + minuto + ":" + segundo;
    return dataFormatada;
  }

Criamos um método que atuará em resposta à conexão do cliente ao servidor. Quando o cliente acessa a página ela dispara este método no servidor e quando este socket receber um método Enviar Mensagem acionamos um método que tem como parâmetros os dados enviados (o campo mensagem) e o call-back que criamos no lado cliente.

Dentro deste método colocamos a segunda parte da funcionalidade: o módulo vai emitir para todos os sockets conectados com o servidor (todos os usuários, por assim dizer) o evento Atualizar Mensagens e passará também qual mensagem nova foi enviada, com uma formatação de data e hora entre colchetes. Para fornecer a data e hora criamos uma função a parte porque ainda utilizaremos este método mais algumas vezes ao longo do desenvolvimento. Logo em seguida chamamos o call-back que criamos no lado cliente, que é o método para limpar os campos.

Finalmente, edite também o arquivo index.html e crie o método que vai atualizar as mensagens para os usuários. A ideia é bem simples: vamos dar um append na div historico_mensagens (as alterações se encontram na Listagem 12). As linhas a seguir devem ser inseridas logo depois do processamento do submit do formulário.

Listagem 12. Atualizando histórico de mensagens.

  ...
  $("form#chat").submit(function(e){
       // Conteúdo da função 
  });
  socket.on("atualizar mensagens", function(mensagem){
  var mensagem_formatada = $("<p />").text(mensagem);
       $("#historico_mensagens").append(mensagem_formatada);
  });

O que percebemos aqui é que basicamente a conversa entre o servidor e o cliente é igual dos dois lados, isto é, os dois possuem eventos emit para emissão de eventos, e on para recepção de eventos. Acessando a aplicação no http://localhost:3000/ em duas abas (não se esqueça de reiniciar o aplicativo no prompt ou terminal antes de acessar no browser) é só enviar uma mensagem e ver o poder do Socket.IO em ação. A aplicação deve apresentar a mensagem como mostra a Figura 9.

Envio de mensagens
Figura 9. Envio de mensagens

A nossa aplicação ainda não atende a todas as necessidades, pois ainda não temos nomes de usuário, então a coisa fica um tanto quanto anônima, mas vamos cuidar disso agora.

Dando nomes aos usuários

Toda aplicação de chat recebe o usuário com um formulário para inserir o apelido e nesta seção vamos criar um formulário simples onde o visitante vai colocar seu nome de usuário. Caso já exista alguém com o mesmo nome vamos então apresentar uma tela de erro avisando ao nosso visitante, do contrário, apresentaremos a tela de chat normalmente.

Para não ficarmos repetindo código e criar mais um arquivo HTML vamos programar nossa página index para que apresente primeiro o formulário de apelido. Se tudo der certo, a página esconde o formulário e apresenta a sala de chat, assim ganhamos tempo e linhas de código para aproveitar mais do nosso projeto.

Vamos começar então pela edição do arquivo index.html, embrulhando nossa sala de chat em uma div com o id sala_chat. Crie também outra div com o id acesso_usuario com um formulário bastante simples contendo apenas o campo de nome de usuário, conforme mostrado na Listagem 13.

Listagem 13. Adaptando a página principal para login de usuário.

  ...
  <div id='acesso_usuario'>
       <form id=’login’>
           <input type='text' placeholder='Insira seu apelido' name='apelido' id='apelido' />
           <input type='submit' value='Entrar' />
       </form>
  </div>
  <div id='sala_chat'>
       <div id="historico_mensagens"></div>
       <form id=’chat’>
           <input type='text' id='texto_mensagem' name='texto_mensagem' />
           <input type='submit' value='Enviar mensagem!' />    
       </form>
  </div>
  ...

Com o formulário inserido na página ainda precisamos editar o style. Portanto, faça as alterações vistas na Listagem 14.

Listagem 14. Alterando o CSS da aplicação.

  ...
  #sala_chat{
       display: none;
  }
  #acesso_usuario{
       height:30px;
       left:50%;
       margin-left:-160px;
       margin-top:-15px;
       position: fixed;
       top:50%;
       width:320px;
  }
  #acesso_usuario form{
       margin:0;
  }

Se atualizarmos agora o navegador (como é alteração de HTML e CSS não é necessário reiniciar a aplicação rodando no nosso prompt) vemos que agora nossa aplicação só mostra um formulário como pretendíamos, mas obviamente ele ainda não funciona, como mostra a Figura 10.

Formulário de entrada
Figura 10. Formulário de entrada

Lembrando-se da implantação do nosso sistema de envio de mensagem podemos imaginar como vai funcionar o lado cliente desta funcionalidade, não? Vamos dar emit em um evento chamado entrar, que por sua vez, vai mandar para o servidor o nome do usuário que foi digitado. Já no retorno do servidor como true vamos simplesmente dar um hide no form e apresentar a nossa sala de chat.

Agora vejamos quais são as alterações que devemos fazer no nosso arquivo index.html, conforme a Listagem 15.

Listagem 15. Implantando funções client-side de acesso de usuário.

  ...
  $("form#login").submit(function(e){
       e.preventDefault();
   
       socket.emit("entrar", $(this).find("#apelido").val(), function(valido){
           if(valido){
                $("#acesso_usuario").hide();
                $("#sala_chat").show();
           }else{
                $("#acesso_usuario").val("");
                alert("Nome já utilizado nesta sala");
           }
       });
  });
  ...

Nosso sistema agora emite o evento para o servidor, logo precisamos validar se o nome está disponível e depois armazená-lo junto aos nomes dos demais usuários. Portanto, vamos fazer as alterações conforme demonstrado na Listagem 16.

Listagem 16. Implantação da verificação de acesso dos usuários.

  var io = require('socket.io')(app);
  var usuarios = [];
  ...
  io.on("connection", function(socket){
  socket.on("entrar", function(apelido, callback){
           if(!(apelido in usuarios)){
  socket.apelido = apelido;
                usuarios[apelido] = socket;
                callback(true);
           }else{
                callback(false);
           }
       });
       socket.on("enviar mensagem", function(mensagem_enviada, callback){
           mensagem_enviada = "[ " + pegarDataAtual() + " ] " + socket.apelido + " diz: " + mensagem_enviada;
           io.sockets.emit("atualizar mensagens", mensagem_enviada);
           callback();
       });
  });

No começo podemos ver a declaração de um vetor chamado usuarios, que vai ser nossa base de nomes, uma estrutura bastante simples para armazenar apenas o nome de cada um que acessar nossa aplicação. Logo depois colocamos um método de resposta ao evento que criamos no client-side. Nos parâmetros passamos apenas o nome do usuário e o método call-back que criamos anteriormente.

Então fazemos uma verificação simples se o usuário consta no nosso vetor de nomes de usuários e se não constar primeiro criamos um atributo apelido dentro do socket, para assim acessarmos depois este valor em outros métodos. Também adicionamos um índice com o nome do usuário e armazenamos o socket, assim, podemos depois resgatar o mesmo através do nome de usuário; isso será útil mais adiante.

Além disso, também mudamos o formato da mensagem, colocamos o apelido do usuário que mandou a mensagem antes do texto propriamente dito. Confira como ficaram as alterações reiniciando a aplicação no prompt e posteriormente acessando o servidor local (http://localhost:3000/).

Agora vamos criar uma lista de usuários para sabermos quem está na nossa sala de chat e também colocar o nome do usuário na mensagem emitida, assim sabemos quem disse o quê.

Veja na Figura 11 como irá ficar nossa lista de usuários, disposta ao lado direito do nosso painel de mensagens.

Lista de usuários
Figura 11. Lista de usuários

Para isto, devemos primeiro alterar o nosso index.html conforme a Listagem 17, colocando um select de tipo multiple (onde estará o nome dos usuários) que ficará ao lado direito do painel principal da conversa. Além do select precisamos também colocar a função que vai ser acionada quando o servidor emitir um evento para atualizar os usuários da lista.

Listagem 17. Adicionando lista de usuários e nome de usuário na mensagem enviada.

  ...
  <div id="historico_mensagens"></div>
  <select multiple="multiple" id='lista_usuarios'><option value=''>Todos</option></select>
  <form id='chat'>
  ... 

Se atualizarmos o browser agora veremos que o design da página não ficou bom, por isso vamos alterar mais um pouco o nosso CSS conforme apresentado na Listagem 18. Para isso altere o estilo da div historico_mensagens, colocando junto o estilo da lista de usuários com alguns atributos novos e inserindo mais alguns trechos de código ao final do arquivo style.css.

Listagem 18. Alterando o estilo da aplicação.

  ...
  #historico_mensagens, #lista_usuarios{
       background: #FFF;
       border:2px solid #45C5BF;
       height: 550px;
       float:left;
       margin-bottom: 10px;
       width:75%;
  }
  ...
  #historico_mensagens{
       border-right: 0;
  }
  #lista_usuarios{
  border-left: 1px solid #45C5BF;
       height: 554px;
       width: 20%;
  }

Por fim, na Listagem 19 vamos emitir o evento pelo servidor para atualizar a lista de usuários e mandar uma mensagem para todos avisando que um novo usuário entrou na sala.

Listagem 19. Emitindo atualização de lista de usuários.

  ...
  socket.on("entrar", function(apelido, callback){
       if(!(apelido in usuarios)){
           socket.apelido = apelido;
           usuarios[apelido] = socket;
   
           io.sockets.emit("atualizar usuarios", Object.keys(usuarios));
           io.sockets.emit("atualizar mensagens", "[ " + pegarDataAtual() + " ] " + apelido + " acabou de entrar na sala");
   
           callback(true);
       }else{
           callback(false);
       }
  });
  ...

Incluímos o código onde é emitido o evento para que todos os sockets avisem que a lista deve ser atualizada, além de termos passado todas chaves do nosso vetor de usuários. Logo em seguida também emitimos outro evento, que já utilizamos anteriormente, o atualizar mensagens. Desta vez especificamos a mensagem em si, passando o apelido do usuário.

Vamos colocar o método de atualização dos usuários na nossa página index.html, conforme a Listagem 20. O código jQuery deve ser inserido no final do conteúdo da tag <script>.

Listagem 20. Método de atualização da lista de usuários.

  ...
  socket.on("atualizar usuarios", function(usuarios){
  $("#lista_usuarios").empty();
  $("#lista_usuarios").append("<option value=''>Todos</option>");
       $.each(usuarios, function(indice){
            var opcao_usuario = $("<option />").text(usuarios[indice]);
            $("#lista_usuarios").append(opcao_usuario);
        });
  });

Agora nossa aplicação de chat está ficando com cara de uma verdadeira sala de chat dos portais de internet com poucas linhas de código e o melhor, não utilizamos banco de dados, apenas simples comunicação entre cliente e servidor.

Vamos implantar mais uma funcionalidade que vai ser disparada quando o usuário sair da sala. Para isto vamos mais uma vez alterar o nosso arquivo app.js com a alteração da Listagem 21 que será também colocada dentro do call-back do io connection.

Listagem 21. Tratamento para quando o usuário sair da sala.

  ...
  io.on("connection", function(socket){
       ...
    socket.on("disconnect", function(){
      delete usuarios[socket.apelido];
      io.sockets.emit("atualizar usuarios", Object.keys(usuarios));
      io.sockets.emit("atualizar mensagens", "[ " + pegarDataAtual() + " ] " + socket.apelido + " saiu da sala");
    });
  });
  ...

Desta vez não precisamos criar um emit para este evento porque ele é nativo do Socket.IO. Além dele, temos o connect (quando o socket realiza uma conexão com o servidor) e o message (para o envio de mensagens - não utilizamos este método aqui no artigo para entendermos exatamente como funciona a criação de eventos).

Na função de call-back do disconnect removemos o socket armazenado e o apelido da lista de usuários. Logo após atualizamos a lista de usuários dos clientes e enviamos uma mensagem avisando que o usuário saiu da sala.

Finalizando o projeto

Precisamos atribuir uma funcionalidade à nossa lista de usuários que está ao lado direito do nosso painel de mensagens. Quando tivermos algum nome selecionado enviaremos uma mensagem privada ao usuário. Trata-se de uma alteração simples porque irá seguir o mesmo estilo da função de envio de mensagem, mas ao invés de usarmos um emit para todos os sockets, vamos emitir apenas para um socket em especifico.

Temos de fazer uma alteração rápida também no método de emissão do evento "enviar mensagem", porque estamos enviando apenas a mensagem que o usuário escreveu, mas a partir de agora teremos de mandar a mensagem e o usuário que ele selecionou. Conforme a Listagem 22 vamos primeiro alterar o método submit do formulário de chat na página index.html.

Listagem 22. Alterando parâmetros a serem enviados no evento enviar mensagem.

  $("form#chat").submit(function(e){
       e.preventDefault();
   
       var mensagem = $(this).find("#texto_mensagem").val();
       var usuario = $("#lista_usuarios").val();
   
       socket.emit("enviar mensagem", {msg: mensagem, usu: usuario}, function(){
           $("form#chat #texto_mensagem").val("");
       });
  });

E agora no servidor vamos adicionar o processamento para o usuário que deve receber a mensagem especificamente, como mostra a Listagem 23.

Listagem 23. Enviando mensagem privada.

  socket.on("enviar mensagem", function(dados, callback){
           
     var mensagem_enviada = dados.msg;
     var usuario = dados.usu;
      if(usuario == null)
        usuario = ''; 
   
        mensagem_enviada = "[ " + pegarDataAtual() + " ] " + socket.apelido + " diz: " + mensagem_enviada;
   
         if(usuario == ''){
                io.sockets.emit("atualizar mensagens", mensagem_enviada);
         }else{
                socket.emit("atualizar mensagens", mensagem_enviada);
                usuarios[usuario].emit("atualizar mensagens", mensagem_enviada);
         }
   
         callback();
   });

No programa, se o usuário for vazio, a mensagem é para todos e então colocamos o código para o emit ser enviado para todos os sockets. Mas se for para um usuário em especifico vamos dar um emit no socket que está armazenado na lista de usuários com o usuário que foi enviado. Ele também será emitido para o próprio usuário que mandou a mensagem, para que a conversa fique visível para os dois.

Agora vamos reiniciar nossa aplicação no terminal, abrir três abas diferentes no nosso navegador, entrar na sala nas três abas e verificar se está tudo ok com a sala de chat. Envie uma mensagem privada para um dos três usuários e uma mensagem para todos. O usuário que mandou as duas mensagens irá vê-las. O usuário que recebeu a mensagem privada também irá visualizar duas mensagens e o terceiro irá ver apenas uma, que foi dita a todos.

Contudo, ainda faltam duas coisas para o nosso chat: um aviso dizendo que a mensagem é privada e uma diferenciação das mensagens privadas, mensagens públicas e mensagens do sistema. Uma vez que já estamos concentrados em uma única função no client-side, o método de atualizar mensagens, vamos fazer uma alteração bastante simples nele.

Na Listagem 24 podemos ver que a alteração consiste em apenas modificar os parâmetros de atualizar mensagens passando agora qual será a classe daquela mensagem: se é uma mensagem privada, uma mensagem de sistema, etc.

Listagem 24. Adicionando classes para cada tipo de mensagem.

  ...
  socket.on("atualizar mensagens", function(dados){
       var mensagem_formatada = $("<p />").text(dados.msg).addClass(dados.tipo);
       $("#historico_mensagens").append(mensagem_formatada);
  });
  ...

O lado ruim dessa implementação é que teremos de alterar todas as chamadas ao método atualizar mensagens. Devemos especificar, portanto, o tipo de mensagem enviada e na Listagem 25 temos como ficará alguns trechos do app.js após as alterações de chamada do evento.

Listagem 25. Alterando chamada do evento atualizar mensagens.

  ...
  io.sockets.emit("atualizar usuarios", Object.keys(usuarios));
  io.sockets.emit("atualizar mensagens", {msg: "[ " + pegarDataAtual() + " ] " + apelido + " acabou de entrar na sala", tipo: 'sistema'});
   
  callback(true);
  ...
  mensagem_enviada = "[ " + pegarDataAtual() + " ] " + socket.apelido + " diz: " + mensagem_enviada;
   
  if(usuario == ''){
           io.sockets.emit("atualizar mensagens", {msg: mensagem_enviada, tipo:'' });
  }else{
           socket.emit("atualizar mensagens", {msg: mensagem_enviada, tipo:'privada' });
           usuarios[usuario].emit("atualizar mensagens", {msg: mensagem_enviada, tipo:'privada' });
  }
  callback();
  });
   
  socket.on("disconnect", function(){
       delete usuarios[socket.apelido];
       io.sockets.emit("atualizar usuarios", Object.keys(usuarios));
       io.sockets.emit("atualizar mensagens", {msg: "[ " + pegarDataAtual() + " ] " + socket.apelido + " saiu da sala", tipo: 'sistema'});
  });

Se reiniciarmos a aplicação e acessarmos no browser, ao inspecionar as mensagens com a ferramenta de desenvolvedores, veremos que agora as mensagens estão com as devidas classes. Então vamos alterar o nosso arquivo style.css para termos uma diferenciação visual, como mostra a Listagem 26.

Listagem 26. Alterações no estilo das mensagens.

  ...
  #historico_mensagens .sistema{
       background-color: #45C5BF;
       color: #FFF;
       font-weight: bold;
  }
  #historico_mensagens .privada{
       background-color: #CCC;
       color: #000;
       font-weight: bold;
  }

O resultado da nossa alteração de CSS vai ficar conforme a Figura 12. Lembrando que você pode modificar o CSS à vontade.

Padrão das mensagens de chat
Figura 12. Padrão das mensagens de chat

Outra coisa que deixa a desejar no sistema de chat é um histórico breve de mensagens assim que o usuário entra na sala. Seria bacana ele já ter pelo menos as últimas cinco mensagens enviadas na sala antes do mesmo entrar. Podemos fazer isto criando outra variável, abaixo da variável usuarios chamada ultimas_mensagens, que será um array. E no evento de enviar mensagens vamos inserir a mensagem no final do vetor e, caso o vetor tenha mais de cinco mensagens armazenadas, removemos a mais antiga.

Todas as alterações vão se concentrar no arquivo app.js, mas vamos fazer por partes a alteração. Primeiramente, criaremos a variável das mensagens e uma função que vai armazenar as mesmas nesta mesma variável, conforme demonstrado na Listagem 27. A primeira parte vai logo no começo do script e a função será colocada no final do arquivo, logo depois da função pegarDataAtual.

Listagem 27. Guardando mensagens em um histórico.

  var app = require('http').createServer(resposta);
  var fs = require('fs');
  var io = require('socket.io')(app);
  var usuarios = [];
  var ultimas_mensagens = [];
  ...
  function armazenaMensagem(mensagem){
       if(ultimas_mensagens.length > 5){
           ultimas_mensagens.shift();
       }
   
       ultimas_mensagens.push(mensagem);
  }

É uma função bastante simples, se o tamanho do vetor for maior que 5, é retirado o primeiro valor da estrutura, e depois é colocado no final dela a mensagem que foi passada por parâmetro.

Agora precisamos fazer com que todos os eventos que emitem o evento de atualizar mensagens chamem esta função passando como parâmetro a mensagem em si. Vejamos os trechos a serem alterados no app.js primeiramente no evento de envio de mensagem, previsto na Listagem 28.

Listagem 28. Chamando a função para guardar mensagens quando elas são enviadas.

  socket.on("enviar mensagem", function(dados, callback){
       var mensagem_enviada = dados.msg;
       var usuario = dados.usu;
       if(usuario == null)
           usuario = '';
   
       mensagem_enviada = "[ " + pegarDataAtual() + " ] " + socket.apelido + " diz: " + mensagem_enviada;
       var obj_mensagem = {msg: mensagem_enviada, tipo: ''};
   
       if(usuario == ''){
           io.sockets.emit("atualizar mensagens", obj_mensagem);
           armazenaMensagem(obj_mensagem);
       }else{
           obj_mensagem.tipo = 'privada';
           socket.emit("atualizar mensagens", obj_mensagem);
           usuarios[usuario].emit("atualizar mensagens", obj_mensagem);
       }
       callback();
  });

Tais alterações ajudarão a deixar o código mais enxuto e sem repetições. Vamos analisar o passo a passo: primeiro criamos um objeto chamado obj_mensagem, onde ficará armazenada a mensagem e o tipo dela; é importante armazenar o tipo para depois diferenciarmos quando for preciso apresentar o histórico para o usuário que acabou de entrar na sala.

No else da nossa condicional, que diferencia se a mensagem é privada ou não, alteramos o valor do tipo de vazio para privada. E por fim armazenamos no nosso vetor a mensagem enviada pelo usuário. Agora na Listagem 29 alteramos o método que é chamado na saída do usuário da sala de bate-papo, afinal este método também emite uma mensagem e seria interessante guardarmos no histórico.

Listagem 29. Armazenando mensagem de saída da sala.

  socket.on("disconnect", function(){
       delete usuarios[socket.apelido];
       var mensagem = "[ " + pegarDataAtual() + " ] " + socket.apelido + " saiu da sala";
       var obj_mensagem = {msg: mensagem, tipo: 'sistema'};
       
       io.sockets.emit("atualizar usuarios", Object.keys(usuarios));
       io.sockets.emit("atualizar mensagens", obj_mensagem);
   
  armazenaMensagem(obj_mensagem);
  });

Aqui a alteração seguiu a mesma linha que na nossa Listagem 28, onde separamos os dados em um objeto, emitimos a mensagem como antes e por fim chamamos a função. Esta é a alteração mais simples até o instante.

Na Listagem 30 devemos alterar o método de entrada do usuário. Esta parte envolve mostrar as mensagens armazenadas no histórico e depois emitir a mensagem de que o usuário ingressou na sala.

Listagem 30. Alterando o método de entrada do usuário no chat.

  socket.on("entrar", function(apelido, callback){
       if(!(apelido in usuarios)){
           socket.apelido = apelido;
           usuarios[apelido] = socket;
   
           for(indice in ultimas_mensagens){
                socket.emit("atualizar mensagens", ultimas_mensagens[indice]);
           }
   
   
           var mensagem = "[ " + pegarDataAtual() + " ] " + apelido + " acabou de entrar na sala";
           var obj_mensagem = {msg: mensagem, tipo: 'sistema'};
   
           io.sockets.emit("atualizar usuarios", Object.keys(usuarios));
           io.sockets.emit("atualizar mensagens", obj_mensagem);
   
           armazenaMensagem(obj_mensagem);
   
           callback(true);
       }else{
           callback(false);
       }
  });

Dessa vez, primeiro imprimimos as mensagens anteriores e depois armazenamos a mensagem de entrada. Mas por que temos de fazer isto nesta ordem? A razão é para não mostrarmos duas vezes a mesma mensagem de que o usuário entrou na sala. Senão é dado um emit na mensagem de entrada.

Feitas as alterações, um novo usuário que entrar na sala terá um histórico breve das mensagens trocadas antes dele entrar na sala. Você pode alterar o valor das mensagens que devem ser exibidas, basta alterar o método que salva as mesmas. Abra duas abas no navegador e faça uma troca de mensagens, depois abra uma terceira para verificar se o terceiro usuário recebeu as cinco últimas mensagens da sala, conforme demonstrado na Figura 13.

Painel de mensagens com histórico
prévio
Figura 13. Painel de mensagens com histórico prévio

Com este projeto bastante simples e com poucas dependências e tecnologias conseguimos criar em poucas linhas uma aplicação bastante funcional e que serve bem ao propósito que estabelecemos. Se você gostou mesmo deste projeto, você pode estendê-lo, talvez colocando um painel de envio de emojis (o link para baixar a biblioteca está na seção Links), criando um painel de admin que consiga expulsar usuários, compartilhamento de imagens e vídeos dentre várias outras possibilidades.

Mas o poder do Socket.IO se estende a mais soluções que podem fazer com que sistemas que antes dependiam de páginas back-end e de um tempo de reposta de entrada e saída do servidor agora possam contar com o protocolo WebSocket para a troca mais ágil de informações. Possibilitando sistemas que acompanhem resultados de monitoramento ou até mesmo precificação em tempo real para os usuários.

Links Úteis

Saiba mais sobre Node.js ;)

Links
Site oficial do Socket.IO

http://socket.io/
Site oficial do Node.js para download da plataforma

https://Node.js.org/
Ajuda para instalação do Node.js no Linux
https://github.com/joyent/node/wiki/
Installing-Node.js-via-package-manager#enterprise-linux-and-fedora

Repositório GitHub do projeto deste artigo
https://github.com/Milleo/ChatJS
Biblioteca EmojiArea
https://github.com/diy/jquery-emojiarea