Olá Habr! Apresento a você a tradução do artigo "Tudo o que você precisa saber sobre o Node.js", de Jorge Ramón.

Atualmente, a plataforma Node.js. é uma das plataformas mais populares para a criação de APIs REST eficientes e escalonáveis. Também é adequado para a criação de aplicativos móveis híbridos, programas de desktop e até para IoT.
Trabalho com a plataforma Node.js. há mais de 6 anos e realmente adoro isso. Esta postagem está tentando principalmente ser um guia sobre como o Node.js realmente funciona.
Vamos começar !!
O que será discutido:
Mundo antes do Node.js
Servidor multithread
Os aplicativos da Web gravados seguindo a arquitetura cliente / servidor funcionam da seguinte maneira - o cliente solicita o recurso necessário do servidor e o servidor envia o recurso em resposta. Nesse esquema, o servidor responde à solicitação e finaliza a conexão.
Esse modelo é efetivo porque cada solicitação ao servidor consome recursos (memória, tempo do processador etc.). Para processar cada solicitação subsequente do cliente, o servidor deve concluir o processamento da solicitação anterior.
Isso significa que o servidor pode processar apenas uma solicitação por vez? Na verdade não! Quando o servidor recebe uma nova solicitação, ele cria um thread separado para processá-lo.
O fluxo , em palavras simples, é o tempo e os recursos que a CPU aloca para executar um pequeno bloco de instruções. Com isso dito, o servidor pode processar várias solicitações por vez, mas apenas uma por thread. Esse modelo também é chamado de modelo de thread por solicitação .

Para processar N pedidos, o servidor precisa de N threads. Se o servidor receber solicitações N + 1, deverá aguardar até que um dos encadeamentos fique disponível.
Na figura acima, o servidor pode processar até 4 solicitações (threads) de cada vez e, quando receber as próximas 3 solicitações, essas solicitações deverão aguardar até que qualquer um desses 4 threads fique disponível.
Uma maneira de se livrar das restrições é adicionar mais recursos (memória, núcleos do processador etc.) ao servidor, mas essa não é a melhor solução ....
E, claro, não se esqueça das limitações tecnológicas.
Bloqueio de entrada / saída
O número limitado de threads no servidor não é o único problema. Talvez você tenha se perguntado por que um único encadeamento não pode processar várias solicitações ao mesmo tempo? tudo devido ao bloqueio de operações de E / S.
Suponha que você esteja desenvolvendo uma loja online e precise de uma página na qual o usuário possa visualizar uma lista de todos os produtos.
O usuário bate em http://yourstore.com/products e o servidor renderiza um arquivo HTML com todos os produtos do banco de dados em resposta. Não é nada complicado, certo?
Mas o que acontece nos bastidores?
- Quando um usuário bate em
/products
método ou função específica deve ser executada para processar a solicitação. Um pequeno pedaço de código (o seu ou o seu framework) analisa o URL da solicitação e procura um método ou função adequada. O fluxo está em execução . 
- Agora, o método ou função desejado é executado, como no primeiro parágrafo, o thread funciona.

- Como você é um bom desenvolvedor, salve todos os logs do sistema em um arquivo e, é claro, para garantir que o roteador execute o método / função desejado - você também registra a linha “Método X executando !!”. Mas tudo isso está bloqueando as operações o fluxo de entrada / saída está aguardando .

- Todos os logs são salvos e as seguintes linhas de função são executadas. O encadeamento está funcionando novamente .

- Hora de acessar o banco de dados e obter todos os produtos - uma consulta simples como os
SELECT * FROM products
faz seu trabalho, mas adivinhe? Sim, esta é uma operação de E / S de bloqueio. O fluxo está aguardando . 
- Você recebeu uma matriz ou uma lista de todos os produtos, mas certifique-se de ter prometido tudo isso. O fluxo está aguardando .

- Agora você tem todos os produtos e é hora de renderizar o modelo para a página futura, mas antes disso você precisa lê-los. O fluxo está aguardando .

- O mecanismo de renderização faz seu trabalho e envia uma resposta ao cliente. O encadeamento está funcionando novamente .

- O fluxo é livre, como um pássaro no céu.

