ホームページ > ウェブフロントエンド > フロントエンドQ&A > ノードにはいくつかの種類のストリームがあります

ノードにはいくつかの種類のストリームがあります

青灯夜游
リリース: 2022-07-12 19:42:52
オリジナル
2233 人が閲覧しました

ノード ストリームには 4 つのタイプがあります。 1. 読み取り可能 (読み取り可能なストリーム)。コンテンツを返すには、「_read」メソッドを実装する必要があります。2. 書き込み可能 (書き込み可能なストリーム)、コンテンツを受け入れるには、「_write」メソッドを実装する必要があります。3. 二重 (読み取りおよび書き込み可能なストリーム)、「_read」および「コンテンツを受け入れて返すには、_write" メソッドを実装する必要があります。 4. Transform (変換ストリーム)、受信したコンテンツを変換して返すには、"_transform" メソッドを実装する必要があります。

ノードにはいくつかの種類のストリームがあります

このチュートリアルの動作環境: Windows 7 システム、nodejs バージョン 16、DELL G3 コンピューター。

ストリームは Nodejs の非常に基本的な概念であり、多くの基本モジュールがストリームに基づいて実装され、非常に重要な役割を果たします。同時に、フローは理解するのが非常に難しい概念でもあります。これは主に、関連ドキュメントが不足していることが原因です。NodeJ の初心者にとって、この概念を真に習得する前に、フローを理解するのに多くの時間がかかります。幸いなことに、ほとんどの NodeJ では、ユーザーにとっては Web アプリケーションの開発にのみ使用され、ストリームの理解が不十分でも使用には影響しません。ただし、ストリームを理解すると、NodeJ の他のモジュールをより深く理解できるようになり、場合によっては、ストリームを使用してデータを処理した方が良い結果が得られることがあります。

Stream

Stream は、Node.js でストリーミング データを処理するための抽象インターフェイスです。ストリームは実際のインターフェイスではなく、すべてのストリームの総称です。実際のインターフェイスは、ReadableStream、WritableStream、および ReadWriteStream です。

interface ReadableStream extends EventEmitter {
    readable: boolean;
    read(size?: number): string | Buffer;
    setEncoding(encoding: BufferEncoding): this;
    pause(): this;
    resume(): this;
    isPaused(): boolean;
    pipe<T extends WritableStream>(destination: T, options?: { end?: boolean | undefined; }): T;
    unpipe(destination?: WritableStream): this;
    unshift(chunk: string | Uint8Array, encoding?: BufferEncoding): void;
    wrap(oldStream: ReadableStream): this;
    [Symbol.asyncIterator](): AsyncIterableIterator<string | Buffer>;
}

interface WritableStream extends EventEmitter {
    writable: boolean;
    write(buffer: Uint8Array | string, cb?: (err?: Error | null) => void): boolean;
    write(str: string, encoding?: BufferEncoding, cb?: (err?: Error | null) => void): boolean;
    end(cb?: () => void): this;
    end(data: string | Uint8Array, cb?: () => void): this;
    end(str: string, encoding?: BufferEncoding, cb?: () => void): this;
}

interface ReadWriteStream extends ReadableStream, WritableStream { }
ログイン後にコピー

ReadableStream と WritableStream はどちらも EventEmitter クラスを継承するインターフェイスであることがわかります (ts のインターフェイスは型をマージするだけであるため、クラスを継承できます)。

上記のインターフェイスに対応する実装クラスは、それぞれ Readable、Writable、Duplex です。

NodeJ には 4 種類のストリームがあります。

  • Readable 読み取り可能ストリーム (ReadableStream を実装)

  • Writable 書き込み可能ストリーム (WritableStream を実装)

  • 二重読み取り可能および書き込み可能ストリーム (Implement を継承) Readable 後の WritableStream)

  • 変換変換ストリーム (Duplex の継承)

