「學習筆記」更好的瞭解Node.js中的緩存區,流

什麼是二進制數據?

計算機是以二進制形式存儲和表示數據,二進制是 01 的集合。例如:01001010。好比,要存儲數字 13 計算機須要將數字轉換爲 1101html

二進制中的 01 被稱爲位(bit)。儘管它們很像表示一個數值,可是其實它們表示是符號。0 表明FALSE, 1表明TRUE。位的運算實際上是對真假值的操做。爲了存儲數據,計算機包含了大量的電路,每個電路能存儲單獨的一個。這種位存儲器,被稱爲"主存儲器"。計算機經過存儲單元組織管理主存儲器。一個典型的存儲單元容量是8位,一個8位的串稱爲一個字節(byte)。node

可是,數字不是咱們惟一須要存儲處理的數據,咱們還須要處理字符串,圖片,視頻。shell

以字符串爲例,那麼計算機怎麼存儲字符串呢?例如,咱們要存儲字符串 "S" ,計算機首先會將 "S", 轉換爲數字'S'.charCodeAt() === 83, 那麼計算機是如何知道83表示"S"的?json

什麼是字符集?

字符集是已經定義好的規則,每個字符都有一個確切的數字表示。字符集有不一樣規則的定義,例如:"Unicode", "ASCII"。瀏覽器中使用"Unicode"字符集。正是"Unicode"字符集,定義了83表示"S"。api

那麼接下來,計算機會直接將83轉爲二進制嗎?並非,咱們還須要使用"字符編碼"。數組

什麼是字符編碼?

字符集定義了使用特定的數字表示字符(漢字也是一樣的)。而字符編碼定義了,如何將數字轉換爲特定長度的二進制數據。常見的utf-8字符編碼,規定了字符最多由4個字節進行編碼(一個字節由8個,0或者1表示)。瀏覽器

h e l l o <==Unicode==> 104 101 108 108 111 <==utf-8==> 1101000 1100101 1101100 1101100 1101111 
複製代碼

對於視頻,音頻,圖片,計算機也有特定規則,將其轉換爲二進制數據。計算機將全部數據類型,存儲爲二進制文件。這些二進制文件就是二進制數據。緩存

什麼是緩衝區?

數據流指數據從一個位置到另外一個位置的移動(一般大文件會被拆解爲塊的形式)。安全

若是數據流動的速度,大於進程處理數據的速度,多餘的數據會在某個地方等待。若是數據流動的速度,小於進程處理數據的速度,那麼數據會在某個地方累計到必定的數量,而後在由進程進行處理。(咱們沒法控制流的速度)post

那個等待數據,累計數據,而後發生出去的地方。就是緩衝區。緩衝區一般位於電腦的RAM(內存)中。

咱們能夠把緩衝區想象成一個公交車站,較早到達車站的乘客的會等待公交車,當公交車以及裝滿離開後,剩下的乘客會等待下一班的公交車到來。那個等待公交車的地方,就是緩衝區。

舉一個常見的緩衝區的例子,咱們在觀看在線視頻的時候,若是你的網速很快,緩衝區老是會被當即填充,而後發送出去,而後當即緩衝下一段視頻。觀看的過程當中,不會有卡頓。若是網速很慢,則會看到loading,表示緩衝區正在被填充,當填充完成後數據被髮送出去,才能看到這段視頻。

Buffer類

什麼是TypedArray?

TypedArray出現前,js沒有讀取或操做二進制數據流的機制。TypedArray並非一個特定的全局對象,而是許多全局對象的統稱。

// TypedArray 指的是下面其中之一
Int8Array // 8位二進制補碼有符號整數的數組
Uint8Array // 8位無符號整型數組(0 > & < 256(8位無符號整形)),等等
Uint8ClampedArray
Int16Array
Uint16Array
Int32Array
Uint32Array
Float32Array
Float64Array
複製代碼

什麼是Buffer類?

Buffer類,是以Nodejs方式實現的Uint8ArrayAPI(Uint8Array屬於TypedArray的一種)。用於與八字節的二進制數據進行交互。

常見的API

Buffer.from

Buffer.from, 接受多種形式的參數,下面介紹幾種常見的

