viernes, 18 de octubre de 2013

Chat sencillo con Node.js, Socket.io y HTML

Pre-requisitos


Para poder realizar esta guía debes tener instalado Node.js y npm previamente (luego realizare un post sobre esto). Además de tener un explorador con el cual podamos soportar javascript y html5.


Introducción


La idea de este post es tratar de explicar como se puede realizar un Chat simple utilizando Node.js, HTML, Javascript y Web Sockets. Se debe aclarar que esto es totalmente académico y por motivos de explicar las posibilidades que brinda Node.js para realizar aplicaciones en tiempo real. 

Quiero darle gracias especiales a Julian Duque (@julian_duque) por la conferencia en MedellinJS (@medellinjs) del uso de Web Sockets de la forma correcta. Aquí pueden ver la conferencia original de Julian Duque con la que me base para realizar este post. 

Que es WebSocket?


WebSocket según wikipedia "WebSocket es una tecnología que proporciona un canal de comunicación bidireccional y full-duplex sobre un único socket TCP". Y según el RFC-6455 "The WebSocket Protocol enables two-way communication between a client running untrusted code in a controlled environment to a remote host that has opted-in to communications from that code". 

El significado que yo interpreto es "WebSocket es un protocolo de comunicación que nos permite enviar y recibir información hacia y desde el servidor en un solo canal". Básicamente nos da la posibilidad de mantener una comunicación con el servidor y que este nos avise de algún cambio que haya ocurrido en nuestro back-end. Está herramienta nos permite realizar paginas mucho mas eficientes en el consumo de trafico de red, ya que antiguamente para estar consultando que estaba pasando en nuestro back-end debíamos realizar llamados periódicos a nuestro servidor usando Ajax y cuando encontráramos un cambio presentarlo en pantalla.

Por tanto el uso de WebSockets para la creación de paginas web de real time es una gran herramienta y abre el espectro para aplicaciones que tengan que estar revisando cambios en la Base de datos o en sistemas de archivo. 


Node.js , WebSockets, Socket.io y más..


En este momento para nadie debe ser un secreto que Node.js esta tomando mucha fuerza y esta permitiendo realizar aplicaciones realmente asombrosas. En este caso voy a explicar que posibilidades tenemos para usar WebSockets en nuestras aplicaciones Node.js.

WebSockets