これらはすべて実装するメソッドがあります:

  • Readable はコンテンツを返すために _read メソッドを実装する必要があります

  • Writable はコンテンツを受け入れるために _write メソッドを実装する必要があります

  • #Duplex はコンテンツを受け入れて返すために _read メソッドと _write メソッドを実装する必要があります

  • Transform は受信したコンテンツを変換して返すために _transform メソッドを実装する必要があります

Readable

Readable ストリーム (Readable) はストリームの一種で、2 つのモードと 3 つの状態があります

2 つの読み取りモード:

  • フロー モード: データは基盤となるシステムからバッファーに読み取られ、バッファーに書き込まれます。バッファーがいっぱいになると、データは EventEmitter

  • # を通じてできるだけ早く登録されたイベント ハンドラーに自動的に渡されます。 ##一時停止モード: このモードでは、EventEmitter はデータ送信をアクティブにトリガーしません。バッファからデータを読み取るには、
  • Readable.read()

    メソッドを明示的に呼び出す必要があります。読み取りにより、EventEmitter への応答がトリガーされますイベント。

  • 3 つの状態:

    readableFlowing === null (初期状態)
  • readableFlowing = == false (一時停止モード)
  • readableFlowing === true (フロー モード)
  • 初期フロー
readable.readableFlowing

null のデータ イベントを追加すると true になります。

pause()

unpipe() が呼び出されるか、バック プレッシャーが受信されるか、readable イベントが追加されると、readableFlowing が実行されます。 false に設定します。この状態では、リスナーをデータ イベントにバインドしても、readableFlowing が true に切り替わりません。

resume()

を呼び出して、読み取り可能なストリームの readableFlowing を true に切り替えます

すべての読み取り可能なイベントを削除して、readableFlowing を有効にしますnullになる方法。

#イベント名説明##読めるバッファーに新しい読み取り可能なデータがあるときにトリガーされます (ノードがキャッシュ プールに挿入されるたびにトリガーされます) その後毎回トリガーされますデータの消費 トリガーされる、パラメータは今回消費されるデータですストリームが閉じられたときにトリガーされますストリームでエラーが発生したときにトリガーメソッド名
data
close
error
説明read(size)size の長さのデータを消費します。現在のデータが size 未満であることを示すには null を返します。それ以外の場合は、データを返します。今回消費した。サイズが渡されない場合、キャッシュ プール内のすべてのデータが消費されることを意味します
const fs = require(&#39;fs&#39;);

const readStreams = fs.createReadStream(&#39;./EventEmitter.js&#39;, {
    highWaterMark: 100// 缓存池浮标值
})

readStreams.on(&#39;readable&#39;, () => {
    console.log(&#39;缓冲区满了&#39;)
    readStreams.read()// 消费缓存池的所有数据,返回结果并且触发data事件
})


readStreams.on(&#39;data&#39;, (data) => {
    console.log(&#39;data&#39;)
})
ログイン後にコピー

https://github1s.com/nodejs/node/blob/v16.14.0/lib/internal/streams/readable.js#L527

当 size 为 0 会触发 readable 事件。

当缓存池中的数据长度达到浮标值 highWaterMark 后,就不会在主动请求生产数据,而是等待数据被消费后在生产数据

暂停状态的流如果不调用 read 来消费数据时,后续也不会在触发 datareadable,当调用 read 消费时会先判断本次消费后剩余的数据长度是否低于 浮标值,如果低于 浮标值 就会在消费前请求生产数据。这样在 read 后的逻辑执行完成后新的数据大概率也已经生产完成,然后再次触发 readable,这种提前生产下一次消费的数据存放在缓存池的机制也是缓存流为什么快的原因

流动状态下的流有两种情况

  • 生产速度慢于消费速度时:这种情况下每一个生产数据后一般缓存池中都不会有剩余数据,直接将本次生产的数据传递给 data 事件即可(因为没有进入缓存池,所以也不用调用 read 来消费),然后立即开始生产新数据,待上一次数据消费完后新数据才生产好,再次触发 data ,一只到流结束。
  • 生产速度快于消费速度时:此时每一次生产完数据后一般缓存池都还存在未消费的数据,这种情况一般会在消费数据时开始生产下一次消费的数据,待旧数据消费完后新数据已经生产完并且放入缓存池

