关于 Node.js 你需要知道的一切

2025-05-25

关于 Node.js 你需要知道的一切

Node.js是当今构建可扩展且高效的 REST API 最流行的技术之一。它还用于构建混合移动应用程序、桌面应用程序,甚至物联网。

我使用 Node.js 已经大约六年了,我非常喜欢它。这篇文章旨在成为理解 Node.js 工作原理的终极指南。

让我们开始吧!!

目录

Node.js 之前的世界

多线程服务器

Web 应用程序采用客户端/服务器模型编写,客户端向服务器请求资源,服务器则返回资源。服务器仅在客户端请求时响应,并在每次响应后关闭连接。

这种模式非常高效,因为每个对服务器的请求都需要时间和资源(内存、CPU 等)。服务器必须先完成前一个请求,才能处理下一个请求。

那么,服务器每次只能处理一个请求吗?其实不然,当服务器收到一个新请求时,该请求将由一个线程来处理。

简单来说,线程就是CPU 分配来执行一小段指令的时间和资源。也就是说,服务器会同时处理多个请求,每个线程一个请求(也称为“每个请求一个线程”模型)。

为了同时处理 N 个请求,服务器需要 N 个线程。如果服务器收到 N+1 个请求,则必须等待,直到这 N 个线程中有一个可用。

在多线程服务器示例中,服务器允许同时最多 4 个请求(线程),并且当它接收到接下来的 3 个请求时,这些请求必须等待,直到这 4 个线程中的任何一个可用。

解决此限制的一种方法是向服务器添加更多资源(内存、CPU 内核等),但这可能根本不是一个好主意......

当然,也会存在技术限制。

阻塞 I/O

服务器中的线程数量并不是唯一的问题。也许你想知道为什么单个线程无法同时处理两个或更多请求?这是因为阻塞了输入/输出操作

假设您正在开发一个在线商店,它需要一个页面供用户查看您的所有产品。

用户访问http://yourstore.com/products,服务器会从数据库中渲染出一个包含所有产品的 HTML 文件。很简单吧?

但是,后面会发生什么?...

  1. 当用户访问/products时,需要执行特定的方法或函数来处理请求,因此一小段代码(可能是你自己的或框架的)会解析请求的 URL 并搜索正确的方法或函数。线程正在运行。✔️

  2. 方法或函数已执行,且前几行代码也已执行。线程正在运行。✔️

  3. 因为你是一名优秀的开发人员,所以你会将所有系统日志保存在一个文件中。当然,为了确保路由执行的是正确的方法/函数,你会记录一个“方法 X 正在执行!!”的字符串,这是一个阻塞的 I/O 操作。线程正在等待。❌

  4. 日志已保存,正在执行下一行。线程已恢复正常运行。✔️

  5. 现在该去数据库获取所有产品了,一个简单的查询(例如SELECT * FROM products)就能搞定,但你猜怎么着?这是一个阻塞 I/O 操作。线程正在等待。❌

  6. 你会得到一个包含所有产品的数组或列表,但为了确保记录下来,线程正在等待。❌

  7. 有了这些产品,就该渲染模板了,但在渲染之前,你需要先读取它。线程正在等待。❌

  8. 模板引擎已完成工作,并将响应发送给客户端。线程已恢复正常运行。✔️

  9. 线程是自由的,就像一只鸟。🕊️

I/O 操作到底有多慢?这得看具体情况。
我们来看看下面的表格:

手术 CPU 时钟周期数
CPU寄存器 3 个刻度
L1缓存 8 个刻度
二级缓存 12个刻度
内存 150个刻度
磁盘 30,000,000 个刻度
网络 250,000,000 个刻度

磁盘和网络操作太慢。您的系统发出了多少次查询或外部 API 调用?

恢复时,I/O操作会使线程等待并浪费资源。

C10K问题

问题

在 21 世纪初,服务器和客户端的速度都很慢。问题在于如何在一台服务器上同时处理 10,000 个客户端连接。

但是为什么我们传统的“每个请求一个线程”模型无法解决这个问题呢?好吧,我们来算一下。

本机线程实现为每个线程分配大约 1 MB 的内存,因此 10k 个线程仅用于线程堆栈就需要10GB 的 RAM,请记住我们处于 21 世纪初!

如今,服务器和客户端的性能都比这更好,几乎所有编程语言和/或框架都能解决这个问题。实际上,这个问题已经升级到在一台服务器上处理 1000 万个客户端连接(也称为C10M 问题)。

JavaScript 可以拯救吗?

剧透警告🚨🚨🚨!!
Node.js 解决了 C10K 问题……但是为什么呢?!

Javascript 服务器端在 21 世纪初并不新鲜,当时在 Java 虚拟机之上有一些基于每个请求线程模型的实现,如 RingoJS 和 AppEngineJS。

但如果这都不能解决 C10K 问题,那为什么 Node.js 能解决呢?因为 JavaScript 是单线程的

Node.js 和事件循环

Node.js

Node.js 是一个基于 Google Chrome 的 Javascript 引擎(V8 引擎)构建的服务器端平台,它将 Javascript 代码编译为机器代码。

Node.js 使用事件驱动的非阻塞 I/O 模型,使其轻量且高效。它不是一个框架,也不是一个库,而是一个运行时环境。

让我们写一个简单的例子:



// Importing native http module
const http = require('http');

// Creating a server instance where every call
// the message 'Hello World' is responded to the client
const server = http.createServer(function(request, response) {
  response.write('Hello World');
  response.end();
});

