Introducción a Node.js
Por Arrecio
En el anterior artículo hablé de JavaScript y TypeScript, y entre otras cosas comenté que en general todos los lenguajes de familia JavaScript (o más bien ECMAScript) ha estado históricamente vinculados a los navegadores web como motor de ejecución de scripts que de alguna manera dotaban de cierto dinamismo a una web desde el lado del cliente.
Aunque este fuera el uso más tradicional desde los inicios ya se pensó en darle otros usos más en la línea de lenguajes multipropósito y no mucho después de los albores de JavaScript en los laboratorios de Netscape construyeron un entorno de ejecución de lado de servidor al estilo de las CGI mediante Perl, y antes incluso que las ASP y poco después PHP.
Y es que Netscape no proveía únicamente navegadores web siendo su propósito empresarial distribuir el servidor web Netscape Enterprise Server (ver Oracle iPlanet Web Server). De hecho JavaScript propiamente dicho es actualmente una marca de la que es propietaria Oracle, empresa que ha publicado una guía para un uso de JavaScript en el lado del servidor.
Algunas de las implementaciones que se han realizado están en esta lista sorprendiéndome encontrar, todo sea dicho, a MongoDB.
Entre los elementos de la lista aparece Node.js, el niño guapo de la familia, el que todos conocen.
Node.js está construido como una arquitectura general de servidor, es decir que su principal función es atender peticiones de usuario, procesarlas y ofrecer una respuesta. Cada petición no es más que una tarea a resolver, y en el caso de Node.js estas se resuelve de manera asíncrona no bloqueante.
El término asíncrono, cuando se relaciona con el software, suele referirse a arquitectura dirigida por eventos. Un caso general es el que se solicita algo al servidor y con la solicitud se envía el método a utilizar para dar a conocer la respuesta. El uso más tradicional es el de los llamados callbacks.
Con "no bloqueante" se dota de una característica al servidor por la cual estamos diciendo que está garantizado que ninguna de las tareas tendrá que esperar por la finalización de otras tareas, algo que podría suceder si alguna de las tareas bloqueara un servicio necesario para resolver también otra tarea.
Sumando ambos conceptos decimos el funcionamiento es que el servidor recibe una petición y se pone a procesarla mientras sigue admitiendo peticiones. Finalizará las mismas en el mayor tiempo posible (las más sencillas probablemente acaben antes) y tras ello responde al cliente con el resultado sin más demora.
La parte asíncrona se resuelve mediante la librería libuv que es la que gestiona el bucle de eventos o event loop que corre en un único hilo, y la parte no bloqueante a través de la implementación de worker threads (multi-hilo) encargados de realizar todo aquello que requiera llamadas al sistema o a librerías externas. Si un worker se bloquea bloqueara únicamente la tarea que está tratando de resolver.
Existe 4 hilos adicionales al que sostiene el event loop que se utilizan específicamente para operaciones de compresión, llamadas al sistema de archivos, criptografía y resolución de nombres que son entendidas como potencialmente bloqueantes. Estos 4 hilos y su utilización son responsabilidad de Node.Js. Para cualquier otra operación que corre el riesgo de bloquear el event loop el programador es responsable de mantener el event loop sin bloqueos (regla de oro de Node.Js) y a para ello es que utilizaremos los workers.
Todo esto se ve mejor con un dibujo:
En el dibujo se ha expuesto un caso de uso de un worker que finaliza por completo una tarea pero en muchas tareas sencillas no necesitaremos hacer uso de los mismos, ni tan siquiera para ejecutar micro-tareas (partes de una tarea), pudiendo en estos casos devolver el resultado directamente a través del respectivo callback.
El dibujo simboliza a unos clientes que realizan peticiones que son enviadas al bucle de eventos, allí para cada petición habrá que realizar una serie de operaciones que conllevarán llamar a una o varias funciones. Los intérpretes de JavaScript van ejecutando el código mediante una pila de llamadas. Cuando alguna función se corresponde a una llamada potencialmente bloqueante de las que gestiona Node.Js entonces pasa su ejecución al hilo correspondiente.
Por otro lado si el programador considera que la tarea o cierta parte de la tarea corre el riesgo de bloquear el event loop puede lanzar la ejecución de ese código mediante un nuevo hilo mediante lo que se conoce como worker thread que se van obteniendo de un pool en el que descansan todos los workers no ocupados y que pasaría a correr en un nuevo hilo.
Cuando las tareas van siendo completamente finalizadas se busca que callback tienen asociado y se despacha el resultado enviándolo a quien hizo la solicitud.