Ariel Rey

Curso Básico de NodeJS - Parte 1 - Introducción

Hola 👋, mi nombre es Ari Rey y soy desarrollador hace más de 10 años. Hace un tiempo venia con la idea en la cabeza de hacer un curso de NodeJS sin ejectuar ni una linea de código 😶. Todo lo que podía encontrar en Youtube, no era realmente un curso de NodeJS, sino mas bien un curso de Programación Javascript utilizando NodeJS. Mi intención con este curso es hacer una recopilación de todos aquellos videos, presentaciones en conferencias, blogs e información que fui encontrando en la Web que permiten explicar verdaderamente qué es NodeJS.

📣 Aviso: Para poder explicar qué es, voy a tener que usar términos y conceptos técnicos que quizá no conozcan. Por lo que voy a estar explicando los mismos de una manera abstracta y simple para que puedan seguir el curso sin necesidad de estar Googleando los mismos.

Si bien voy a estar subiendo este curso en mi canal de Youtube, voy a estar escribiendo el mismo en Collected Notes para los que les guste leer o por algún motivo no puedan verlo.

¿Qué es NodeJS?

alt

Lo primero que hay que dejar en claro es que NodeJS no es un lenguaje de programación, Javascript es un lenguaje de programación. NodeJS es un runtime environment que utiliza al lenguaje Javascript para funcionar.

Un runtime environment es un ambiente provisto por los sistemas operativos a las aplicaciones para enviar mensajes y ejecutar instrucciones ó comandos para poder comunicarse con periféricos o recursos de la computadora 🤔.

En el siguiente ejemplo, podemos ver cómo NodeJS por medio del sistema operativo se comunica con un Disco Duro, ya sea para leer un archivo, escribirlo o tan solo listar un directorio. No necesariamente la comunicación se puede dar solo con un Disco Duro, sino también puede darse con el CPU para ejecutar operaciones matemáticas como suma, resta, división, etc...

alt

Pero, ¿cómo es que mediante código Javascript puedo acceder al Disco Dura, al CPU, etc...?. Lo primero que tenemos que saber, es que la comunicación entre los sistemas operativos y los periféricos de la computadora se realiza en "código de bajo nivel" o "código maquina", es decir, 1 o 0. Acá es donde entra Javascript en la ecuación. NodeJS cuenta con un runtime engine o motor de ejecución que se encarga de convertir "código de alto nivel", en este caso Javascript, en código maquina.

El código de alto nivel es aquel que puede ser escrito e interpretado por humanos 🧍*. Esto permite que a la hora de programar, podamos hacerlo abstraernos de como el mismo es ejecutado.*

En el caso de NodeJS, esta conversión es realizada por él "Motor de Ejecución V8" desarrollado por Google. Este motor fue creado para ser usado por el navegador Chrome y Chromium para interpretar y ejecutar el código Javascript de una página Web.

alt

Cómo podemos ver en la imagen, el punto de partida es un archivo de tipo Javascript. Él mismo es entregado a NodeJS, que mediante el Motor de Ejecución V8 será interpretado y convertido a código maquina en tiempo de ejecución, es decir, cuando el programa se esta ejecutando.

Código de Alto Nivel a Código Maquina

Todo lenguaje de programación tiene un runtime environment que compila o interpreta el código y termina retornando código maquina. Aunque no lo crean, podemos comparar JAVA con NodeJS. Si, así como lo escuchaste...

alt

Si lo vemos muy, muy de lejos, JAVA y NodeJS son bastante parecidos 🤯. Los 2 tienen un runtime engine o "algo" que funciona como tal, que recibe código de alto nivel y retorna código maquina. En el caso de JAVA, es la Java Virtual Machine (JVM) y en el caso de de NodeJS es el Google V8. Para entender mejor cómo hace esta conversión NodeJS, primero veamos cómo lo hace JAVA.

Para los próximos ejemplos, vamos a estar viendo un programa que realiza las siguientes tareas:

Tenemos un programa que declara tres variables enteras (a, b, c), 2 (a, b) con valores asignados (10 y 20) y una tercera (c) que es igual a la suma de las variables a, b. Finalmente, se imprime el resultado de la suma (c).

