Hola Habr! Te presento la traducción del artículo "Todo lo que necesitas saber sobre Node.js" de Jorge Ramón.

Hoy en día, la plataforma Node.js es una de las plataformas más populares para construir API REST eficientes y escalables. También es adecuado para crear aplicaciones móviles híbridas, programas de escritorio e incluso para IoT.
He estado trabajando con la plataforma Node.js durante más de 6 años y realmente me encanta. Esta publicación trata principalmente de ser una guía de cómo funciona Node.js en realidad.
¡Comencemos!
Lo que se discutirá:
Mundo antes de Node.js
Servidor multiproceso
Las aplicaciones web escritas siguiendo la arquitectura cliente / servidor funcionan de la siguiente manera: el cliente solicita el recurso necesario del servidor y el servidor envía el recurso en respuesta. En este esquema, el servidor responde a la solicitud y finaliza la conexión.
Este modelo es efectivo porque cada solicitud al servidor consume recursos (memoria, tiempo de procesador, etc.). Para procesar cada solicitud posterior del cliente, el servidor debe completar el procesamiento de la anterior.
¿Significa esto que el servidor solo puede procesar una solicitud a la vez? En realidad no! Cuando el servidor recibe una nueva solicitud, crea un hilo separado para procesarla.
El flujo , en palabras simples, es el tiempo y los recursos que la CPU asigna para ejecutar un pequeño bloque de instrucciones. Dicho esto, el servidor puede procesar varias solicitudes a la vez, pero solo una por subproceso. Tal modelo también se llama modelo de subproceso por solicitud .

Para procesar N solicitudes, el servidor necesita N subprocesos. Si el servidor recibe solicitudes N + 1, debe esperar hasta que uno de los hilos esté disponible.
En la figura anterior, el servidor puede procesar hasta 4 solicitudes (subprocesos) a la vez y cuando recibe las siguientes 3 solicitudes, estas solicitudes deben esperar hasta que alguno de estos 4 subprocesos esté disponible.
Una forma de deshacerse de las restricciones es agregar más recursos (memoria, núcleos de procesador, etc.) al servidor, pero esta no es la mejor solución ...
Y, por supuesto, no te olvides de las limitaciones tecnológicas.
Bloqueo de entrada / salida
El número limitado de subprocesos en el servidor no es el único problema. ¿Quizás se preguntó por qué un solo hilo no puede procesar múltiples solicitudes al mismo tiempo? todo debido al bloqueo de operaciones de E / S.
Supongamos que está desarrollando una tienda en línea y necesita una página donde el usuario pueda ver una lista de todos los productos.
El usuario llama a http://yourstore.com/products y el servidor procesa un archivo HTML con todos los productos de la base de datos en respuesta. Para nada complicado, ¿verdad?
¿Pero qué pasa detrás de escena?
- Cuando un usuario llama a
/products
, se debe ejecutar /products
método o función particular para procesar la solicitud. Un pequeño fragmento de código (el suyo o su marco) analiza la URL de solicitud y busca un método o función adecuados. La corriente se está ejecutando . 
- Ahora se ejecuta el método o función deseada, como en el primer párrafo, el hilo funciona.

- Como es un buen desarrollador, guarda todos los registros del sistema en un archivo y, por supuesto, para asegurarse de que el enrutador realiza el método / función deseados, también registra la línea "¡Método X en ejecución!". Pero todo esto es operaciones de bloqueo el flujo de entrada / salida está esperando .

- Todos los registros se guardan y se ejecutan las siguientes líneas de función. El hilo está funcionando de nuevo .

- Es hora de acceder a la base de datos y obtener todos los productos: una consulta simple como
SELECT * FROM products
hace su trabajo, pero ¿adivina qué? Sí, esta es una operación de bloqueo de E / S. La corriente está esperando . 
- Ha recibido una matriz o una lista de todos los productos, pero asegúrese de haber prometido todo esto. La corriente está esperando .

- Ahora tiene todos los productos y es hora de presentar la plantilla para la página futura, pero antes de eso debe leerlos. La corriente está esperando .

- El motor de renderizado hace su trabajo y envía una respuesta al cliente. El hilo está funcionando de nuevo .

- El flujo es libre, como un pájaro en el cielo.