他们的区别仅仅在于数据生产后缓存池是否还存在数据,如果存在数据则将生产的数据 push 到缓存池等待消费,如果不存在则直接将数据交给 data 而不加入缓存池。

值得注意的是当一个缓存池中存在数据的流从暂停模式进入的流动模式时,会先循环调用 read 来消费数据只到返回 null

暂停模式

ノードにはいくつかの種類のストリームがあります

暂停模式下,一个可读流读创建时,模式是暂停模式,创建后会自动调用 _read 方法,把数据从数据源 push 到缓冲池中,直到缓冲池中的数据达到了浮标值。每当数据到达浮标值时,可读流会触发一个 " readable " 事件,告诉消费者有数据已经准备好了,可以继续消费。

一般来说, &#39;readable&#39; 事件表明流有新的动态:要么有新的数据,要么到达流的尽头。所以,数据源的数据被读完前,也会触发一次 &#39;readable&#39; 事件;

消费者 " readable " 事件的处理函数中,通过 stream.read(size) 主动消费缓冲池中的数据。

const { Readable } = require(&#39;stream&#39;)

let count = 1000
const myReadable = new Readable({
    highWaterMark: 300,
    // 参数的 read 方法会作为流的 _read 方法,用于获取源数据
    read(size) {
        // 假设我们的源数据上 1000 个1
        let chunk = null
        // 读取数据的过程一般是异步的,例如IO操作
        setTimeout(() => {
            if (count > 0) {
                let chunkLength = Math.min(count, size)
                chunk = &#39;1&#39;.repeat(chunkLength)
                count -= chunkLength
            }
            this.push(chunk)
        }, 500)
    }
})
// 每一次成功 push 数据到缓存池后都会触发 readable
myReadable.on(&#39;readable&#39;, () => {
    const chunk = myReadable.read()//消费当前缓存池中所有数据
    console.log(chunk.toString())
})
ログイン後にコピー

值得注意的是, 如果 read(size) 的 size 大于浮标值,会重新计算新的浮标值,新浮标值是size的下一个二次幂(size <= 2^n,n取最小值)

//  hwm 不会大于 1GB.
const MAX_HWM = 0x40000000;
function computeNewHighWaterMark(n) {
  if (n >= MAX_HWM) {
    // 1GB限制
    n = MAX_HWM;
  } else {
    //取下一个2最高幂,以防止过度增加hwm
    n--;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    n++;
  }
  return n;
}
ログイン後にコピー

流动模式

ノードにはいくつかの種類のストリームがあります

所有可读流开始的时候都是暂停模式,可以通过以下方法可以切换至流动模式:

  • 添加 " data " 事件句柄;
  • 调用 “ resume ”方法;
  • 使用 " pipe " 方法把数据发送到可写流

流动模式下,缓冲池里面的数据会自动输出到消费端进行消费,同时,每次输出数据后,会自动回调 _read 方法,把数据源的数据放到缓冲池中,如果此时缓存池中不存在数据则会直接吧数据传递给 data 事件,不会经过缓存池;直到流动模式切换至其他暂停模式,或者数据源的数据被读取完了( push(null) );

可读流可以通过以下方式切换回暂停模式:

  • 如果没有管道目标,则调用 stream.pause()
  • 如果有管道目标,则移除所有管道目标。调用 stream.unpipe() 可以移除多个管道目标。
const { Readable } = require(&#39;stream&#39;)

let count = 1000
const myReadable = new Readable({
    highWaterMark: 300,
    read(size) {
        let chunk = null
        setTimeout(() => {
            if (count > 0) {
                let chunkLength = Math.min(count, size)
                chunk = &#39;1&#39;.repeat(chunkLength)
                count -= chunkLength
            }
            this.push(chunk)
        }, 500)
    }
})

myReadable.on(&#39;data&#39;, data => {
    console.log(data.toString())
})
ログイン後にコピー