使用8字節的數組,做爲參數

const buf = Buffer.from([0b1101000, 0b1100101, 0b1101100, 0b1101100, 0b1101111])
// hello
console.log(buf.toString())
複製代碼

使用字符串做爲參數

const buf = Buffer.from('Hello World!');

// HelloWorld
console.log(buf.toString())
複製代碼

Buffer.alloc

建立一個指定大小,而且已經初始化的Buffer(默認被0填充)

// 建立一個大小爲10個字節的Buffer,並使用0進行填充
const buf = Buffer.alloc(10)
// <Buffer 00 00 00 00 00 00 00 00 00 00>
console.log(buf)

// 建立一個大小爲12個字節的Buffer,並使用漢字進行填充
const buf = Buffer.alloc(12, '大')
// 打印出 大大大大,由於漢字是3個字節的關係,因此填充了4個漢字
console.log(buf.toString())
複製代碼

Buffer.allocUnsafe

建立一個指定大小,可是沒有初始化填充的Buffer

// 建立一個大小爲10個字節的Buffer,沒有被初始化
const buf = Buffer.allocUnsafe(10)
複製代碼

爲何說 Buffer.allocUnsafe 是不安全的?Buffer是內存的抽象,嘗試運行console.log(Buffer.allocUnsafe(10000).toString()), 咱們應該能夠從控制檯看到打印出了內存裏的一些東西

buffer.write

向Buffer中寫入字符串,若是Buffer空間不夠,多餘的字符串不會被寫入

const buf = Buffer.alloc(5)
// 您好的長度是6個字節
buf.write('您好')
// 您
console.log(buf.toString())
複製代碼

buffer.toJSON

將buffer中的數據轉換爲Unicode碼

const buf = Buffer.from('hello')

// {
// type: 'Buffer',
// data: [ 104, 101, 108, 108, 111 ] h e l l o 的 Unicode碼
// }
console.log(buf.toJSON())
複製代碼

buffer.toString

將buffer解碼成字符串

const buf = Buffer.from([0b1101000, 0b1100101, 0b1101100, 0b1101100, 0b1101111 ])
// hello
console.log(buf.toString())
複製代碼

String Decoder

考慮下面這種狀況,由於兩個漢字的字節長度是6,因此字節長度等於5的buffer是放不下的,因此打印出來的字符串是不完整的。

const buf = Buffer.alloc(5, '您好')
// 您�
console.log(buf.toString())
複製代碼

那麼有什麼辦法能夠將Buffer中不完整的字符串輸出出來呢?咱們可使用String Decoder

const { StringDecoder } = require('string_decoder')
const decoder = new StringDecoder('utf8')

// Buffer.from('好') <Buffer e5 a5 bd>
const str1 = decoder.write(Buffer.alloc(5, '您好'))
// 您
console.log(str1)
const str2 = decoder.end(Buffer.from([0xbd]))
// 好
console.log(str2)
複製代碼

StringDecoder的實例接受寫入Buffer的實例,使用內部緩衝區確保解碼的字符串不包含不完成的字節,而且將不完整的字節,保存起來,直到下一次使用write或者end。

decoder.write

返回已解碼的字符串,字符串不包含不完整的字節,不完整的字節。不完整的字節會保存到decoder內部的緩衝區中。

const { StringDecoder } = require('string_decoder')
const decoder = new StringDecoder('utf8')

// 哈嘍 <Buffer e5 93 88 e5 96 bd>
const str = decoder.write(Buffer.from([0xe5, 0x93, 0x88, 0xe5, 0x96]))
// 哈,0xe5, 0x96因爲不完整不會被返回,而是保存在decoder的內部緩衝區
console.log(str)
複製代碼

decoder.end

會將decoder內部緩存區剩餘的buffer一次性返回。

const { StringDecoder } = require('string_decoder')
const decoder = new StringDecoder('utf8')

// 哈嘍 <Buffer e5 93 88 e5 96 bd>
decoder.write(Buffer.from([0xe5, 0x93, 0x88, 0xe5, 0x96]))
const str = decoder.end()
// �,decoder內部緩衝區剩餘的字節是不完整的
console.log(str)
複製代碼