¿Qué tan lentas son las operaciones de E / S? Bueno, depende de lo específico. Miremos la mesa:
Las operaciones de red y lectura de disco son demasiado lentas. Imagine cuántas solicitudes o llamadas a API externas podría manejar su sistema durante este tiempo.
Para resumir: las operaciones de E / S hacen que el hilo espere y desperdicie recursos.
Problema C10K
El problema
C10k (eng. C10k; 10k conexiones - problema de 10 mil conexiones)
A principios de la década de 2000, las máquinas de servidor y cliente eran lentas. El problema surgió al procesar 10,000 conexiones de clientes a la misma máquina en paralelo.
Pero, ¿por qué el modelo tradicional de subprocesos por solicitud (subproceso a pedido) no puede resolver este problema? Bueno, usemos un poco de matemática.
La implementación nativa de subprocesos asigna más de 1 MB de memoria por flujo, dejando esto: para 10 mil subprocesos, se requieren 10 GB de RAM y esto es solo para la pila de flujo. ¡Sí, y no lo olviden, estamos a principios de la década de 2000!
Hoy en día, las computadoras del servidor y del cliente funcionan de manera más rápida y eficiente, y casi cualquier lenguaje o marco de programación puede hacer frente a este problema. Pero, de hecho, el problema no está resuelto. Para 10 millones de conexiones de clientes a una máquina, el problema vuelve nuevamente (pero ahora es el problema C10M ).
Rescate de JavaScript?
Spoilers de precaución
!!!
Node.js en realidad resuelve el problema C10K ... pero ¿cómo?
El JavaScript del lado del servidor no era algo nuevo e inusual a principios de la década de 2000, en ese momento ya había implementaciones en la parte superior de la JVM (máquina virtual java): RingoJS y AppEngineJS, que funcionaban en el modelo de subprocesos por solicitud.
Pero si no podían resolver el problema, ¿cómo podría Node.js? Todo porque JavaScript es de un solo subproceso .
Node.js y el bucle de eventos
Node.js
Node.js es una plataforma de servidor que se ejecuta en el motor Google Chrome - V8, que puede compilar código JavaScript en código máquina.
Node.js utiliza un modelo controlado por eventos y una arquitectura de E / S sin bloqueo , lo que lo hace ligero y eficiente. Esto no es un marco, ni una biblioteca, es un tiempo de ejecución de JavaScript.
Escribamos un pequeño ejemplo:
E / S sin bloqueo
Node.js utiliza operaciones de entrada / salida sin bloqueo, ¿qué significa esto:
- El hilo principal no será bloqueado por las operaciones de E / S.
- El servidor continuará atendiendo solicitudes.
- Tendremos que trabajar con código asincrónico .
Escribamos un ejemplo en el que el servidor envía una página HTML en respuesta a una solicitud a /home
, y para todas las demás solicitudes: 'Hello World'. Para enviar una página HTML, primero debe leerla desde un archivo.
home.html
<html> <body> <h1>This is home page</h1> </body> </html>
index.js
const http = require('http'); const fs = require('fs'); const server = http.createServer(function(request, response) { if (request.url === '/home') { fs.readFile(`${ __dirname }/home.html`, function (err, content) { if (!err) { response.setHeader('Content-Type', 'text/html'); response.write(content); } else { response.statusCode = 500; response.write('An error has ocurred'); } response.end(); }); } else { response.write('Hello World'); response.end(); } }); server.listen(8080);
Si la url solicitada es /home
, el módulo fs
nativo se usa para leer el archivo home.html
.
Las funciones que se http.createServer
en http.createServer
y fs.readFile
como argumentos son devoluciones de llamada . Estas funciones se realizarán en algún momento en el futuro (el primero, tan pronto como el servidor recibe la solicitud, y el segundo, cuando el archivo se lee del disco y se coloca en el búfer).
Mientras el archivo se lee desde el disco, Node.js puede procesar otras solicitudes e incluso leer el archivo nuevamente y todo esto en una secuencia ... ¡pero cómo?
Bucle de eventos
El bucle de eventos es la magia que ocurre dentro de Node.js. Esto es literalmente un bucle sin fin y en realidad un hilo.
Libuv es una biblioteca C que implementa este patrón y es parte del núcleo Node.js. Puedes aprender más sobre libuv aquí .
Un ciclo de eventos tiene 6 fases, cada ejecución de las 6 fases se llama tick .
- temporizadores : en esta fase, se ejecutan devoluciones de llamada programadas por los
setTimeout()
y setInterval()
; - devoluciones de llamada pendientes : casi todas las devoluciones de llamada se ejecutan, excepto los eventos
close
, temporizadores y setImmediate()
; - inactivo, preparar : utilizado solo para fines internos;
- encuesta : responsable de recibir nuevos eventos de E / S. Node.js puede bloquearse en este punto;
- check : las devoluciones de llamada causadas por el método
setImmediate()
se ejecutan en esta etapa; - devoluciones de llamada cercanas : por ejemplo
socket.on('close', ...)
;
Bueno, solo hay un hilo, y este hilo es un bucle de eventos, pero ¿quién realiza todas las E / S?
Presta atencion
!!!
Cuando un bucle de eventos necesita realizar una operación de E / S, utiliza el subproceso del sistema operativo del grupo de subprocesos, y cuando se completa la tarea, la devolución de llamada se pone en cola durante la fase de devoluciones de llamada pendientes .
¿No es genial?
El problema de las tareas intensivas en CPU
¡Node.js parece perfecto! Puedes crear lo que quieras.
Escribamos una API para calcular números primos.
Un número primo es un número entero (natural) mayor que uno y divisible por solo 1 y por sí mismo.
Dado un número N, la API debe calcular y devolver los primeros N primos en la lista (o matriz).
primes.js
function isPrime(n) { for(let i = 2, s = Math.sqrt(n); i <= s; i++) { if(n % i === 0) return false; } return n > 1; } function nthPrime(n) { let counter = n; let iterator = 2; let result = []; while(counter > 0) { isPrime(iterator) && result.push(iterator) && counter--; iterator++; } return result; } module.exports = { isPrime, nthPrime };
index.js
const http = require('http'); const url = require('url'); const primes = require('./primes'); const server = http.createServer(function (request, response) { const { pathname, query } = url.parse(request.url, true); if (pathname === '/primes') { const result = primes.nthPrime(query.n || 0); response.setHeader('Content-Type', 'application/json'); response.write(JSON.stringify(result)); response.end(); } else { response.statusCode = 404; response.write('Not Found'); response.end(); } }); server.listen(8080);
prime.js
es la implementación de los cálculos necesarios: la función isPrime
comprueba si el número es primo y nthPrime devuelve N tales números.
El archivo index.js
es responsable de crear el servidor y utiliza el módulo prime.js
para procesar cada solicitud de /primes
. El número N se lanza a través de la cadena de consulta en la URL.
Para obtener los primeros 20 primos, debemos realizar una solicitud a http://localhost:8080/primes?n=20
.
Supongamos que tenemos 3 clientes que nos llaman e intentan acceder a nuestra API de E / S sin bloqueo:
- La primera consulta 5 primos por segundo.
- El segundo pide 1000 primos por segundo
- El tercero solicita 10,000,000,000 primos, pero ...
Cuando el tercer cliente envía una solicitud, el subproceso principal se bloquea y este es el síntoma principal del problema de las tareas intensivas de CPU . Cuando el hilo principal está ocupado realizando una tarea "pesada", se vuelve inaccesible para otras tareas.
¿Pero qué hay de libuv? Si recuerda, esta biblioteca ayuda a Node.js a realizar operaciones de entrada / salida utilizando hilos del sistema operativo evitando bloquear el hilo principal y tiene toda la razón, esta es la solución a nuestro problema, pero para que esto sea posible, nuestro módulo debe estar escrito en el idioma C ++ para que libuv pueda trabajar con él.
Afortunadamente, a partir de v10.5, el módulo nativo de subprocesos de trabajo se ha agregado a Node.js.
Los trabajadores y sus flujos.
Como nos dice la documentación :
Los trabajadores son útiles para realizar operaciones JavaScript intensivas en CPU; no los use para operaciones de entrada / salida, los mecanismos ya integrados en Node.js hacen frente de manera más eficiente a esas tareas que el subproceso Worker.
Código fijo
Es hora de reescribir nuestro código:
primes-workerthreads.js
const { workerData, parentPort } = require('worker_threads'); function isPrime(n) { for(let i = 2, s = Math.sqrt(n); i <= s; i++) if(n % i === 0) return false; return n > 1; } function nthPrime(n) { let counter = n; let iterator = 2; let result = []; while(counter > 0) { isPrime(iterator) && result.push(iterator) && counter--; iterator++; } return result; } parentPort.postMessage(nthPrime(workerData.n));
index-workerthreads.js
const http = require('http'); const url = require('url'); const { Worker } = require('worker_threads'); const server = http.createServer(function (request, response) { const { pathname, query } = url.parse(request.url, true); if (pathname === '/primes') { const worker = new Worker('./primes-workerthreads.js', { workerData: { n: query.n || 0 } }); worker.on('error', function () { response.statusCode = 500; response.write('Oops there was an error...'); response.end(); }); let result; worker.on('message', function (message) { result = message; }); worker.on('exit', function () { response.setHeader('Content-Type', 'application/json'); response.write(JSON.stringify(result)); response.end(); }); } else { response.statusCode = 404; response.write('Not Found'); response.end(); } }); server.listen(8080);
En el index-workerthreads.js
, cada solicitud a /primes
crea una instancia de la clase Worker
(desde el módulo nativo worker_threads
) para cargar y ejecutar el primes-workerthreads.js
en el subproceso de trabajo. Cuando la lista de números primos se calcula y está lista, se activa el evento del message
: el resultado cae en la secuencia principal debido a que el trabajador no tiene trabajo restante, también activa el evento de exit
, permitiendo que la secuencia principal envíe datos al cliente.
primes-workerthreads.js
cambiado un poco. Importa workerData
(esta es una copia de los parámetros pasados del hilo principal) y parentPort
través del cual el resultado del trabajo del trabajador se devuelve al hilo principal.
Ahora intentemos nuestro ejemplo nuevamente y veamos qué sucede:
El hilo principal ya no está bloqueado
!!!!!
Ahora todo funciona como debería, pero producir trabajadores sin ninguna razón todavía no es una buena práctica; crear hilos no es un placer barato. Asegúrese de crear un grupo de subprocesos antes de esto.
Conclusión
Node.js es una tecnología poderosa que debe explorarse siempre que sea posible.
Mi recomendación personal: ¡siempre ten curiosidad! Si sabe cómo funciona algo desde adentro, puede trabajar con él de manera más eficiente.
Eso es todo por hoy chicos. Espero que esta publicación te haya sido útil y hayas aprendido algo nuevo sobre Node.js.
Gracias por leer y nos vemos en las próximas publicaciones.
.