您需要了解的所有关于Node.js的信息

哈Ha! 我向您呈现JorgeRamón的文章“您需要了解的有关Node.js的所有知识”的翻译。



如今,Node.js平台是用于构建高效且可扩展的REST API的最受欢迎的平台之一。 它还适用于构建混合移动应用程序,桌面程序,甚至用于物联网。


我已经使用Node.js平台超过6年了,我真的很喜欢它。 这篇文章主要是试图作为Node.js实际工作方式的指南。


让我们开始吧!


将讨论什么:




Node.js之前的世界


多线程服务器


遵循客户机/服务器体系结构编写的Web应用程序的工作方式如下:客户机从服务器请求必要的资源,服务器作为响应发送资源。 在此方案中,服务器响应该请求并终止连接。


该模型之所以有效,是因为对服务器的每个请求都消耗资源(内存,处理器时间等)。 为了处理来自客户端的每个后续请求,服务器必须完成对前一个请求的处理。


这是否意味着服务器一次只能处理一个请求? 真的不是! 服务器收到新请求后,将创建一个单独的线程来处理它。


简而言之, 流程是CPU分配用于执行一小段指令的时间和资源。 话虽如此,服务器一次只能处理多个请求,但每个线程只能处理一个。 这样的模型也称为每个请求线程模型



要处理N个请求,服务器需要N个线程。 如果服务器收到N + 1个请求,则它必须等待直到其中一个线程可用。


在上图中,服务器一次最多可以处理4个请求(线程),并且当它接收到接下来的3个请求时,这些请求必须等待,直到这4个线程中的任何一个可用为止。


摆脱限制的一种方法是向服务器添加更多资源(内存,处理器内核等),但这不是最佳解决方案...。



并且,当然,不要忘记技术限制。


阻止输入/输出


服务器上有限数量的线程不是唯一的问题。 也许您想知道为什么单个线程不能同时处理多个请求? 都是由于阻塞了I / O操作



假设您正在开发在线商店,并且需要一个页面,用户可以在其中查看所有产品的列表。


用户敲了一下http://yourstore.com/products ,服务器作为响应,呈现了数据库中所有产品的HTML文件。 一点也不复杂,对吧?


但是幕后发生了什么?


  • 用户敲开/products必须执行特定的方法或功能才能处理请求。 一小段代码(您或您的框架)解析请求URL,并寻找合适的方法或函数。 流正在运行
  • 现在,执行了所需的方法或函数,如第一段所述, 线程开始工作。
  • 由于您是一名优秀的开发人员,因此将所有系统日志保存到文件中,当然,要确保路由器执行所需的方法/功能,您还需要在“方法X执行中!”这一行中进行日志记录。输入/输出流正在等待
  • 保存所有日志,并执行以下功能行。 该线程再次工作
  • 是时候访问数据库并获取所有产品了-像SELECT * FROM products这样的简单查询就可以完成工作,但是您猜怎么着? 是的,这是一个阻塞的I / O操作。 流正在等待
  • 您已经收到一个阵列或所有产品的列表,但是请确保已保证所有这些。 流正在等待
  • 现在您已经拥有了所有产品,现在该为将来的页面呈现模板了,但是在此之前,您需要阅读它们。 流正在等待
  • 呈现引擎完成其工作,并将响应发送给客户端。 该线程再次工作
  • 流是自由的,就像天空中的鸟。

I / O操作有多慢? 好吧,这取决于具体情况。 让我们看一下表格:


运作方式CPU周期
CPU寄存器3项措施
L1快取8项措施
L2快取12项措施
内存150措施
磁碟30,000,000措施
联播网250,000,000条

网络和磁盘读取操作太慢。 想象一下,在这段时间内您的系统可以处理多少个外部API请求或调用。


总结一下:I / O操作使线程等待并浪费资源。




问题C10K


问题


C10k (英文C10k; 10k个连接 -1万个连接问题)


在2000年代初期,服务器和客户端计算机运行缓慢。 当并行处理与同一台计算机的10,000个客户端连接时,就会出现此问题。


但是,为什么传统的“每请求线程数”模型(按请求线程数)无法解决此问题? 好吧,让我们使用一些数学。


线程的本机实现为每个流分配超过1 MB的内存,剩下的-对于1万个线程,需要10 GB的RAM,这仅用于流堆栈。 是的,不要忘记,我们处于2000年代初期!



如今,服务器和客户端计算机可以更快,更高效地工作,几乎任何编程语言或框架都可以解决此问题。 但实际上,问题尚未解决。 对于与一台计算机的一千万个客户端连接,问题再次出现(但现在是C10M Problem )。


JavaScript救援?


小心扰流板 !!!
Node.js实际上解决了C10K问题...但是如何呢?


