我有一个关于Node.js流的问题 - 特别是它们在概念上如何工作。
不缺乏关于如何使用流的文档。但是我很难找到数据流的工作方式。
我对网络通信的有限理解,HTTP,是完整的"包"数据来回发送。与订购公司目录的个人类似,客户端向服务器发送GET(目录)请求,服务器响应目录。浏览器没有收到目录的页面,而是整本书。
节点流可能是多部分消息吗?
我喜欢REST模型 - 特别是它是无状态的。浏览器和服务器之间的每次交互都是完全自包含且足够的。节点流因此不是RESTful吗?一位开发人员提到了与套接字管道的相似性,这使得连接保持打开状回到我的目录订购示例,这会像是一个商业广告系列"但是等等!还有更多!"而不是完全包含的目录?
很大一部分流是接收器“下游”的能力。发送消息,例如暂停' &安培; '继续'上游。这些消息包括什么?他们是POST吗?
最后,我对Node如何工作的有限视觉理解包括此事件循环。函数可以放在线程池的不同线程上,事件循环继续。但是,不应该发送数据流来保持事件循环被占用(即停止)直到流完成?它是如何继续关注“暂停”的?来自下游的请求?n事件循环是否将流放置在池中的另一个线程上以及何时遇到“暂停”?请求,检索相关的线程并暂停它?
我已经阅读了node.js文档,完成了nodeschool教程,构建了一个heroku应用程序,购买了两本书(真实的,自包含的,书籍,有点像以前说过的目录,可能不喜欢节点流),问几个节点"代码训练营的教练 - 所有人都谈论如何使用流,但没有人谈论下面实际发生的事情。
也许您遇到了一个很好的资源来解释这些是如何工作的?对于非CS心灵来说,这可能是一个很好的拟人类比吗?
答案 0 :(得分:8)
首先要注意的是:node.js流不仅限于HTTP请求。 HTTP请求/网络资源只是node.js中流的一个示例。
Streams对于可以在小块中处理的所有内容都很有用。它们允许您以更小的块处理潜在的巨大资源,更容易适合您的RAM。
假设您有一个文件(大小为几千兆字节),并希望将所有小写转换为大写字符并将结果写入另一个文件。天真的方法将使用fs.readFile
读取整个文件(为简洁起见省略了错误处理):
fs.readFile('my_huge_file', function (err, data) {
var convertedData = data.toString().toUpperCase();
fs.writeFile('my_converted_file', convertedData);
});
不幸的是,这个approch很容易压倒你的RAM,因为在处理它之前必须存储整个文件。您还会浪费宝贵的时间等待文件读取。以较小的块处理文件是否有意义?在等待硬盘提供剩余数据时,您可以在获得第一个字节后立即开始处理:
var readStream = fs.createReadStream('my_huge_file');
var writeStream = fs.createWriteStream('my_converted_file');
readStream.on('data', function (chunk) {
var convertedChunk = chunk.toString().toUpperCase();
writeStream.write(convertedChunk);
});
readStream.on('end', function () {
writeStream.end();
});
这种方法要好得多:
打开流后,node.js会打开文件并开始读取。一旦操作系统将一些字节传递给读取文件的线程,它将被传递给您的应用程序。
回到HTTP流:
暂停HTTP流:这不是在HTTP级别完成的,而是更低级别。如果你暂停流node.js将停止从底层TCP套接字读取。 然后发生的事情取决于内核。它仍然可以缓冲传入的数据,因此一旦完成当前的工作,它就为您准备好了。 It may also inform the sender at the TCP level that it should pause sending data。应用程序不需要处理这个问题。这不关他们的事。事实上,发件人应用程序可能甚至没有意识到你不再积极阅读!
所以它基本上是在数据可用时尽快提供,但不会压倒你的资源。基础努力工作由操作系统(例如net
,fs
,http
)或您正在使用的流的作者完成(例如zlib
一个Transform
信息流,通常用fs
或net
。}
答案 1 :(得分:3)
答案 2 :(得分:2)
我认为你正在思考这一切是如何运作的,我喜欢它。
Streams适用于两件事:
当一个操作很慢时,它会在你得到它们时给你部分结果。例如读取一个文件,它很慢,因为硬盘很慢,它可以在读取文件时为您提供部分文件。使用流,您可以使用文件的这些部分并立即开始处理它们。
他们也很好地将程序连接在一起(读取功能)。就像在命令行中一样,您可以将不同的程序连接在一起以产生所需的输出。示例:cat file | grep word
。
大多数需要时间处理并且可以在获取部分结果时给你部分结果的操作不是由Node.js完成的,它们是由V8 JS引擎完成的,它只将这些结果交给JS供你使用它们。
网页可以发送不同的编码方式。一开始只有一种方式。在请求时发送整页的位置。现在它有更高效的编码来做到这一点。其中一个是分块的,网页的一部分被发送,直到整个页面被发送。这很好,因为网页可以在收到时进行处理。想象一下网络浏览器。它可以在下载完成之前开始呈现网站。
首先,Node.js流只能在同一个Node.js程序中工作。 Node.js流不能与另一台服务器甚至程序中的流交互。
这意味着在下面的示例中,Node.js无法与网络服务器通信。它无法告诉它暂停或恢复。
Node.js <-> Network <-> Webserver
真正发生的是Node.js要求一个网页并开始下载它,并且无法停止下载。只是放下插座。
它开始缓冲请求,直到您准备再次开始使用它。但下载从未停止过。
我有一个完整的答案准备解释事件循环是如何工作的,但我认为watch this talk更适合你。
答案 3 :(得分:2)
那么,首先,什么是流? 嗯,使用流,我们可以处理意思是逐段读取和写入数据,而无需完成整个读取或写入操作。因此,我们不必将所有数据保留在内存中即可执行这些操作。
例如,当我们使用流读取文件时,我们读取部分数据,对其进行处理,然后释放内存,然后重复此操作,直到处理完整个文件为止。或者想想YouTube或Netflix,它们都被称为“流媒体公司” ,因为它们使用相同的原理流式传输视频 。
因此,不必等到整个视频文件加载完毕,而是逐段或逐块地进行处理,这样,即使在下载整个文件之前,您也可以开始观看。因此,这里的原理不仅仅是关于Node.JS。但是一般来说对于计算机科学是普遍的。
因此,如您所见,这使流成为处理大量数据(例如视频或我们正在从外部来源逐段接收的数据)的理想选择。此外,流传输使数据在内存方面的处理效率更高,因为不需要将所有数据都保留在内存中,也就时间而言,因为我们可以在数据到达时开始处理数据,< strong>而不是等到一切都到达。
如何在Node.JS中实现它们:
因此在Node中,流有四种基本类型: 可读流,可写流,双工流和转换流。但是可读性和可写性是最重要的,可读性流是我们可以读取并消费数据的流。流在核心Node模块中无处不在,例如,http服务器收到请求时进入的数据实际上是可读流。因此,与请求一起发送的所有数据都是逐块而不是一大块。同样,来自文件系统的另一个示例是,我们可以使用FS模块中的读取屏幕来逐段读取文件,这实际上对于大型文本文件非常有用。
另外要注意的一件事是,流实际上是 EventEmitter 类的实例。这意味着所有流都可以发出并侦听命名的事件。对于可读流,它们可以发出,我们可以收听许多不同的事件。但是最重要的两个是数据和结束事件。 当有新数据要消耗时会发出数据事件,而没有更多数据要消耗时就发出结束事件。当然,我们可以对这些事件做出相应的反应。
最后,除了事件,我们还具有可以在流上使用的重要功能。对于可读流,最重要的是管道和读取功能。超级重要的管道功能,基本上使我们能够将流连接在一起,将数据从一个流传递到另一个流,而不必担心任何事件。
接下来,我们可以向其中写入数据的可写流。因此,基本上,这与可读流相反。一个很好的例子是我们可以发送回客户端的http响应,它实际上是可写的流。因此,我们可以将数据写入其中的流。因此,当我们要发送数据时,我们必须将其写入某个地方,对吗?那个地方是可写的数据流,这很有意义,对吗?
例如,如果我们想将大视频文件发送给客户端,我们就像Netflix或YouTube一样。现在有关事件,最重要的是消耗事件和结束事件。最重要的功能是写入和结束功能。
关于双工流。它们只是同时可读写的流。这些不太常见。但是无论如何,一个很好的例子是来自net模块的Web套接字。 Web套接字基本上只是客户端和服务器之间的通信通道,它可以双向运行,并且在建立连接后保持打开状态。
最后,变换流是双工流,因此它们是可读和可写的流,它们同时可以在读取或写入数据时修改或变换数据。一个很好的例子是zlib核心模块,用于压缩实际上使用转换流的数据。
***节点将这些http请求和响应实现为流,然后可以使用,可以使用每种流类型可用的事件和函数来使用它们。当然,我们也可以实现自己的流,然后使用这些相同的事件和功能来使用它们。
现在让我们尝试一些示例:
const fs = require('fs');
const server = require('http').createServer();
server.on('request', (req, res) =>{
fs.readFile('./txt/long_file.txt', (err, data)=>{
if(err) console.log(err);
res.end(data);
});
});
server.listen('8000','127.0.01', ()=>{
console.log(this);
});
假设long_file.txt文件包含1000000K行,每行包含100个以上的单词,那么这是一个包含大量数据的拥抱文件,现在在上面的示例中问题是通过使用 readFile()函数节点会将整个文件加载到内存中,因为只有在将整个文件加载到内存节点后才能将数据作为响应对象进行传输。
当文件很大时,以及当有大量请求发送到服务器时,随着时间的流逝,节点进程将很快耗尽资源,并且您的应用程序将退出工作,一切都会崩溃。
让我们尝试使用流找到解决方案:
const fs = require('fs');
const server = require('http').createServer();
server.on('request', (req, res) =>{
const readable = fs.createReadStream('./txt/long_file.txt');
readable.on('data', chunk=>{
res.write(chunk);
});
readable.on('end',()=>{
res.end();
})
readable.on('error', err=>{
console.log('err');
res.statusCode=500;
res.end('File not found');
});
});
server.listen('8000','127.0.01', ()=>{
console.log(this);
});
在上面的示例中,使用流,我们正在有效地流传输文件,正在读取文件的一部分,并在可用后立即使用响应的write方法将其发送给客户端。流。然后,当下一个价格可用时,将发送该价格,一直到读取整个文件并将其流式传输到客户端。
因此,流基本上已完成从文件中读取数据,结束事件将发出信号,表明不再有数据将写入此可写流。
通过上述实践,我们解决了先前的问题,但上述示例仍然存在巨大的问题,称为反压。
问题在于,我们可读的流(用于从磁盘读取文件的可读流)比通过网络实际发送带有响应可写流的结果快得多。这将使响应流不堪重负,因为响应流无法如此快地处理所有这些传入数据,因此该问题称为背压。
该解决方案使用管道运算符,它将处理数据传入和传出的速度。
const fs = require('fs');
const server = require('http').createServer();
server.on('request', (req, res) =>{
const readable = fs.createReadStream('./txt/long_file.txt');
readable.pipe(res);
});
server.listen('8000','127.0.01', ()=>{
console.log(this);
});