// Listening port 8080
server.listen(8080);


Enter fullscreen mode Exit fullscreen mode

非阻塞 I/O

Node.js 是非阻塞 I/O,这意味着:

  1. 主线程不会在 I/O 操作中被阻塞。
  2. 服务器将继续处理请求。
  3. 我们将使用异步代码

让我们写一个例子,每次/home请求服务器都会发送一个 HTML 页面,否则服务器会发送“Hello World”文本。要发送 HTML 页面,需要先读取文件。

主页.html



<html>
  <body>
    <h1>This is home page</h1>
  </body>
</html>


Enter fullscreen mode Exit fullscreen mode

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);   


Enter fullscreen mode Exit fullscreen mode

如果请求的 url 是/home使用fs本机模块,那么我们就读取home.html文件。

传递给http.createServer和 的函数fs.readFile被称为回调函数。这些函数将在未来的某个时间执行(第一个函数在服务器收到请求时执行,第二个函数在文件读取完毕并缓冲内容时执行)。

在读取文件时,Node.js 仍然可以处理请求,甚至可以在单个线程中同时再次读取文件……但是怎么做呢?!

事件循环

事件循环是 Node.js 背后的魔法。简而言之,事件循环实际上是一个无限循环,并且是唯一可用的线程。

Libuv是一个实现此模式的 C 库,它是 Node.js 核心模块的一部分。您可以在此处阅读有关 libuv 的更多信息。

事件循环有六个阶段,所有阶段的执行称为一个tick

  • 计时器:此阶段执行由setTimeout()和安排的回调setInterval()
  • 待处理回调:执行几乎所有回调,但关闭回调、计时器安排的回调等除外setImmediate()
  • 空闲,准备:仅在内部使用。
  • poll:检索新的 I/O 事件;节点将在适当的时候在此处阻塞。
  • 检查setImmediate()回调在这里被调用。关闭回调:比如socket.on(‘close’)

好的,所以只有一个线程,该线程是事件循环,但谁来执行 I/O 操作?

注意📢📢📢!!!
当事件循环需要执行 I/O 操作时,它会使用来自池的 OS 线程(通过 libuv 库),当作业完成时,回调将排队等待在待处理回调阶段执行。

这不是很棒吗?

CPU 密集型任务的问题

Node.js 看起来很完美,你可以构建任何你想要的东西。

让我们构建一个 API 来计算素数。

质数是大于 1 的整数,其因数只有 1 和它本身。

给定一个数字 N,API 必须计算并返回列表(或数组)中的前 N ​​个素数。

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 };


Enter fullscreen mode Exit fullscreen mode

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);


Enter fullscreen mode Exit fullscreen mode

prime.js是素数的实现,isPrime检查给定一个数字 N,该数字是否为素数,并nthPrime获取第 n 个素数(当然)。

index.js创建一个服务器,并在每次调用时使用该库/primes。N 号通过查询字符串传递。

为了获取前 20 个素数,我们向 发出请求http://localhost:8080/primes?n=20

假设有 3 个客户端尝试访问这个令人惊叹的非阻塞 API:

  • 第一个每秒请求前 5 个素数。
  • 第二个每秒请求前 1,000 个素数。
  • 第三个请求一次前 10,000,000,000 个素数,但是......

当第三个客户端发送请求时,主线程会被阻塞,这是因为素数库是CPU 密集型的。主线程正忙于执行 CPU 密集型代码,无法执行其他任何操作。

但是 libuv 呢?如果你还记得这个库是如何帮助 Node.js 使用 OS 线程执行 I/O 操作以避免阻塞主线程的,那么这就是我们问题的解决方案。但是要使用 libuv,我们的库必须用 C++ 语言编写。

值得庆幸的是,Node.js v10.5 引入了Worker Threads

工作线程

正如文档所述:

Worker 对于执行 CPU 密集型 JavaScript 操作很有用;不要将它们用于 I/O,因为 Node.js 内置的异步执行操作机制已经比 Worker 线程更有效地处理它。

修复代码

现在该修复我们的初始代码了:

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));


Enter fullscreen mode Exit fullscreen mode

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);


Enter fullscreen mode Exit fullscreen mode

index-workerthreads.js每次调用都会创建一个新的类实例Worker(来自原生模块),以便在工作线程中worker_threads加载并执行文件。当素数列表计算完成后,事件会被触发,并将结果发送到主线程;由于任务已完成,事件也会被触发,让主线程将数据发送到客户端。primes-workerthreads.jsmessageexit

primes-workerthreads.js稍微改变了一下。它导入了workerData(从主线程传递的参数),parentPort这就是我们向主线程发送消息的方式。

现在让我们再次执行 3 个客户端示例,看看会发生什么:

主线程不再阻塞🎉🎉🎉🎉🎉!!!!!

它按预期工作,但像这样生成工作线程并非最佳实践,创建新线程的成本并不低。请务必事先创建线程池。

结论

Node.js 是一项强大的技术,值得学习。
我的建议是保持好奇心,了解事物的运作方式,才能做出更好的决策。

好了,各位,就到这里吧。希望你们对 Node.js 有了新的认识。
感谢阅读,期待下篇文章再见❤️。

文章来源:https://dev.to/jorge_rockr/everything-you-need-to-know-about-node-js-lnc
PREV
20 个可部署任何应用程序的站点(付费/免费替代方案)
NEXT
免费部署个人网站/PHP+MySQL Web 应用的 6 种方法