什麼是流?

流是數據的集合,流不像字符串或者數組同樣是當即可用的,流不會所有存在內存中。處理大量數據時,流很是有用。

Nodejs中,許多模塊都實現了流模式。下圖是實現了流模式的內置模塊(圖片來自於Samer Buna的在線課程)

image

流的類型

  1. Writable,可寫入流,是寫入目標的抽象,常見的例子:fs.createWriteStream
  2. Readable,可讀取流,是數據源的抽象。常見的例子:fs.createReadStream
  3. Duplex,可讀可寫流(雙工流)
  4. Transform,也是一種可讀可寫流,可是能夠讀取寫入的時候修改轉換數據。因此也能夠叫作轉換流。例如:zlib.createGzip壓縮數據流

流的管道

const fs = require('fs')
// 可讀流做爲數據源
const readable = fs.createReadStream('./數據源.json')
// 可寫流做爲目標
const writable = fs.createWriteStream('./目標.json')
// 將數據源經過管道鏈接到目標
readable.pipe(writable)
複製代碼

在這幾行簡單的代碼中咱們將可讀流的輸出(readable做爲數據源),鏈接管道至可寫流的輸入(writable做爲目標)。源必須是可讀流,目標必須是可寫流。

const fs = require('fs')
const zlib = require('zlib')

const readable = fs.createReadStream('./數據源.json')
// gzip是一個雙工流
const gzip = zlib.createGzip()
const writable = fs.createWriteStream('./目標.gz')

// 數據源鏈接到轉換流(gzip),轉換流處理數據後,鏈接到目標上
readable
    .pipe(gzip)
    .pipe(writable)
複製代碼

咱們也能夠將可讀流的管道鏈接到雙工流(轉換流)上。總結一下pipe方法的用法。pipe能夠返回一個目標流,目標流能夠鏈接到雙工流,可寫流上。

可讀流
    .pipe(雙工流)
    .pipe(雙工流)
    .pipe(可寫流)
複製代碼

使用pipe是消費流最簡單的方法,它會自動管理一些操做,好比錯誤處理,好比若是可讀流沒有數據可供消費時的狀況。固然咱們也能夠經過事件消費流,可是最好避免二者混合使用。

流的事件

若是須要對流實現,更自定義的控制,可使用事件消費流。下面的這段代碼和以前的pipe的代碼是等效的。

const fs = require('fs')

const readable = fs.createReadStream('./數據源.json')
const writable = fs.createWriteStream('./目標.json')

// 當可讀流綁定data事件時,會將流切換到流動模式
readable.on('data', (chunk) => {
    writable.write(chunk);
})

readable.on('end', () => {
    writable.end()
})
複製代碼

下圖是可讀流,可寫流的事件與方法的列表(圖片來自於Samer Buna的在線課程)

事件列表.png

關於上面的例子中存在的一些問題

上面對於流事件的示例中,是存在隱患的。具體的問題緣由,能夠查看個人這篇文章簡單理解 backpressure(背壓)機制

// 其實這段代碼是其實有問題的
readable.on('data', (chunk) => {
    writable.write(chunk);
})
複製代碼

流的可讀流的暫停和流動模式

默認狀況下,可讀流是處於暫停狀態的,可是它們能夠被切換到流動模式,並在須要時切換回暫停模式。有時,模式會被自動發生切換。

在暫停模式時,咱們可使用read方法從流中讀取數據。

const fs = require('fs')

const readable = fs.createReadStream('./數據源.json')
const writable = fs.createWriteStream('./目標.json')
// 當可讀流是能夠被讀取時或者會發生變化時或者到達流的盡頭時。readable能夠被觸發
readable.on('readable', () => {
    let chunk
    while (chunk = readable.read(1)) {
        writable.write(chunk)
    }
})
複製代碼

在流動模式時,數據會持續流動,咱們必須添加事件並消費它。若是不能處理流動的數據,數據是會丟失的。咱們能夠添加data事件處理數據。添加data事件,會自動將可讀流的模式,由暫停模式切換到流動模式。

若是須要實現兩種模式的手動切換可使用resume()(從暫停模式中恢復)和pause()(進入暫停模式)方法。(若是監聽了可讀流的readable的事件,resume()方法無效)