Quão lentas são as operações de E / S? Bem, isso depende do específico. Vamos olhar para a mesa:
As operações de leitura de rede e disco são muito lentas. Imagine quantas solicitações ou chamadas para APIs externas seu sistema poderia lidar durante esse período.
Para resumir: As operações de E / S fazem o encadeamento aguardar e desperdiçar recursos.
Problema C10K
O problema
C10k (eng. C10k; conexões 10k - problema com 10 mil conexões)
No início dos anos 2000, as máquinas servidor e cliente estavam lentas. O problema surgiu ao processar 10.000 conexões de clientes com a mesma máquina em paralelo.
Mas por que o modelo tradicional de thread por solicitação (thread sob solicitação) não conseguiu resolver esse problema? Bem, vamos usar um pouco de matemática.
A implementação nativa de encadeamentos aloca mais de 1 MB de memória por fluxo, deixando isso - para 10 mil encadeamentos, são necessários 10 GB de RAM e isso é apenas para a pilha de fluxos. Sim, e não se esqueça, estamos no início dos anos 2000 !!
Hoje, os servidores e os computadores clientes trabalham com mais rapidez e eficiência e quase qualquer linguagem ou estrutura de programação pode lidar com esse problema. Mas, de fato, o problema não está resolvido. Para 10 milhões de conexões de clientes com uma máquina, o problema retorna novamente (mas agora é o problema do C10M ).
Resgate de JavaScript?
Spoilers de Cuidado
!!!
O Node.js resolve o problema do C10K ... mas como ?!
O JavaScript do lado do servidor não era algo novo e incomum no início dos anos 2000, naquela época já havia implementações na JVM (máquina virtual java) - RingoJS e AppEngineJS, que trabalhavam no modelo de thread por solicitação.
Mas se eles não conseguiram resolver o problema, como o Node.js ?! Tudo porque o JavaScript é de thread único .
Node.js e o loop de eventos
Node.js
O Node.js é uma plataforma de servidor executada no mecanismo do Google Chrome - V8, que pode compilar o código JavaScript no código da máquina.
O Node.js usa um modelo orientado a eventos e uma arquitetura de E / S sem bloqueio , o que o torna leve e eficiente. Isso não é uma estrutura, nem uma biblioteca, é um tempo de execução JavaScript.
Vamos escrever um pequeno exemplo:
E / S sem bloqueio
O Node.js usa operações de entrada / saída sem bloqueio, o que isso significa:
- O encadeamento principal não será bloqueado pelas operações de E / S.
- O servidor continuará a atender solicitações.
- Teremos que trabalhar com código assíncrono .
Vamos escrever um exemplo no qual o servidor envia uma página HTML em resposta a uma solicitação para /home
e para todas as outras solicitações - 'Hello World'. Para enviar uma página HTML, você deve primeiro lê-la em um arquivo.
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);
Se o URL solicitado for /home
, o módulo fs
nativo será usado para ler o arquivo home.html
.
Funções que se enquadram em http.createServer
e fs.readFile
como argumentos são retornos de chamada . Essas funções serão executadas em algum momento no futuro (a primeira assim que o servidor receber a solicitação e a segunda quando o arquivo for lido do disco e colocado no buffer).
Enquanto o arquivo está sendo lido do disco, o Node.js pode processar outras solicitações e até mesmo ler o arquivo novamente e tudo isso em um fluxo ... mas como ?!
Loop de eventos
O loop de eventos é a mágica que acontece dentro do Node.js. Este é literalmente um loop sem fim e, na verdade, um thread.
Libuv é uma biblioteca C que implementa esse padrão e faz parte do kernel do Node.js. Você pode aprender mais sobre o libuv aqui .
Um ciclo de eventos possui 6 fases, cada execução de todas as 6 fases é chamada de tick .
- timers : nesta fase, retornos de chamada agendados pelos métodos
setTimeout()
e setInterval()
são executados; - retornos de chamada pendentes : quase todos os retornos de chamada são executados, exceto eventos de
close
, temporizadores e setImmediate()
; - ocioso, prepare : usado apenas para fins internos;
- sondagem : responsável por receber novos eventos de E / S. Node.js pode bloquear neste momento;
- check : retornos de chamada causados pelo método
setImmediate()
são executados neste estágio; - fechar retornos de chamada : por exemplo
socket.on('close', ...)
;
Bem, existe apenas um segmento, e esse segmento é um loop de eventos, mas quem realiza toda a E / S?
Preste atenção
!!!
Quando um loop de eventos precisa executar uma operação de E / S, ele usa o encadeamento do SO do conjunto de encadeamentos e, quando a tarefa é concluída, o retorno de chamada é enfileirado durante a fase de retorno de chamada pendente .
Isso não é legal?
O problema das tarefas que consomem muita CPU
O Node.js parece perfeito! Você pode criar o que quiser.
Vamos escrever uma API para calcular números primos.
Um número primo é um número inteiro (natural) maior que um e divisível por apenas 1 e por si só.
Dado um número N, a API deve calcular e retornar os primeiros N primos na lista (ou 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
é a implementação dos cálculos necessários: a função isPrime
verifica se o número é primo e nthPrime retorna N tais números.
O arquivo index.js
é responsável por criar o servidor e usa o módulo prime.js
para processar cada solicitação para /primes
. O número N é lançado através da string de consulta no URL.
Para obter os 20 primeiros números primos, precisamos fazer uma solicitação para http://localhost:8080/primes?n=20
.
Suponha que tenhamos 3 clientes nos bloqueando e tentando acessar nossa API de E / S sem bloqueio:
- A primeira consulta 5 inicia a cada segundo.
- O segundo pede 1000 números primos a cada segundo
- O terceiro solicita 10.000.000.000 de números primos, mas ...
Quando o terceiro cliente envia uma solicitação, o encadeamento principal é bloqueado e esse é o principal sintoma do problema de tarefas com uso intenso de CPU . Quando o thread principal está ocupado executando uma tarefa "pesada", fica inacessível para outras tarefas.
Mas e o libuv? Se você se lembra, essa biblioteca ajuda o Node.js. a executar operações de entrada / saída usando threads do sistema operacional, evitando o bloqueio do thread principal e você está absolutamente certo, esta é a solução para o nosso problema, mas para que isso seja possível, nosso módulo deve ser escrito no idioma C ++ para que o libuv possa trabalhar com ele.
Felizmente, a partir da v10.5, o módulo Native Threads do Trabalhador foi adicionado ao Node.js.
Trabalhadores e seus fluxos
Como a documentação nos diz:
Os trabalhadores são úteis para executar operações JavaScript com uso intenso de CPU; não os use para operações de entrada / saída; os mecanismos já incorporados ao Node.js são mais eficientes no manuseio dessas tarefas do que o encadeamento Worker.
Correção de código
É hora de reescrever nosso 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);
No index-workerthreads.js
, cada solicitação para /primes
cria uma instância da classe Worker
(do módulo nativo worker_threads
) para fazer upload e executar o primes-workerthreads.js
no encadeamento do trabalhador. Quando a lista de números primos é calculada e pronta, o evento da message
é acionado - o resultado cai no fluxo principal devido ao trabalho que ainda não tem trabalho, ele também aciona o evento de exit
, permitindo que o fluxo principal envie dados ao cliente.
primes-workerthreads.js
mudou um pouco. Ele importa workerData
(esta é uma cópia dos parâmetros passados do encadeamento principal) e parentPort
através dos quais o resultado do trabalho do trabalhador é passado de volta para o encadeamento principal.
Agora vamos tentar nosso exemplo novamente e ver o que acontece:
O encadeamento principal não está mais bloqueado
!!!!!
Agora tudo funciona como deveria, mas produzir trabalhadores sem motivo ainda não é uma boa prática; criar threads não é um prazer barato. Certifique-se de criar um pool de threads antes disso.
Conclusão
O Node.js é uma tecnologia poderosa que deve ser explorada sempre que possível.
Minha recomendação pessoal - sempre seja curioso! Se você souber como algo funciona por dentro, pode trabalhar com ele de forma mais eficiente.
Isso é tudo por hoje pessoal. Espero que este post tenha sido útil para você e que você aprendeu algo novo sobre o Node.js.
Obrigado pela leitura e até a próxima postagem.
.