De Java a Código Maquina

alt

Antes de ejecutar el programa, si o si se tiene que compilar el código JAVA mediante una librería llamada javac. Esta es provista por la Java Development Kit (JDK), el kit de desarrollo de JAVA. El proceso de compilación nos dará como resultado un código llamado bytecode. Él bytecode es un código intermedio entre el código de alto nivel y código de bajo nivel.

La Java Development Kit (JDK) es una librería que se tiene que instalar si se quiere programar en JAVA

El segundo paso consiste entregarle el bytecode a la JRE, que mediante la JVM (runtime engine) interpretará el código y lo convertirá a código maquina.

Si nos ponemos a analizarlo en detalle 🧐, podemos decir que JAVA es un lenguaje compilado, esto es porque se compila el código antes de ser ejecutado. Ahora veamos cómo funciona NodeJS.

De Javascript a Código Maquina

La diferencia más notable es que el proceso se resume en un solo paso. El único paso consiste en entregarle el código Javascript al Motor de Ejecución V8 que interpreta el código Javascript y lo convierte a código maquina.

alt

Si bien internamente es compilado mediante Just In Time Compiler en tiempo de ejecución, el bytecode intermedio no se guarda. Esto es anecdótico, no nos compliquemos...

alt

Quizá en este momento estén necesitando un café ☕

Modelo de Comunicación

NodeJS usa un modelo no bloqueante de entrada y salida (Input / Output) basado en eventos que lo hace liviano y efectivo. ¿WTF?, ¿no?. Antes de seguir avanzando, tenemos que entender los siguientes conceptos:

  • ¿Qué significa "Entrada y Salida" (E/S ó I/O)?

Cuando decimos entrada y salida, nos referimos a la comunicación entre la computadora y el mundo exterior 🪐. Con entrada nos referimos a los datos recibidos por el sistema y en cuanto a salida nos referimos a los datos enviados por él. Podríamos decir, que la comunicación entre el Sistema Operativo y el Disco Duro se realiza mediante la comunicación E/S (I/O).

  • ¿Qué significa "no bloqueante"?

Lo primero que tenemos que saber es qué existen 2 modelos de comunicación. El modelo bloqueante y él no bloqueante. En el modelo bloqueante, cuando voy a realizar una tarea que demora un determinado tiempo, voy a tener que esperar hasta que termine de realizar dicha tarea ⏳. Como por ejemplo, leer el contenido de un archivo, realizar una operación matemática compleja, etc... En cambio en el modelo no bloqueante, mientras realizo una tarea que va a demorar determinado tiempo ⏳, puedo hacer otra tarea sin necesidad de esperar a que termine la anterior.

Bloqueante Vs. No Bloqueante 🥊

Hagamos de cuenta que vamos a ejecutar un programa que va a ejecutar las siguientes tareas:

1. Obtener la información del usuario 1
2. Imprimir la información del usuario 1
3. Obtener la información del usuario 2
4. Imprimir la información del usuario 2
5. Tomar un Café

alt

El diagrama de la izquierda representa la ejecución del programa mediante un modelo bloqueante, y el de la derecha la ejecución de un modelo no bloqueante.

En el modelo bloqueante (izquierda), podemos ver que cada vez que ejecutamos una tarea, tenemos que esperar ⏳ a que termine para ejecutar la siguiente. En el modelo no bloqueante (derecha), podemos ver que solamente empezamos a realizar la tarea. Mientras se ejecuta la misma, podemos empezar a realizar la siguiente tarea sin tener que esperar a que termine la anterior.

Como se puede ver en la imagen, el modelo no bloqueante termina antes que el modelo bloqueante.

Hilos de Ejecución

Todas estas tareas (obtener información de usuarios, tomar café, etc...) son ejecutadas dentro de un hilo ó también llamado thread en ingles. Un hilo ó thread es un espacio en donde se pueden ejecutar pequeñas tareas de manera encadenada (una detrás de otra).

alt

Veamos cómo funciona él mismo ejemplo dentro de un hilo de ejecución con un modelo bloqueante.

alt

Pedimos la información del usuario 1 y nos bloqueamos ⏳