下面的例子中,每一次經過可寫流寫入一點數據,都會暫停一秒可讀流1s,1s後繼續寫入。

const fs = require('fs')

const readable = fs.createReadStream('./數據源.json')
const writable = fs.createWriteStream('./目標.json')

// 自動切換到流動模式
readable.on('data', (chunk) => {
    console.log('寫入')
    writable.write(chunk)
    readable.pause()
    console.log('暫停')
    // 暫停1s後,從新切換到流動模式
    setTimeout(() => {
        readable.resume()
    }, 1000)
})
複製代碼

下圖是兩種模式以前的切換(圖片來自於Samer Buna的在線課程)。添加data事件時(單獨添加data事件,不存在readable事件時),模式會自動切換。

暫停和流動模式的互相轉換

流的實現

建立自定義可寫流

建立一個自定義的可寫流,咱們須要繼承 stream.Writable 類。並在子類中實現 _write 方法。

const { Writable } = require('stream')

class CustomWritable extends Writable {
    /** * @param chunk 須要寫入的數據 * @param encoding 編碼格式 * @param next 處理完成的回調 */
    _write(chunk, encoding, next) {
        try {
            // 僅僅是作了一個打印
            console.log(`僅僅作了一個打印:${chunk.toString()}`)
            next()
        } catch (error) {
            next(error)
        }
    }
}
複製代碼

這個流自己沒有多大的意義。這個自定義可寫流,只會對可讀流輸入的數據,進行一個打印。咱們能夠連接到 process.stdin 可讀流上,對終端的輸入,進行一個打印。

const customWritable = new CustomWritable()

process.stdin.pipe(customWritable)
複製代碼

建立自定義可讀流

建立一個可讀流,咱們須要繼承 stream.Readable 類。並在子類中實現 _read 方法。

const { Readable } = require('stream'); 

class CustomReadable extends Readable {
    _read () {
    }
}
複製代碼

這個流自己也沒有很大的意義。咱們可使用 push 方法,將數據添加到流的內部隊列中,以供流進行消費。咱們能夠結合前一個自定義的可寫流,將 push 的數據打印出來。

const customReadable = new CustomReadable()
const customWritable = new CustomWritable()

customReadable.push('我喜歡西爾莎羅南')
// 通知流不會有任何數據了
customReadable.push(null)

// 可讀流將數據傳遞給可寫流
// 可寫流將數據打印出來
customReadable.pipe(customWritable)
複製代碼

建立自定義轉換流

建立一個可讀流,咱們須要繼承 stream.Transform 類。並在子類中實現 _transform 方法。

const { Transform } = require('stream')

class CustomTransform extends Transform {
    _transform (chunk, encoding, next) {
        // 咱們把可讀流的內容,所有轉換爲大寫
        chunk = chunk.toString().toUpperCase()
        next(null, chunk)
    }
}
複製代碼

這個自定義流,會將可讀流傳過來的字符串,所有轉換爲大寫。

const customReadable = new CustomReadable()
const customWritable = new CustomWritable()
const customTransform = new CustomTransform()

customReadable.push('abcdefg')
customReadable.push(null)

customReadable
    .pipe(customTransform)
    .pipe(customWritable)
複製代碼

對象模式

Node中的流,默認都是使用Buffer 或者 字符串進行傳輸,咱們能夠開啓 objectMode 開關。使流能夠傳輸js的對象。

const { Readable, Writable } = require('stream')

class CustomReadable extends Readable {
    _read () {
    }
}

class CustomWritable extends Writable {
    _write(chunk, _, next) {
        try {
            // 這裏能夠打印js對象
            // chunk可使js對象
            console.log(chunk)
            next()
        } catch (error) {
            next(error)
        }
    }
}

const customReadable = new CustomReadable({
    objectMode: true // 開啓對象模式
})
const customWritable = new CustomWritable({
    objectMode: true // 開啓對象模式
})

// 咱們能夠傳輸對象了
customReadable.push(['a', 'b', 'c', 'd'])
customReadable.push(null)

customReadable.pipe(customWritable)
複製代碼

參考

相關文章
相關標籤/搜索