什么是 Node.js?为什么使用 Node.js
什么是 Node.js
在浏览器中运行的传统意义上的 JavaScript。这是因为浏览器的核心分为两部分:渲染引擎和JavaScript引擎。前者负责渲染 HTML + CSS,后者负责 JavaScript 执行。 Chrome使用的JavaScript引擎是V8,速度非常快。
Node.js是一个服务器端框架,底层使用V8引擎。我们知道Apache+PHP和Java Servlet可以用来开发动态网页。 Node.js 与它们类似,但是是使用 JavaScript 开发的。
介绍完定义,我们举一个简单的例子。新建一个app.js文件,添加以下内容:
var http = require('http');
http.createServer(function (request, response) {
response.writeHead(200, {'Content-Type': 'text/plain'}); // HTTP Response 头部
response.end('Hello World\n'); // 返回数据 “Hello World”
}).listen(8888); // 监听 8888 端口
// 终端打印如下信息
console.log('Server running at http://127.0.0.1:8888/');
这样简单的HTTP Server就完成了。上传node app.js
运行,稍后访问即可看到结果。
为什么使用 Node.js
在处理新技术时,最好问一些关于原因的问题。既然PHP、Python、Java都可以用于后端开发,那为什么还要学习Node.js呢?至少我们应该知道什么情况下选择Node.js更好。
一般来说,Node.js适合以下场景:
- 实时应用,例如在线多人协作工具、网络聊天中的应用等。 O,比如向客户端提供API,读取数据。
- 实时应用程序,例如经常上传文件的客户端。
- 前后边缘分离。
其实前两者可以归结为一,那就是客户使用长期的沟通。虽然并发数很大,但大部分都是被动连接。
Node.js 也有其局限性。它不适合CPU密集型任务,例如人工计算、视频和图像处理等。
显然,这些错误不是随机的,也不是记忆的,也不是简单地重复别人说的话。我们需要对Node.js原理有具体的了解才能做出准确的判断。
基本概念
在介绍Node.js之前,先解释一下一些基本概念,将有助于你更深入地理解Node.js。
并发
与客户端不同,服务器设计者非常关心的一个数据是并发数,即该服务器能够支持客户端同时请求的数量。最初的C10K问题是如何使用单台服务器支持10K并发。当然,随着软硬件性能的提升,C10K不再是问题。我们开始尝试解决C10M问题,也就是单台服务器如何处理百万并发。
C10K交付时,我们还在使用Apache服务器。它的运行原理是,每当网络请求到达时,它就中断子进程,并在子进程中运行PHP脚本。脚本执行后,将结果发送给客户端。
这样可以保证不同的进程不会互相排斥。即使某个进程出现问题,也不会影响整个服务器。但缺点也很明显:进程是一个相当重的概念,有自己的堆栈,需要大量内存。 ,服务器可以运行的进程数有上限,可以是几千个左右。
虽然Apache后来使用了FastCGI,但它只是一个进程池,减少了创建进程的成本,但无法增加并发数。
Java Servlet 使用线程池,即每个 Servlet 运行在单个线程上。虽然螺纹比台阶轻,但它们也是相连的。尝试了每个线程的单独堆栈大小为1M,仍然不行。另外,多线程程序会带来各种各样的问题,程序员必须意识到这一点。
如果不使用线程,有两种解决方案,使用协程和非阻塞I/O。协程比线程更轻。多个协程可以在单个线程上运行并由程序员调度。这项技术在Go语言中得到了广泛的应用。 Node.js 使用非阻塞 I/O 来处理高兼容性情况。
非阻塞I/O
这里描述的I/O可以分为两种类型:网络I/O和文件I/O。事实上,两者非常相似。 I/O 可分为两个阶段。首先,将文件(网络)的内容复制到缓冲区。该缓冲区位于操作系统的特殊内存区域。然后缓冲区的内容被复制到用户程序存储区域。
对于阻塞 I/O,它会阻塞从发起读取请求、准备缓冲区到用户进程接收数据的两个步骤。
非忙I/O轮询内核以查看缓冲区是否准备好,如果没有准备好则继续执行其他工作。当缓冲区准备好时,缓冲区的内容被复制到用户进程。这个过程是非常受限制的。
I/O复用技术是指使用单个线程处理多个I/O网络。俗称 select
、epoll
用于轮询所有套接字的函数。例如,Apache 使用前者,而 Nginx 和 Node.js 使用后者。不同的是后者更有效。由于 I/O 多路复用是一种单向轮询,因此它也是一种非阻塞 I/O 解决方案。
异步I/O是理想的I/O模型,但不幸的是,真正的异步I/O并不存在。 Linux 上的 AIO 使用信号和回调发送数据,但存在错误。 Windows现有的libeio和IOCP使用池和块I/O来模拟异步I/O。
Node.js 线程模型
很多文章都说 Node.js 是单线程的。不过,这种说法并不严格,甚至可以说是不负责任的,因为我们至少会思考以下几个问题:
- Node.js 是如何处理线程一的请求的?
- Node.js 如何在线程中对文件进行异步 I/O?
- Node.js 如何复用服务器上多个 CPU 的处理能力?
网络 I/O
Node.js 确实可以在单个线程中处理许多并发请求,但这需要编程技能。我们看一下文章开头的代码。执行app.js文件后,控制台立即有输出,我们访问网页时只会看到“Hello, World”。
这是因为Node.js是事件驱动的,也就是说只有网络请求事件发生时才会执行回调函数。当许多请求进来时,它们就会排队等待处理。
这看起来是理所当然的事情,但如果你没有深入理解 Node.js 运行在单线程上,并且回调操作是同时执行的,并且你按照传统的模型来设计程序,那么会导致这是一个大问题。作为一个简单的例子,这里的字符串“Hello World”可能是运行另一个模块的结果。如果认为“Hello World”的生成太耗时,则当前网络请求的回调将被阻塞,从而导致后续网络请求得不到应答。
解决方案很简单,使用异步回调机制即可。我们可以传递 response
参数用于在另一个模块中生成结果,异步生成结果,最后在回调函数中执行实际结果。这样做的好处是不会阻塞http.createServer
的回调函数,所以不会出现无响应的请求。
例如,我们更改服务器登录名。其实想要完成路线的话,思路都是一样的:
var http = require('http');
var output = require('./string') // 一个第三方模块
http.createServer(function (request, response) {
output.output(response); // 调用第三方模块进行输出
}).listen(8888);
第三方模块:
function sleep(milliSeconds) { // 模拟卡顿
var startTime = new Date().getTime();
while (new Date().getTime() < startTime + milliSeconds);
}
function outputString(response) {
sleep(10000); // 阻塞 10s
response.end('Hello World\n'); // 先执行耗时操作,再输出
}
exports.output = outputString;
总之,用Node.js编程时,比较耗时的事情必须异步完成。以避免阻塞当前操作。因为您正在向客户端提供服务,并且所有代码始终是单线程并顺序执行的。
如果初学者看到这里还是不明白,建议看《Nodejs入门》这本书,或者看下一章关于动作循环的内容。
文件I/O
我在上一篇文章中也强调过,异步是为了提升体验,避免延迟。要真正节省处理时间,发挥多核CPU性能,我们还是需要依赖多线程并行处理。
Node.js 在底层维护着一个游戏池。正如前面在基本概念部分提到的,不存在真正的异步文件 I/O,并且通常使用池来完成。线程池中有四个线程用于文件 I/O。
请注意,我们无法直接运营游泳池,也不需要关心它的存在。池的目的只是完成I/O操作,而不是执行CPU密集型任务,例如图像和视频处理、大规模计算等。
如果需要处理的CPU密集型任务很少,我们可以启动多个Node.js进程并使用IPC系统进行进程间通信,或者调用外部C++/Java程序。如果你有很多 CPU 密集型任务,那就意味着选择 Node.js 是一个错误的决定。
CPU水
到目前为止,我们知道Node.js使用I/O复用技术,使用单个线程来控制I/O网络,并使用一个池和几个线程来模拟异步。文件输入/输出。那么在 32 核 CPU 上,Node.js 的单线程是不是显得毫无用处呢?
答案是否定的,我们可以启动多个Node.js进程。与上一节不同的是,不需要与进程进行通信。它们都监听同一个端口,并在最外层使用 Nginx 进行负载均衡。
Nginx 负载均衡的设置非常简单,只需编辑配置文件即可:
http{
upstream sampleapp {
// 可选配置项,如 least_conn,ip_hash
server 127.0.0.1:3000;
server 127.0.0.1:3001;
// ... 监听更多端口
}
....
server{
listen 80;
...
location / {
proxy_pass http://sampleapp; // 监听 80 端口,然后转发
}
}
负载均衡的基本规则是将网络请求按顺序放在不同的端口上。我们可以使用标志least_conn
来转发到连接数最少的Node.js进程,也可以使用ip_hash
来确保但请求必须遵循相同的IP。在同一个 Node.js 进程中。
多个Node.js进程可以充分发挥多核CPU的处理能力,同时还具有很强的扩展性。
事件循环
Node.js 中有一个事件(Event Loop),有 iOS 开发经验的同学可能会比较熟悉。是的,它在某种程度上类似于 Runloop。
完整的Event Loop也可以分为几个步骤,依次是轮询、检查、关闭调用、定时器、I/O调用和Idle。
由于 Node.js 是事件驱动的,因此每个事件的回调函数将编写在事件循环的不同阶段。例如,对 fs.readFile
的调用将添加到 I/O 调用中,对 setImmediate
的调用将添加到下一个循环中。当轮询流程结束时, process.nextTick()
回调会在当前流程结束之后、下一个流程开始之前添加。
不同的异步方法回调会在不同的步骤中执行。掌握这一点很重要,否则会因为排序问题出现逻辑错误。
事件循环不断循环,在每一步中,该步骤中编写的所有函数调用都会同时执行。这就是为什么我在网络 I/O 部分说在运行回调时不要调用阻塞方法,并且始终使用异步逻辑来执行耗时的操作。耗时过长的回调函数会导致Event Loop长时间卡在一个进程中,新的网络请求无法得到及时响应。
因为本文的目的是对Node.js有一个初步全面的了解。我不会详细介绍事件循环的每个步骤。具体可以查看官方文档。
事实证明Event Loop有点low。为了充分利用事件驱动的概念,Node.js 包含了 EventEmitter
这个类:
var EventEmitter = require('events');
var util = require('util');
function MyThing() {
EventEmitter.call(this);
setImmediate(function (self) {
self.emit('thing1');
}, this);
process.nextTick(function (self) {
self.emit('thing2');
}, this);
}
util.inherits(MyThing, EventEmitter);
var mt = new MyThing();
mt.on('thing1', function onThing1() {
console.log("Thing1 emitted");
});
mt.on('thing2', function onThing1() {
console.log("Thing2 emitted");
});
根据输出,后面定义的 很多Node.js模块都继承自EventEmitter,比如下一节介绍的 使用数据流的好处是显而易见的,并且有现实生活中的例证。例如,老师在暑假布置家庭作业。如果学生每天做一点作业(作业增加),他们可以更轻松地完成作业。如果事情堆积如山,到最后一天,面对堆积如山的作业本,你会感到无助。 服务器开发也是如此。假设用户上传一个1G的文件或者读取本地1G的文件。没有数据流的概念,我们需要开一个1G的缓冲区,然后当缓冲区满的时候一次性全部处理。 如果我们使用流式数据,我们可以定义一个非常小的缓冲区,例如大小为1Mb。当缓冲区已满时,将执行回调函数来处理该小部分以消除积压。 其实读取文件 不同的流还可以链在一起,比如读取一个压缩文件,边读边解压,将解压后的内容写入到文件中: Node.js提供了一个非常简单的数据库操作,上面是简单介绍一下它的使用。 对于长并行连接,事件驱动模型比线程更轻,并且可以轻松扩展到具有负载均衡的多个Node.js进程。因此,Node.js 非常适合为 I/O 密集型应用程序提供服务。但这种方法的缺点是无法处理CPU密集型任务。 Node.js 通常以流的形式定义数据,这也为其提供了良好的封装。 Node.js 采用前端语言(JavaScript)和后端服务器开发,很好地实现了前后端分离。 作者:bestswifter 是 。 ,先执行,这与Event Loop的调用规则完全一致。
fs.readStream
,它用于创建可读流,打开文件,读取数据,read相关的事件会审核完成后将被抛出。 数据流
请求
和fs
模块是可读数据的:在另一种技术中,它可以用在通道内容中是另一种。 Stream:var fs = require('fs');
var readableStream = fs.createReadStream('file1.txt');
var writableStream = fs.createWriteStream('file2.txt');
readableStream.pipe(writableStream);
var fs = require('fs');
var zlib = require('zlib');
fs.createReadStream('input.txt.gz')
.pipe(zlib.createGunzip())
.pipe(fs.createWriteStream('output.txt'));
总结
链接:https://juejin.im/post/57b54f151532bc0063ebfe31
来源:掘金如需商业印刷,请联系作者获得许可。非商业转载请注明来源。
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。