En Node.js tenemos una librería que podemos usar para WebSocket que se llama "ws" (https://npmjs.org/package/ws) el cual dicen en su página de npm es posiblemente la mas rápida de todas las librerías realizadas para Node.js en el uso de WebSockets.

Ejemplo

Servidor
var WebSocketServer = require('ws').Server
  , wss = new WebSocketServer({port: 8080});
wss.on('connection', function(ws) {
    ws.on('message', function(message) {
        console.log('received: %s', message);
    });
    ws.send('something');
});
Cliente
var WebSocket = require('ws');
var ws = new WebSocket('ws://www.host.com/path');
ws.on('open', function() {
    ws.send('something');
});
ws.on('message', function(data, flags) {
    // flags.binary will be set if a binary data is received
    // flags.masked will be set if the data was masked
});

Socket.io


Aunque ws es probablemente el mecanismo mas eficiente para el uso de WebSockets, tiene limitaciones en cuanto el uso de los puertos, el envío de variables y el manejo de eventos personalizados. Por tanto hay librerías mucho mas completas y con herramientas que nos facilitan muchas operaciones. Es aquí donde entra Socket.io (https://npmjs.org/package/socket.io), que es probablemente la librería para el uso de WebSockets mas popular en Node.js.

Ejemplo

Servidor
var socketio = require('socket.io');

var http = require('http');

var fs = require('fs');

var server = http.createServer(function (req, res) {

  res.writeHead(200, { 'Content-type': 'text/html' });

  fs.createReadStream('./index.html').pipe(res);

});

server.listen(80);

var io = socketio.listen(server);

io.on('connection', function (socket) {

  socket.emit('Event', 'from server!');

  socket.on('Event', function (msg) {    console.log('Received: %s', msg);

    socket.emit('world');

  });

}); 
Cliente
<script src="/socket.io/socket.io.js"></script>
<script>
  var socket = io.connect('http://localhost');
  socket.on('Event', function (data) {
    console.log(data);
    socket.emit('otro evento', { my: 'data' });
  });
</script>


Engine.io


Todo indica que Socket.io es lo mejor para usar WebSockets en Node.js, pero se han presentado problemas en aplicaciones con mas de 100 usuarios concurrentes, es por esto que la comunidad ha intentado crear soluciones a estos problemas de concurrencia para la librería de Socket.io, por esto surge Engine.io.
Servidor
var engine = require('engine.io')
  , server = engine.listen(80)

server.on('connection', function (socket) {
  socket.send('utf 8 string');
});

Cliente
var engine = require('engine.io')
  , http = require('http').createServer().listen(3000)
  , server = engine.attach(http)

server.on('connection', function (socket) {
  socket.on('message', function () { });
  socket.on('close', function () { });
});

Manos a la obra


Lo primero que vamos hacer es instalar las dependencias que necesitamos para el chat, en este caso vamos a utilizar Socket.io. Entonces empecemos, creamos una carpeta y descargamos con npm las dependencias, para esto ingresamos a la consola de comandos y ejecutamos lo siguiente:

mkdir Chat
cd Chat
npm install socket.io
Una vez ejecutemos estos comandos vamos a tener un ambiente listo para desarrollar nuestra aplicación con socket.io en Node.js. Ahora nos dedicamos a crear el servidor del node.js que va manejar nuestro chat, para esto creamos un archivo llamado "chat.js" dentro de nuestra carpeta Chat con el siguiente código:
var socketio = require('socket.io'); var http = require('http'); var fs = require('fs'); var server = http.createServer(function (req, res) {   res.writeHead(200, { 'Content-type': 'text/html' });   fs.createReadStream('./index.html').pipe(res); }); server.listen(8080);
Como pueden ver estamos creando las variables socketio, http y fs. La variable de socketio nos va permitir usar la librería socketio, la variable http nos va permitir iniciar un servidor http donde vamos a exponer nuestra pagina web y la variable fs nos permite leer archivos del sistema. Para esto iniciamos nuestro servidor el cual va soportar html y leemos un archivo html "index.html" el cual vamos a usar como nuestra vista. Finalmente lo que le decimos a nuestro servidor es que se va ejecutar en el puerto 8080. Este es el código del archivo html que debemos guardar al mismo nivel del archivo "chat.js":
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml">     <head>         <title>WebSockets Chat Example</title>         <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"> </script>         <script src="/socket.io/socket.io.js"></script>         <style>             .leftSide{ float:left; min-width:25%; min-height:400px; margin:10px; padding:15px; border:1px solid #333;}             .rightSide{ float:right; min-width:65%;min-height:400px; margin:10px; padding:15px;border:1px solid #333; }         </style>     </head>     <body>         <h2>Welcome to the WebSockets Chat</h2>         <section class="user">             <label>User Name </label> <input type="text" id="user"/>             <input type="button" value="Join" id="btnJoin"/>         </section>         <section id="chat">             <section class="leftSide"></section>             <section class="rightSide"></section>             <section>                 <input type="button" value="Send" id="send" style="margin:10px; padding:10px;" />                 <input type="text" id="textSend" style="margin:10px;"/>             </section>         </section>     </body> </html>
Con esto ya podemos ver nuestra aplicación corriendo con tan solo correr el siguiente comando:
node chat.js


Como ven ya tenemos nuestra pagina web, solicitando un usuario para iniciar el chat, por el momento nuestra pagina no va hacer nada, para agregar la funcionalidad de nuestro chat vamos agregar el siguiente código al archivo "chat.js":
var users = [];//Con esto manejamos el listado de nuestros usuarios var io = socketio.listen(server);//Iniciamos la libreria con nuestro servidor
//Controlamos el evento cuando se conecte e imprimimos en consola el mensaje recibido
//emitiendo el evento hello hacia el cliente con un mensaje inicial. io.on('connection', function (socket) {   socket.on('hello', function (msg) {     console.log('Received: %s', msg);     socket.emit('hello', 'Hello from server');   });   socket.on('login', function (msg) {     console.log('Received Login: %s', msg);     socket.emit('login', {"user": "System","text": "Welcome to the chat"});     users.push(msg);   });
//Por defecto enviamos un mensaje de bienvenida para el evento message
  socket.emit('message', '<p class="message">Welcome from server!</p>');  
//Controlamos el evento message y replicamos el mensaje a todos los clientes
//que se encuentren conectados   socket.on('message', function (msg) {     console.log('Received message: %s', msg);     var text = '<p class="message"><b>'+msg.user+'</b>:'+msg.message+'</p>';     io.sockets.emit('message', {"user": msg.user,"text": text});   });  
//Controlamos el evento list que nos va servir para ver el listado de usuarios
//conectados   socket.on('list', function (msg) {     console.log('Received list: %s', msg);      socket.emit('list', users);   }); }); 
Ahora pasamos al lado del cliente donde vamos a consumir todos los eventos que acabamos de agregar a nuestro chat, además de algunos eventos para iniciar el chat y ocultar algunos controles en la vista. Para esto agregamos un elemento script en nuestro archivo "index.html" con el siguiente código:
//Inicializamos nuestras variables
var socket = null; var user = null;
$(document).ready(function(){ $("#chat").hide();//Ocultamos por defecto el layer del chat     //controlamos el evento del boton de iniciar chat
$("#btnJoin").click(function(){         socket = io.connect('http://localhost:8080');//iniciamos conexion con el server         user = $("#user").val();//guardamos el usuario insertado en memoria         socket.emit('login',user);//llamamos el evento login del server         //Controlamos el evento cuando el servidor responda
socket.on('login', function (msg) {         console.log('Received Login: %s', msg);     }); //Llamamos el evento de list para obtener los usuarios conectados
    socket.emit('list',"list");      //Controlamos la respuesta del server
socket.on('list', function (msg) {         console.log('Received List: %s', msg);         $(".leftSide").html(msg);     }); //Controlamos el evento message que enviará el servidor
    socket.on('message', function (msg) {         console.log('Received Message: %s', msg);         $(".rightSide").append(msg.text);     });     $("#chat").show();     $(".user").hide();
});
$("#send").click(function(){       socket.emit('message',{"user": user,"message": $("#textSend").val()}); });
});
Con esto ya tenemos la logica del servidor y la logica del cliente para ejecutar nuestro chat. Por lo que ejecutamos el comando:
node chat.js
Pueden hacer la prueba entrando desde pestañas diferentes al puerto 8080 de su localhost, utilizando diferentes exploradores o si quieren hacerlo exponiendo la dirección en una red. A continuación como quedaron cada nuestros dos archivos:

chat.js
var socketio = require('socket.io');
var http = require('http');
var fs = require('fs');
var server = http.createServer(function (req, res) {
  res.writeHead(200, { 'Content-type': 'text/html' });
  fs.createReadStream('./index.html').pipe(res);
});
server.listen(8080);


var users = [];//Con esto manejamos el listado de nuestros usuarios
var io = socketio.listen(server);//Iniciamos la libreria con nuestro servidor


//Controlamos el evento cuando se conecte e imprimimos en consola el mensaje recibido
//emitiendo el evento hello hacia el cliente con un mensaje inicial. io.on('connection', function (socket) {   socket.on('hello', function (msg) {     console.log('Received: %s', msg);     socket.emit('hello', 'Hello from server');   });   socket.on('login', function (msg) {     console.log('Received Login: %s', msg);     socket.emit('login', {"user": "System","text": "Welcome to the chat"});     users.push(msg);   });
//Por defecto enviamos un mensaje de bienvenida para el evento message
  socket.emit('message', '<p class="message">Welcome from server!</p>');  
//Controlamos el evento message y replicamos el mensaje a todos los clientes
//que se encuentren conectados   socket.on('message', function (msg) {     console.log('Received message: %s', msg);     var text = '<p class="message"><b>'+msg.user+'</b>:'+msg.message+'</p>';     io.sockets.emit('message', {"user": msg.user,"text": text});   });  
//Controlamos el evento list que nos va servir para ver el listado de usuarios
//conectados   socket.on('list', function (msg) {     console.log('Received list: %s', msg);      socket.emit('list', users);   });
});

index.html
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml">     <head>         <title>WebSockets Chat Example</title>         <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"> </script>         <script src="/socket.io/socket.io.js"></script>
<script>
//Inicializamos nuestras variables
var socket = null; var user = null;
$(document).ready(function(){ $("#chat").hide();//Ocultamos por defecto el layer del chat     //controlamos el evento del boton de iniciar chat
$("#btnJoin").click(function(){         socket = io.connect('http://localhost:8080');//iniciamos conexion con el server         user = $("#user").val();//guardamos el usuario insertado en memoria         socket.emit('login',user);//llamamos el evento login del server         //Controlamos el evento cuando el servidor responda
socket.on('login', function (msg) {         console.log('Received Login: %s', msg);     }); //Llamamos el evento de list para obtener los usuarios conectados
    socket.emit('list',"list");      //Controlamos la respuesta del server
socket.on('list', function (msg) {         console.log('Received List: %s', msg);         $(".leftSide").html(msg);     }); //Controlamos el evento message que enviará el servidor
    socket.on('message', function (msg) {         console.log('Received Message: %s', msg);         $(".rightSide").append(msg.text);     });     $("#chat").show();     $(".user").hide();
});
$("#send").click(function(){       socket.emit('message',{"user": user,"message": $("#textSend").val()}); });
}); </script>         <style>             .leftSide{ float:left; min-width:25%; min-height:400px; margin:10px; padding:15px; border:1px solid #333;}             .rightSide{ float:right; min-width:65%;min-height:400px; margin:10px; padding:15px;border:1px solid #333; }         </style>     </head>     <body>         <h2>Welcome to the WebSockets Chat</h2>         <section class="user">             <label>User Name </label> <input type="text" id="user"/>             <input type="button" value="Join" id="btnJoin"/>         </section>         <section id="chat">             <section class="leftSide"></section>             <section class="rightSide"></section>             <section>                 <input type="button" value="Send" id="send" style="margin:10px; padding:10px;" />                 <input type="text" id="textSend" style="margin:10px;"/>             </section>         </section>     </body> </html>


Conclusiones


Node.js es un lenguaje que esta avanzando muy rápido y nos permite realizar aplicaciones realmente emocionantes para tiempo real y muchos mas escenarios. El ejemplo que realice no tiene en cuenta detalles de estilos, validaciones o rendimiento, solo fue un ejemplo básico de como podemos utilizar las herramientas que nos brinda Node.js para hacer aplicaciones con WebSockets. 

Si te sirvió esta guía por favor deja un comentario y si no te sirvió también. 

4 comentarios:

  1. Buen post, gracias por el aporte Fabián.

    ResponderBorrar
  2. Excelente, podrias indicarme un ejemplo de control de sesiones cuando un usuario pasa a inactivo por tiempo o por que cierra la session

    ResponderBorrar