alt

Cuando termina, imprimimos la información del usuario 1, pedimos la información del usuario 2 y nos bloqueamos ⏳

alt

Cuando termina, imprimimos la información del usuario 2 y nos tomamos el café ☕

Cómo pudieron ver, todas las tareas que tarda un determinado tiempo, bloquean el hilo de ejecución. Pero, ¿Podemos ejecutar varias tareas al mismo tiempo en un sistema bloqueante? , y la respuesta es Multihilo ó Multithread.

## Multihilo ó Multithread

La única manera de realizar varias tareas en un sistema bloqueante es mediante la utilización de múltiples hilos de ejecución. Ni bien ejecutemos una tarea que demore un determinado tiempo ⏳ y bloqueé nuestro hilo, vamos a crear y cambiar el foco de ejecución a otro hilo. Este cambio de foco se llama context switching.

Veamos el ejemplo anterior pero con múltiples hilos de ejecución:

alt

Pedimos la información del usuario 1 ⏳ y nos bloqueamos. Luego, cambiamos el foco al hilo 2

alt

En el hilo 2, pedimos la información del usuario 2 ⏳ y nos bloqueamos. Mientras el hilo 1 y el hilo 2 se encuentran bloqueados, cambiamos el foco al hilo 3

alt

En el hilo 3 tomamos un café ☕. Una vez terminado el café ☕ destruimos el hilo 3 💣. Cambiamos el foco al hilo 1 que ya termino de pedir la información del usuario 1

alt

En el hilo 1 imprimimos la información y luego destruimos el hilo 1 💣. Se cambia el foco al hilo 2 para esperar a que termine de traer la información ⏳

alt

Por último, en el hilo 2 imprimimos la información y luego destruimos el hilo 2 💣

¿Y ahora qué? 🤷‍♂️

Entonces, usando múltiples hilos de ejecución podríamos funcionar como los modelos no bloqueantes (sin entrar en detalle). Quizá estes diciendo genial, no veo diferencia ahora!, pero tenés que saber qué nada es gratis 💸. Tenemos que tener en cuenta los siguientes puntos:

  • Cada hilo de ejecución ocupa un tamaño predeterminado en memoria. Por ejemplo, podríamos decir que cada hilo ocupa 1 MB, sin importar si la tarea realmente ocupa lo mismo o menos.

  • Cada cambio de "foco" o "context switching" no es instantáneo, lleva tiempo, no importa si son milisegundos o nanosegundos, si lo hacemos muchas veces, es costoso.

alt

En la imagen podemos ver que el total de las tareas ocupa 350 KB. Pero, el total de los hilos utilizados ocupan 3 MB en memoria.

## ¿Hilo "No Bloqueante"?

Si toda tarea se ejecuta dentro de hilos de ejecución, entonces, ¿cómo funciona un modelo no bloqueante?. En un modelo no bloqueante, todo se realiza en un solo hilo de ejecución y sin bloquearse 😱. Nuevamente, ¿WTF?, ¿Cómo hace todo dentro de un solo hilo y no me bloqueo?...

Lo que se hace es mandar al "éter" las tareas que demoran tiempo ⏳. Enviarlo al "éter" nos permite no bloquearnos. Una vez terminada la ejecución de dicha tarea, el "éter" nos dice: "Hilo, ya termine!, tengo la información que necesitas". Esta comunicación se realiza mediante algo llamado eventos, en Javascript, mediante los famosos callbacks 😎.

alt

Pero, ¿cómo es que al tener un hilo no nos bloqueamos?, ¿cómo funciona esta comunicación mediante eventos?, tengo muchas preguntas! 😫. Todos los modelos no bloqueantes funcionan gracias a un "loop infinito", en el caso de NodeJS, algo llamado "Event loop". Pero eso lo vamos a ver más adelante...

Gracias

Gracias por haber leído la primera parte. Si quieren pueden ver el video completo del curso acá, subscribirse y darle me gusta. Nos vemos en la próxima.

  • 05-07-20: Gracias a la fuerza que tomo la publicación en Twitter, estuve realizando unos cambios y emojizandolo para que quede más completo y prolijo