Node.
js (5) Real Time web con SocketIO
Bienvenidos nuevamente. En esta oportunidad y después más de un mes si escribir nada, me gustaría empezar a hablar un poco sobre una
de las características más populares del ecosistema [Link], que es su facilidad para crear aplicaciones real-time. Veremos una breve
introducción a la librería SocketIO y un pequeño ejemplo de uso para entender su funcionamiento. En las próximas entradas vamos a ir
profundizando un poco más. Empecemos con algunas definiciones:
¿A qué llamamos Web Real-Time?
En lineas generales, este concepto hace referencia a la características que poseen las aplicaciones web modernas de poder recibir
información y actualizaciones del servidor, sin realizar ninguna acción del lado del cliente. En otras palabras, es un conjunto de tecnologías
que permiten que una aplicación web reciba información en el momento en que esta se genera sin tener que que
chequear periódicamente las fuentes. Ejemplos de este tipo de interacciones entre el servidor y el cliente son las notificaciones de las redes
sociales, la llegada de un nuevo mail en gmail, la llegada de un nuevo mensaje en el chat de facebook o en el hangout de google, etc. Todos
estos eventos producen cambios en la página web sin que el cliente realice ninguna acción.
¿Realmente es un concepto nuevo?
No. La idea de aplicaciones web en tiempo real no es nueva, desde los orígenes de la web existieron numerosos sitios
que ofrecían aplicaciones de este tipo, tales como chats o juegos. Lo que cambió es la tecnología. Los primeros intentos de aplicaciones real-
time vinieron de la mano de los Applets de java. Para aquellos que tengan la suerte de no saber de que se trata, digamos que son pequeños
programas de java con su propia interfaz gráfica que corrían en el browser dentro de la VM de Java. Con el tiempo fueron quedando en
desuso, y el lugar fue ocupado por Flash, que domino el mundo de las aplicaciones real time hasta los últimos años. Como vemos, hasta
ahora siempre se dependía de alguna tecnología externa al navegador y a HTML para realizar este tipo de funcionalidad. Con la llegada de
AJAX se popularizaron distintas técnicas para emular una comunicación bidireccional con el servidor. En general se agrupan bajo el
termino Comet. Algunas de ellas son Long Polling, HTTP server push, etc. No vamos a hablar de cada una de ellas porque hay
mucha información en la web, que espero que no necesiten en el futuro. En lineas generales se trata de hacks para lograr algo para lo que el
protocolo HTTP no fue creado: tener un canal de comunicación bidireccional entre el cliente y el servidor.
WebSockets
HTML 5 incorpora en su especificación un nuevo protocolo de comunicación entre el cliente y el servidor llamado WebSocket. Los
WebSockets son una tecnología que permite al cliente abrir una conexión bidireccional con un servidor que soporte este protocolo. A través
de este socket el cliente y el servidor pueden enviar y recibir datos hasta que alguno de los dos cierre la conexión. El mayor problema que
tiene esta tecnología, es (para variar) el cross-browsing. Solo los navegadores modernos implementaron WebSockets y la implementación no
es del todo compatible entre ellos. Una situación parecida a la tecnología AJAX en sus inicios. Esto implica que para poder
hacer aplicaciones usando WebSockets los programadores deberíamos hacer una serie de malabares de diversa complejidad para que la
misma funcione en los distintos navegadores, y pensar alguna estrategia de retrocompatibilidad para navegadores más viejos. Por suerte,
todo este trabajo ya fue realizado por los creadores de la librería SocketIO.
SocketIO
Se trata de un modulo de [Link] disponible a través de npm, que nos provee de una librería del lado del servidor y del lado del cliente para
facilitar la comunicación real time entre el servidor web y el navegador. SocketIO resuelve el problema del cross-browsing, detectando la
compatibilidad del navegador y utilizando el mejor medio disponible para establecer la comunicación. Así mismo nos abstrae de
la complejidad de los web sockets permitiéndonos trabajar dentro de un esquema orientado a eventos pudiendo escuchar eventos del
servidor en el cliente y atender eventos generados en el cliente desde el servidor. Una vez que comprendemos la dinámica, esta forma de
trabajo se vuelve muy intuitiva y a la vez potente. Vamos con algún ejemplo sencillo para romper el hielo...
Partimos de la base de que ya tenemos todo lo necesario para trabajar con nodejs. En caso contrario los invito a revisar los posts anteriores.
Vamos a crear una nueva aplicación express, pero como nos interesa hacer un ejemplo básico, lo vamos a hacer a mano.
Creamos un directorio llamado SocketIOTest, dentro del cual crearemos tres archivos: [Link], [Link] y [Link].
1. SocketIOTest
1. [Link]
2. [Link]
3. [Link]
Primero editamos archivo [Link] dentro del cual vamos a definir las dependencias de la aplicación. En este caso usaremos Express y
SocketIO en sus ultimas versiones.
{
"name": "SocketIOTest",
"version": "0.0.1",
"private": true,
"scripts": {
"start": "node app"
},
"dependencies": {
"express": "*",
"[Link]": "*"
}
}
Ahora vamos a editar el archivo [Link] donde crearemos nuestro primer serviror real-time. Veamos las primeras lineas:
var http = require('http');
var express = require('express');
var socket = require('[Link]');
var app = express();
var server = [Link](app);
var io = [Link](server);
Las primeras 5 lineas ya nos resultan conocidas. Importamos los modulos que vamos a utilizar: http, express y [Link] como hacemos
siempre al inicio de una aplicación node. Lo siguiente que hacemos es crear una instancia de una aplicación express y levantar un servidor
http usando express como middleware al igual que hicimos en el post anterior. Una vez que tenemos el servidor http, lo usaremos para
instanciar nuestro modulo [Link] a través del metodo [Link](). Este método recibe una instancia del modulo HTTP de node y
configura varios handlers en el servidor. A partir de entonces tendremos dos middlewares en nuestra aplicación (app y io)
que atenderán eventos a través del modulo http. Express lo utilizaremos simplemente para configurar las rutas de nuestra aplicación y
[Link] para atender las conexiones real time que se generen desde el cliente.
Vamos entonces con la parte express de nuestra aplicación. En este caso será simplemente servir un archivo estático ([Link]) desde la
ruta principal de la aplicación (/). Esta tarea podríamos haberla hecho sin express, pero la idea es ir viendo como trabajar con estas
dos librerías juntas, ya que se esta transformando en una especie de estándar en el desarrollo de [Link]. Incluso existen varios frameworks
que unifican las dos librerías.
//Routing con Express
[Link]('/', function (req, res) {
[Link]('Express: Conexión en "/" sirviendo archivo
estático...');
[Link](__dirname + '/[Link]');
});
Y ahora vamos a la parte real time. Tenemos que definir un handler para los mensajes [Link]. Esto lo hacemos a través de la instancia del
modulo [Link] creada. El primer evento que debemos manejar es la conexión por parte del cliente. Este se transforma en una suerte de
punto de entrada de la aplicación real-time.
//Sockets events con [Link]
[Link]('connection', function(socket){
[Link]('SocketIO: Usuario Conectado...');
[Link]('disconnect', function () {
[Link]('SocketIO: Usuario Desconetado...');
});
});
El metodo [Link]() se utiliza para manejar eventos generados por el cliente. Existen varios eventos predefinidos como 'connection'
emitido cada vez que un usuario se conecta al servidor [Link] y 'disconect' que se emite cuando el cliente abandona la conexión. La
sintaxis es sensilla. El primer parametro es el nombre del evento al que queremos responder, y el segundo parametro una función callback
que se ejecutará cuando el evento tenga lugar. Lo que estamos haciendo por ahora es hacer un log en consola cuando un usuario se conecta
y registrar también cuando abandona la conexión.
Finalmente, decimos a nuestro servidor que se quede escuchando en un puerto como hacemos siempre.
[Link](3000);
Ahora vayamos a nuestro archivo [Link] para construir el cliente [Link].
<html>
<head>
<title>Ejemplo de uso: SocketIO</title>
<script src="/[Link]/[Link]"></script>
<script>
var socket = [Link]('[Link]
</script>
</head>
<body>
<h1>Test SocketIO</h1>
</body>
</html>
La inclusión de la librería "/[Link]/[Link]" en la linea 4 merece cierta explicación. Puede que hayas notado que no existe ese
directorio con el archivo de la librería. Lo que realmente ocurre es que cuando el browser la solicita, es el servidor de [Link] quién atiende
la petición y genera el archivo de la librería para el cliente en forma dinámica. En la línea 6 creamos la conexión con el server utilizando
el método [Link]() al cual le pasamos la url de nuestro servidor http. [Link] nos permite trabajar con la misma lógica del lado del
cliente y del lado del servidor. A través del objeto socket, podemos manejar eventos generados por el servidor y también emitir eventos a los
cuales el servidor podrá responder como veremos enseguida. Antes vamos a instalar las dependencias de nuestra aplicación y a probarla
para ver si todo anda bien.
para ello simplemente nos paramos en el directorio SocketIOTest y desde la consola corremos el comando:
npm install
De este modo se descargaran los fuentes de las dependencias que configuramos en el archivo [Link] (express y [Link]) los cuales
se guardaran dentro del directorio node_modules, el cual npm crea por defecto si no existe.
Luego simplemente ejecutamos la aplicación usando el interprete de node:
node [Link]
Ahora podemos apuntarle desde el navegador a [Link] y deberíamos ver en la terminal primero el mensaje de Express, y luego
el de [Link] "usuario conectado". Si cerramos la ventana del navegador [Link] detectara que cerramos la sección y veremos en la
consola el mensaje "usuario desconectado".
Ahora hagamos algo un poco más interesante. Veamos de que manera podemos enviar notificaciones al cliente en real-time si que este
realice ninguna acción. Modifiquemos la parte [Link] del archivo [Link]:
//Sockets events con [Link]
[Link]('connection', function(socket){
[Link]('SocketIO: Usuario Conectado...');
var sendNotification = function(){
var time = new Date();
[Link]('notification', { mensaje: time + ': Nueva notificación
enviada desde el servidor.'});
}
var sendNotificationInterval = setInterval(sendNotification,5000);
[Link]('disconnect', function () {
[Link]('SocketIO: Usuario Desconetado...');
clearInterval(sendNotificationInterval);
});
});
Lo que hicimos fue crear una función llamada sendNotification, dentro de la cual usamos la instancia de socket creada cuando el cliente se
conectó y emitimos un evento usando el metodo emit. Este método recibe un nombre que queramos ponerle al evento, en este caso
'notification' y el objeto que queremos enviar al cliente. Enviamos un json con la fecha y hora seguida del mensaje "Nueva notificación enviada
desde el servidor". Luego usamos la función setInterval de javascript para emitir este evento cada 5 segundos. Cuando el cliente se
desconecta, limpiamos el interval creado.
De este modo estamos emitiendo un evento desde el servidor. Veamos como hace el cliente para manejar este evento. Editamos el archivo
del cliente [Link]:
<html>
<head>
<script src="/[Link]/[Link]"></script>
<script>
var socket = [Link]('[Link]
[Link]('notification', function (data) {
var n = [Link] + '<br>';
[Link]("notificationContainer").innerHTML
+= n;
});
</script>
</head>
<body>
<h1>Test SocketIO</h1>
<div id="notificationContainer"></div>
</body>
</html>
A través del objeto socket, creamos un handler para el evento 'notification' que emitimos desde el servidor, del mismo modo que en el código
del servidor manejamos los eventos del cliente. Nuestro callback para el evento notification recibe el mensaje y lo muestra en la página dentro
del div 'notificationContainer'. Cada dev que el servidor envíe una nueva notificación se ejecutará este callback y veremos la nueva
notificación en el cuerpo de la página.
Ahora hagamos una ultima modificación al archivo del cliente. Vamos a ver como emitir eventos desde el cliente para que sean escuchados
por el servidor.
<html>
<head>
<script src="/[Link]/[Link]"></script>
<script>
var socket = [Link]('[Link]
[Link]('notification', function (data) {
var n = [Link] + '<br>';
[Link]("notificationContainer").innerHTML
+= n;
});
var detenerNotificaciones = function(){
[Link]('stopNotifications');
}
</script>
</head>
<body>
<h1>Test SocketIO</h1>
<button onclick="detenerNotificaciones()">Detener
Notificaciones</button>
<div id="notificationContainer"></div>
</body>
</html>
Del mismo modo que trabajamos del lado del servidor, hacemos uso del método emit y en esta caso solo emitimos un evento al que
llamamos 'stopNotifications' sin pasarle ningún dato adicional al servidor. Lo que pretendemos es que el servidor deje de enviarnos
notificaciones cuando emitamos ese evento. Creamos la función 'detenerNotificaciones' con esta lógica y la asociamos al evento onclick de
un elemento button que agregamos al html. De este modo al presionar ese botón, se detendrán las notificaciones del server.
Veamos como debería quedarnos el código del servidor, agregando un handler para este evento emitido por el cliente:
var http = require('http');
var express = require('express');
var socket = require('[Link]');
var app = express();
var server = [Link](app);
var io = [Link](server);
//Routing con Express
[Link]('/', function (req, res) {
[Link]('Express: Conexión en "/" sirviendo archivo
estático...');
[Link](__dirname + '/[Link]');
});
//Sockets events con [Link]
[Link]('connection', function(socket){
[Link]('SocketIO: Usuario Conectado...');
var sendNotification = function(){
var time = new Date();
[Link]('notification', { mensaje: time + ': Nueva notificación
enviada desde el servidor.'});
}
var sendNotificationInterval = setInterval(sendNotification,5000);
[Link]('stopNotifications', function () {
[Link]('SocketIO: Notificaciones detenidas por el
usuario...');
clearInterval(sendNotificationInterval);
});
[Link]('disconnect', function () {
[Link]('SocketIO: Usuario Desconetado...');
clearInterval(sendNotificationInterval);
});
});
[Link](3000);
En la linea 26 agregamos el handler para el evento 'stopNotifications'. Lo que hacemos es simplemente un log en consola avisando que el
usuario detuvo las notificaciones y hacemos un clearInterval de la función que las emitía.
Esto fue una breve introducción a esta gran librería. Ya se puede empezar a ver como cambia la lógica de la comunicación entre el servidor y
el cliente y las posibilidades que se abren. La idea es ir viendo en próximos posts mas detalles sobre la api y las distintas posibilidades que
ofrece, y también construir algún ejemplo más útil.