在2000年代初期,服务器端JavaScript并不是什么新奇的东西,当时,已经有基于JVM(java虚拟机)的实现-RingoJS和AppEngineJS,它们可以按请求的线程模型工作。


但是,如果他们不能解决问题,那么Node.js怎么可能? 都是因为JavaScript是单线程的




Node.js和事件循环


Node.js


Node.js是运行在Google Chrome引擎-V8上的服务器平台,可以将JavaScript代码编译为机器代码。


Node.js使用事件驱动模型和无阻塞I / O架构,这使其轻巧高效。 这不是框架,也不是库,它是JavaScript运行时。


让我们写一个小例子:


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

非阻塞I / O


Node.js使用非阻塞输入/输出操作,这意味着什么:


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

让我们写一个示例,其中服务器发送HTML页面以响应对/home的请求,并响应所有其他请求-“ Hello World”。 要发送HTML页面,您必须首先从文件中读取它。


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

如果请求的URL是/home ,则使用本机fs模块读取home.html文件。


作为参数属于http.createServerfs.readFile函数是回调 。 这些功能将在将来的某个时候执行(第一个功能,服务器收到请求后,第二个功能,从磁盘读取文件并将其放置在缓冲区中)。


从磁盘读取文件时,Node.js可以处理其他请求,甚至可以在一个流中再次读取文件以及所有这些信息……但是如何?


事件循环


事件循环是Node.js内部发生的魔术。 这实际上是一个无限循环,实际上是一个线程。



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


一个事件周期有6个阶段,所有6个阶段的每次执行都称为tick



  • timers :在此阶段,执行由setTimeout()setInterval()方法安排的回调;
  • 待处理的回调 :几乎执行所有回调 ,但close事件,计时器和setImmediate()
  • 空闲,准备 :仅用于内部目的;
  • poll :负责接收新的I / O事件。 此时,Node.js可能会阻塞;
  • check :由setImmediate()方法引起的回调在此阶段执行;
  • close回调 :例如socket.on('close', ...) ;

好吧,只有一个线程,而这个线程是一个事件循环,但是谁来执行所有的I / O?


注意一下 !!!
当事件循环需要执行I / O操作时,它将使用线程池中的OS线程,并且当任务完成时,回调将在挂起的回调阶段排队。



那不是很酷吗?




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

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是必要计算的实现: prime.js函数检查数字是否为质数,nthPrime返回N个此类数字。


index.js文件负责创建服务器,并使用prime.js模块处理对/primes prime.js每个请求。 URL中的查询字符串会引发数字N。


要获得前20个素数,我们需要向http://localhost:8080/primes?n=20发出请求。


假设我们有3个客户敲诈我们并尝试访问我们的非阻塞I / O API:


  • 第一个查询每秒查询5个素数。
  • 第二个每秒请求1000个素数
  • 第三个请求10,000,000,000个素数,但是...


当第三个客户端发送请求时,主线程被阻塞,这是CPU密集型任务问题的主要症状。 当主线程忙于执行“繁重”任务时,其他任务将无法访问它。


但是libuv呢? 如果您还记得,这个库可以帮助Node.js使用OS线程执行输入/输出操作,避免阻塞主线程,您绝对是对的,这是解决我们问题的方法,但是为此,我们的模块必须使用以下语言编写: C ++,因此libuv可以使用它。


幸运的是,从v10.5开始,本机Worker Threads模块已添加到Node.js。



工人及其流动


文档所示


辅助程序对于执行CPU密集型JavaScript操作非常有用; 不要将它们用于输入/输出操作,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)); 

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

index-workerthreads.js ,对/primes每个请求都会创建Worker类的实例(来自本机模块worker_threads ),以将primes-workerthreads.js文件上载并执行到worker线程中。 当准备好素数列表时,将触发message事件-由于工作人员没有工作,结果落入了主流,他还触发了exit事件,从而使主流可以将数据发送到客户端。


primes-workerthreads.js有所更改。 它导入workerData (这是从主线程传递来的参数的副本)和parentPort通过它们工人的工作结果将被传递回主线程。


现在,让我们再次尝试我们的示例,看看会发生什么:



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



现在一切正常,但是无缘无故地生产工人仍然不是一种好习惯;创建线程并不是一种廉价的乐趣。 确保在此之前创建线程池。


结论


Node.js是一项强大的技术,应尽可能地对其进行探索。
我的个人建议-总是好奇! 如果您知道内部是如何工作的,则可以更有效地使用它。


今天的家伙就这些了。 希望本文对您有所帮助,并且您了解了有关Node.js的新知识。


感谢您的阅读,在接下来的文章中见。

Source: https://habr.com/ru/post/zh-CN460661/


All Articles