Writable

相对可读流来说,可写流要简单一些。

ノードにはいくつかの種類のストリームがあります

当生产者调用 write(chunk) 时,内部会根据一些状态(corked,writing等)选择是否缓存到缓冲队列中或者调用 _write,每次写完数据后,会尝试清空缓存队列中的数据。如果缓冲队列中的数据大小超出了浮标值(highWaterMark),消费者调用 write(chunk) 后会返回 false,这时候生产者应该停止继续写入。

那么什么时候可以继续写入呢?当缓冲中的数据都被成功 _write 之后,清空了缓冲队列后会触发 drain 事件,这时候生产者可以继续写入数据。

当生产者需要结束写入数据时,需要调用 stream.end 方法通知可写流结束。

const { Writable, Duplex } = require(&#39;stream&#39;)
let fileContent = &#39;&#39;
const myWritable = new Writable({
    highWaterMark: 10,
    write(chunk, encoding, callback) {// 会作为_write方法
        setTimeout(()=>{
            fileContent += chunk
            callback()// 写入结束后调用
        }, 500)
    }
})

myWritable.on(&#39;close&#39;, ()=>{
    console.log(&#39;close&#39;, fileContent)
})
myWritable.write(&#39;123123&#39;)// true
myWritable.write(&#39;123123&#39;)// false
myWritable.end()
ログイン後にコピー

注意,在缓存池中数据到达浮标值后,此时缓存池中可能存在多个节点,在清空缓存池的过程中(循环调用_read),并不会向可读流一样尽量一次消费长度为浮标值的数据,而是每次消费一个缓冲区节点,即使这个缓冲区长度于浮标值不一致也是如此

const { Writable } = require(&#39;stream&#39;)


let fileContent = &#39;&#39;
const myWritable = new Writable({
    highWaterMark: 10,
    write(chunk, encoding, callback) {
        setTimeout(()=>{
            fileContent += chunk
            console.log(&#39;消费&#39;, chunk.toString())
            callback()// 写入结束后调用
        }, 100)
    }
})

myWritable.on(&#39;close&#39;, ()=>{
    console.log(&#39;close&#39;, fileContent)
})

let count = 0
function productionData(){
    let flag = true
    while (count <= 20 && flag){
        flag = myWritable.write(count.toString())
        count++
    }
    if(count > 20){
        myWritable.end()
    }
}
productionData()
myWritable.on(&#39;drain&#39;, productionData)
ログイン後にコピー

上述是一个浮标值为 10 的可写流,现在数据源是一个 0——20 到连续的数字字符串,productionData 用于写入数据。

  • 首先第一次调用 myWritable.write("0") 时,因为缓存池不存在数据,所以 "0" 不进入缓存池,而是直接交给 _wirtemyWritable.write("0") 返回值为 true

  • 当执行 myWritable.write("1") 时,因为 _wirtecallback 还未调用,表明上一次数据还未写入完,位置保证数据写入的有序性,只能创建一个缓冲区将 "1" 加入缓存池中。后面 2-9 都是如此

  • 当执行 myWritable.write("10") 时,此时缓冲区长度为 9(1-9),还未到达浮标值, "10" 继续作为一个缓冲区加入缓存池中,此时缓存池长度变为 11,所以 myWritable.write("1") 返回 false,这意味着缓冲区的数据已经足够,我们需要等待 drain 事件通知时再生产数据。

  • 100ms过后,_write("0", encoding, callback)callback 被调用,表明 "0" 已经写入完成。然后会检查缓存池中是否存在数据,如果存在则会先调用 _read 消费缓存池的头节点("1"),然后继续重复这个过程直到缓存池为空后触发 drain 事件,再次执行 productionData

  • 调用 myWritable.write("11"),触发第1步开始的过程,直到流结束。

Duplex

在理解了可读流与可写流后,双工流就好理解了,双工流事实上是继承了可读流然后实现了可写流(源码是这么写的,但是应该说是同时实现了可读流和可写流更加好)。

ノードにはいくつかの種類のストリームがあります

Duplex 流需要同时实现下面两个方法

  • 实现 _read() 方法,为可读流生产数据

  • 实现 _write() 方法,为可写流消费数据

上面两个方法如何实现在上面可写流可读流的部分已经介绍过了,这里需要注意的是,双工流是存在两个独立的缓存池分别提供给两个流,他们的数据源也不一样

以 NodeJs 的标准输入输出流为例:

  • 当我们在控制台输入数据时会触发其 data 事件,这证明他有可读流的功能,每一次用户键入回车相当于调用可读的 push 方法推送生产的数据。
  • 当我们调用其 write 方法时也可以向控制台输出内容,但是不会触发 data 事件,这说明他有可写流的功能,而且有独立的缓冲区,_write 方法的实现内容就是让控制台展示文字。
// 每当用户在控制台输入数据(_read),就会触发data事件,这是可读流的特性
process.stdin.on(&#39;data&#39;, data=>{
    process.stdin.write(data);
})

// 每隔一秒向标准输入流生产数据(这是可写流的特性,会直接输出到控制台上),不会触发data
setInterval(()=>{
    process.stdin.write(&#39;不是用户控制台输入的数据&#39;)
}, 1000)
ログイン後にコピー

Transform

ノードにはいくつかの種類のストリームがあります

可以将 Duplex 流视为具有可写流的可读流。两者都是独立的,每个都有独立的内部缓冲区。读写事件独立发生。

                             Duplex Stream
                          ------------------|
                    Read  <-----               External Source
            You           ------------------|  
                    Write ----->               External Sink
                          ------------------|
ログイン後にコピー

Transform 流是双工的,其中读写以因果关系进行。双工流的端点通过某种转换链接。读取要求发生写入。

                                 Transform Stream
                           --------------|--------------
            You     Write  ---->                   ---->  Read  You
                           --------------|--------------
ログイン後にコピー

对于创建 Transform 流,最重要的是要实现 _transform 方法而不是 _write 或者 _read_transform 中对可写流写入的数据做处理(消费)然后为可读流生产数据。

转换流还经常会实现一个 `_flush` 方法,他会在流结束前被调用,一般用于对流的末尾追加一些东西,例如压缩文件时的一些压缩信息就是在这里加上的
ログイン後にコピー
const { write } = require(&#39;fs&#39;)
const { Transform, PassThrough } = require(&#39;stream&#39;)

const reurce = &#39;1312123213124341234213423428354816273513461891468186499126412&#39;

const transform = new Transform({
    highWaterMark: 10,
    transform(chunk ,encoding, callback){// 转换数据,调用push将转换结果加入缓存池
        this.push(chunk.toString().replace(&#39;1&#39;, &#39;@&#39;))
        callback()
    },
    flush(callback){// end触发前执行
        this.push(&#39;<<<&#39;)
        callback()
    }
})


// write 不断写入数据
let count = 0
transform.write(&#39;>>>&#39;)
function productionData() {
    let flag = true
    while (count <= 20 && flag) {
        flag = transform.write(count.toString())
        count++
    }
    if (count > 20) {
        transform.end()
    }
}
productionData()
transform.on(&#39;drain&#39;, productionData)


let result = &#39;&#39;
transform.on(&#39;data&#39;, data=>{
    result += data.toString()
})
transform.on(&#39;end&#39;, ()=>{
    console.log(result)
    // >>>0@23456789@0@1@2@3@4@5@6@7@8@920<<<
})
ログイン後にコピー

更多node相关知识,请访问:nodejs 教程

以上がノードにはいくつかの種類のストリームがありますの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

関連ラベル:
ソース:php.cn
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
最新の問題
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート