Node中Buffer 經常使用API解讀

Buffer 概述

在 ES6 引入 TypedArray 以前,JavaScript 語言沒有讀取或操做二進制數據流的機制。 Buffer 類被引入做爲 NodeJS API 的一部分,使其能夠在 TCP 流或文件系統操做等場景中處理二進制數據流。
Buffer 屬於 Global 對象,使用時不需引入,且 Buffer 的大小在建立時肯定,沒法調整。npm

建立 Buffer

在 NodeJS v6.0.0 版本以前,Buffer 實例是經過 Buffer 構造函數建立的,即便用 new 關鍵字建立,它根據提供的參數返回不一樣的 Buffer,但在以後的版本中這種聲明方式就被廢棄了,替代 new 的建立方式主要有如下幾種。數組

一、Buffer.alloc 和 Buffer.allocUnsafe

Buffer.allocBuffer.allocUnsafe 建立 Buffer 的傳參方式相同,參數爲建立 Buffer 的長度,數值類型。緩存

Buffer.alloc 和 Buffer.allocUnsafe 建立 Buffer
1
2
3
4
5
6
7
8
複製代碼
// Buffer.alloc 建立 Buffer
let buf1 = Buffer.alloc(6);

// Buffer.allocUnsafe 建立 Buffer
let buf2 = Buffer.allocUnsafe(6);

console.log(buf1); // <Buffer 00 00 00 00 00 00>
console.log(buf2); // <Buffer 00 e7 8f a0 00 00>
複製代碼

經過代碼能夠看出,用 Buffer.allocBuffer.allocUnsafe 建立 Buffer 是有區別的,Buffer.alloc建立的 Buffer 是被初始化過的,即 Buffer 的每一項都用 00 填充,而 Buffer.allocUnsafe 建立的 Buffer 並無通過初始化,在內存中只要有閒置的 Buffer 就直接 「抓過來」 使用。安全

Buffer.allocUnsafe 建立 Buffer 使得內存的分配很是快,但已分配的內存段可能包含潛在的敏感數據,有明顯性能優點的同時又是不安全的,因此使用需格外 「當心」。bash

二、Buffer.from

Buffer.from 支持三種傳參方式:編輯器

  • 第一個參數爲字符串,第二個參數爲字符編碼,如 ASCIIUTF-8Base64 等等。
  • 傳入一個數組,數組的每一項會以十六進制存儲爲 Buffer 的每一項。
  • 傳入一個 Buffer,會將 Buffer 的每一項做爲新返回 Buffer 的每一項。

傳入字符串和字符編碼:函數

傳入字符串和字符編碼
1
2
3
複製代碼
let buf = Buffer.from("hello", "utf8");

console.log(buf); // <Buffer 68 65 6c 6c 6f>
複製代碼

傳入數組:性能

數組成員爲十進制數
1
2
3
複製代碼
let buf = Buffer.from([1, 2, 3]);

console.log(buf); // <Buffer 01 02 03>
複製代碼
數組成員爲十六進制數
1
2
3
4
複製代碼
let buf = Buffer.from([0xe4, 0xbd, 0xa0, 0xe5, 0xa5, 0xbd]);

console.log(buf); // <Buffer e4 bd a0 e5 a5 bd>
console.log(buf.toString("utf8")); // 你好
複製代碼

在 NodeJS 中不支持 GB2312 編碼,默認支持 UTF-8,在 GB2312 中,一個漢字佔兩個字節,而在 UTF-8 中,一個漢字佔三個字節,因此上面 「你好」 的 Buffer 爲 6 個十六進制數組成。ui

數組成員爲字符串類型的數字
1
2
3
複製代碼
let buf = Buffer.from(["1", "2", "3"]);

console.log(buf); // <Buffer 01 02 03>
複製代碼

傳入的數組成員能夠是任何進制的數值,當成員爲字符串的時候,若是值是數字會被自動識別成數值類型,若是值不是數字或成員爲是其餘非數值類型的數據,該成員會被初始化爲 00this

建立的 Buffer 能夠經過 toString 方法直接指定編碼進行轉換,默認編碼爲 UTF-8

傳入 Buffer:

傳入一個 Buffer
1
2
3
4
5
6
7
8
複製代碼
let buf1 = Buffer.from("hello", "utf8");

let buf2 = Buffer.from(buf1);

console.log(buf1); // <Buffer 68 65 6c 6c 6f>
console.log(buf2); // <Buffer 68 65 6c 6c 6f>
console.log(buf1 === buf2); // true
console.log(buf1[0] === buf2[0]); // false
複製代碼

當傳入的參數爲一個 Buffer 的時候,會建立一個新的 Buffer 並複製上面的每個成員。

Buffer 爲引用類型,一個 Buffer 複製了另外一個 Buffer 的成員,當其中一個 Buffer 複製的成員有更改,另外一個 Buffer 對應的成員會跟着改變,由於指向同一個引用,相似於 「二維數組」。

Buffer 類比二維數組
1
2
3
4
5
複製代碼
let arr1 = [1, 2, [3]];
let arr2 = arr1.slice();

arr2[2][0] = 5;
console.log(arr1); // [1, 2, [5]]
複製代碼

Buffer 的經常使用方法

一、fill

Buffer 的 fill 方法能夠向一個 Buffer 中填充數據,支持傳入三個參數:

  • value:將要填充的數據;
  • start:填充數據的開始位置,不指定默認爲 0
  • end:填充數據的結束位置,不指定默認爲 Buffer 的長度。
1
2
3
4
複製代碼
let buf = Buffer.alloc(3);

buf.fill(1);
console.log(buf); // <Buffer 01 01 01>
複製代碼
1
2
3
4
複製代碼
let buf = Buffer.alloc(6);

buf.fill(1, 2, 4);
console.log(buf); // <Buffer 00 00 01 01 00 00>
複製代碼

上面代碼能夠看出填充數據是 「包前不包後的」,fill 的第一個參數也支持是多個字節,從被填充 Buffer 的起始位置開始,一直到結束,會循環填充這些字節,剩餘的位置不夠填充這幾個字節,會填到哪算哪,有可能不完整,若是 fill 指定的結束位置大於了 Buffer 的長度,會拋出 RangeError 的異常。

1
2
3
4
複製代碼
let buf = Buffer.alloc(6);

buf.fill("abc", 1, 5);
console.log(buf); // <Buffer 00 61 62 63 61 00>
複製代碼
1
2
3
4
複製代碼
let buf = Buffer.alloc(3);

buf.fill("abc", 4, 8);
console.log(buf); // throw new errors.RangeError('ERR_INDEX_OUT_OF_RANGE');
複製代碼

二、slice

Buffer 的 slice 方法與數組的 slice 方法用法徹底相同,相信數組的 slice 已經足夠熟悉了,這裏就很少贅述了,Buffer 中截取出來的都是 Buffer。

1
2
3
4
5
6
7
8
9
複製代碼
let buf = Buffer.from("hello", "utf8");

let a = buf.slice(0, 2);
let b = buf.slice(2);
let b = buf.slice(-2);

console.log(a.toString()); // he
console.log(b.toString()); // llo
console.log(c.toString()); // o
複製代碼

三、indexOf

Buffer 的 indexOf 用法與數組和字符串的 indexOf 相似,第一個參數爲查找的項,第二個參數爲查找的起始位置,不一樣的是,對於 Buffer 而言,查找的多是一個字符串,表明多個字節,查找的字節在 Buffer 中必須有連續相同的字節,返回連續的字節中第一個字節的索引,沒查找到返回 -1

1
2
3
4
5
複製代碼
let buf = Buffer.from("你*好*嗎", "utf8");

console.log(buf); // <Buffer e4 bd a0 2a e5 a5 bd 2a e5 90 97>
console.log(buf.indexOf("*")); // 3
console.log(buf.indexOf("*", 4)); // 7
複製代碼

四、copy

Buffer 的 copy 方法用於將一個 Buffer 的字節複製到另外一個 Buffer 中去,有四個參數:

  • target:目標 Buffer
  • targetStart:目標 Buffer 的起始位置
  • sourceStart:源 Buffer 的起始位置
  • sourceEnd:源 Buffer 的結束位置
容器 Buffer 長度充足
1
2
3
4
5
6
7
複製代碼
let targetBuf = Buffer.alloc(6);
let sourceBuf = Buffer.from("你好", "utf8");

// 將 「你好」 複製到 targetBuf 中
sourceBuf.copy(targetBuf, 0, 0, 6);

console.log(targetBuf.toString()); // 你好
複製代碼
容器 Buffer 長度不足
1
2
3
4
5
複製代碼
let targetBuf = Buffer.alloc(3);
let sourceBuf = Buffer.from("你好", "utf8");

sourceBuf.copy(targetBuf, 0, 0, 6);
console.log(targetBuf.toString()); // 你
複製代碼

上面第二個案例中雖然要把整個源 Buffer 都複製進目標 Buffer 中,可是因爲目標 Buffer 的長度只有 3,因此最終只能複製進去一個 「你」 字。

Buffer 與數組不一樣,不能經過操做 length 和索引改變 Buffer 的長度,Buffer 一旦被建立,長度將保持不變。

數組對比 Buffer —— 操做 length
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
複製代碼
// 數組
let arr = [1, 2, 3];
arr[3] = 4;
console.log(arr); // [1, 2, 3, 4]

arr.length = 5;
console.log(arr); // [1, 2, 3, 4, empty]


// Buffer
let buf = Buffer.alloc(3);
buf[3] = 0x00;
console.log(buf); // <Buffer 00 00 00>

buf.length = 5;
console.log(buf); // <Buffer 00 00 00>
console.log(buf.length); // 3
複製代碼

經過上面代碼能夠看出數組能夠經過 length 和索引對數組的長度進行改變,可是 Buffer 中相似的操做都是不生效的。

copy 方法的 Polyfill:

模擬 copy 方法
1
2
3
4
5
複製代碼
Buffer.prototype.myCopy = function (target, targetStart, sourceStart, sourceEnd) {
    for(let i = 0; i < sourceEnd - sourceStart; i++) {
        target[targetStart + i] = this[sourceStart + i];
    }
}
複製代碼

五、Buffer.concat

與數組相似,Buffer 也存在用於拼接多個 Buffer 的方法 concat,不一樣的是 Buffer 中的 concat 不是實例方法,而是靜態方法,經過 Buffer.concat 調用,且傳入的參數不一樣。

Buffer.concat 有兩個參數,返回值是一個新的 Buffer:

  • 第一個參數爲一個數組,數組中的每個成員都是一個 Buffer;
  • 第二個參數表明新 Buffer 的長度,默認值爲數組中每一個 Buffer 長度的總和。

Buffer.concat 會將數組中的 Buffer 進行拼接,存入新 Buffer 並返回,若是傳入第二個參數規定了返回 Buffer 的長度,那麼返回值存儲拼接後前規定長度個字節。

1
2
3
4
5
6
7
8
9
10
11
複製代碼
let buf1 = Buffer.from("你", "utf8");
let buf2 = Buffer.from("好", "utf8");

let result1 = Buffer.concat([buf1, buf2]);
let result2 = Buffer.concat([buf1, buf2], 3);

console.log(result1); // <Buffer e4 bd a0 e5 a5 bd>
console.log(result1.toString()); // 你好

console.log(result2); // <Buffer e4 bd a0>
console.log(result2.toString()); // 你
複製代碼

Buffer.concat 方法的 Polyfill:

模擬 Buffer.concat
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
複製代碼
Buffer.myConcat = function (bufferList, len) {
    // 新 Buffer 的長度
    len = len || bufferList.reduce((prev, next) => prev + next.length, 0);

    let newBuf = Buffer.alloc(len); // 建立新 Buffer
    let index = 0; // 下次開始的索引

    // 循環存儲 Buffer 的數組進行復制
    bufferList.forEach(buf => {
        buf.myCopy(newBuf, index, 0, buf.length);
        index += buf.length;
    });

    return newBuf;
}
複製代碼

六、Buffer.isBuffer

Buffer.isBuffer 是用來判斷一個對象是不是一個 Buffer,返回布爾值。

1
2
3
4
5
複製代碼
let obj = {};
let buf = Buffer.alloc(6);

console.log(Buffer.isBuffer(obj)); // false
console.log(Buffer.isBuffer(buf)); // true
複製代碼

封裝一個 split

字符串中的 split 是常用的方法,能夠用分隔符將字符串切成幾部分存儲在數組中,Buffer 自己沒有 split 方法,可是也會有相似的使用場景,因此咱們在 Buffer 中本身封裝一個 split

Buffer 的 split 方法參數爲一個分隔符,這個分隔符多是一個或多個字節的內容,返回值爲一個數組,分隔開的部分做爲獨立的 Buffer 存儲在返回的數組中。

封裝 Buffer 的 split 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
複製代碼
Buffer.prototype.split = function (sep) {
    let len = Buffer.from(sep).length; // 分隔符所佔的字節數
    let result = []; // 返回的數組
    let start = 0; // 查找 Buffer 的起始位置
    let offset = 0; // 偏移量

    // 循環查找分隔符
    while ((offset = this.indexOf(sep, start)) !== -1) {
        // 將分隔符以前的部分截取出來存入
        result.push(this.slice(start, offset));
        start = offset + len;
    }

    // 處理剩下的部分
    return result.push(this.slice(start));
}
複製代碼

驗證 split 方法:

驗證 split
1
2
3
4
5
6
7
8
9
10
複製代碼
let buf = Buffer.from("哈登愛籃球愛夜店", "utf8");
let bufs = buf.split("愛");

console.log(bufs);
// [ <Buffer e5 93 88 e7 99 bb>,
//   <Buffer e7 af ae e7 90 83>,
//   <Buffer e5 a4 9c e5 ba 97> ]

newBufs = bufs.map(buf => buf.toString());
console.log(newBufs); // [ '哈登', '籃球', '夜店' ]
複製代碼

Buffer 的編碼轉換

咱們知道 NodeJS 中的默認編碼爲 UTF-8,且不支持 GB2312 編碼,假如如今有一個編碼格式爲 GB2312txt 文件,內容爲 「你好」,如今咱們使用 NodeJS 去讀取它,因爲在 UTF-8GB2312 編碼中漢字所佔字節數不一樣,因此讀出的內容沒法解析,即爲亂碼。

1
2
3
4
5
6
7
8
9
10
11
複製代碼
// 引入依賴
const fs = require("fs");
const path = require("path");

let buf = Buffer.from("你好", "utf8");
let result = fs.readFileSync(path.resolve(__dirname, "a.txt"));

console.log(buf); // <Buffer e4 bd a0 e5 a5 bd>
console.log(buf.toString()); // 你好
console.log(result); // <Buffer c4 e3 ba c3>
console.log(result.toString()); // ���
複製代碼

若是必定要在 NodeJS 中來正確解析這樣的內容,這樣的問題仍是有辦法解決的,咱們須要藉助 iconv-lite 模塊,這個模塊能夠將一個 Buffer 按照指定的編碼格式進行編碼或解碼。

因爲 iconv-lite 是第三方提供的模塊,在使用前須要安裝,安裝命令以下:

npm install iconv-lite

若是想正確的讀出其餘編碼格式文件的內容,上面代碼應該更改成:

1
2
3
4
5
6
7
8
複製代碼
// 引入依賴
const fs = require("fs");
const path = require("path");
const iconvLite = require("iconv-lite");

let result = fs.readFileSync(path.resolve(__dirname, "a.txt"));

console.log(iconvLite.decode(result, "gb2312")); // 你好
複製代碼

去掉 BOM 頭

上面讀取 GB2312 編碼的 txt 文件也能夠經過打開文件從新保存爲 UTF-8 或用編輯器直接將編碼手動修改成 UTF-8,此時讀取的文件不須要進行編碼轉換,可是會產生新的問題。

產生 BOM 頭
1
2
3
4
5
6
7
8
9
複製代碼
// 引入依賴
const fs = require("fs");
const path = require("path");

let buf = Buffer.from("你好", "utf8");
let result = fs.readFileSync(path.resolve(__dirname, "a.txt"));

console.log(buf); // <Buffer e4 bd a0 e5 a5 bd>
console.log(result); // <Buffer ef bb bf e4 bd a0 e5 a5 bd>
複製代碼

在手動修改 txt 文件編碼後執行上面代碼,發現讀取的 Buffer 與正常狀況相比前面多出了三個字節,只要存在文件編碼的修改就會在這個文件的前面產生多餘的字節,叫作 BOM 頭。

BOM 頭是用來判斷文本文件是哪種 Unicode 編碼的標記,其自己是一個 Unicode 字符,位於文本文件頭部。

雖然 BOM 頭起到了標記文件編碼的做用,可是它並不屬於文件的內容部分,所以會產生一些問題,如文件編碼發生變化後沒法正確讀取文件的內容,或者多個文件在合併的過程當中,中間會夾雜着這些多餘內容,因此在 NodeJS 文件操做的源碼中,Buffer 編碼轉換的模塊 iconv-lite 中,以及 Webpack 對項目文件進行打包編譯時都進行了去掉 BOM 頭的操做。

爲了讓上面的代碼能夠正確的讀取並解析編碼被手動修改過的文件內容,咱們這裏也須要進行去掉 BOM 頭的操做。

去掉 BOM 頭的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
複製代碼
function BOMStrip(result) {
    if (Buffer.isBuffer(result)) {
        // 若是讀取的內容爲 Buffer
        if (result[0] === 0xef && result[1] === 0xbb && result[2] === 0xbf) {
            // 若前三個字節是否和 BOM 頭的前三字節相同,去掉 BOM 頭
            return Buffer.slice(3);
        }
    } else {
        // 若是不是 Buffer
        if (result.charCodeAt(0) === 0xfeff) {
            // 判斷第一項是否和 BOM 頭的十六進制相同,去掉 BOM 頭
            return result.slice(1);
        }
    }
}
複製代碼

使用去掉 BOM 頭的方法並驗證上面讀文件的案例:

驗證去 BOM 頭的方法
1
2
3
4
5
6
7
8
9
10
複製代碼
// 引入依賴
const fs = require("fs");
const path = require("path");

// 兩種方式讀文件
let result1 = fs.readFileSync(path.resolve(__dirname, "a.txt"));
let result2 = fs.readFileSync(path.resolve(__dirname, "a.txt"), "utf8");

console.log(BOMStrip(result1).toString()); // 你好
console.log(BOMStrip(result2)); // 你好
複製代碼

緩存 Buffer

產生亂碼問題
1
2
3
4
5
6
7
複製代碼
let buf = Buffer.from("你好", "utf8");

let a = buf.slice(0, 2);
let b = buf.slice(2, 6);

console.log(a.toString()); // �
console.log(b.toString()); // �好
複製代碼

UTF-8 編碼,一個漢字三個字節,使用 slice 方法對一個表達漢字的 Buffer 進行截取,若是截取長度不是 3的整數倍,此時沒法正確解析,會顯示亂碼,相似這種狀況可使用模塊 string_decoder 對不能組成漢字的 Buffer 進行緩存,string_decoder 是核心模塊,不須要安裝。

緩存 Buffer
1
2
3
4
5
6
7
8
9
10
11
12
13
複製代碼
// 引入依賴
const { StringDecoder } = require("string_decoder");

let buf = Buffer.from("你好", "utf8");

let a = buf.slice(0, 2);
let b = buf.slice(2, 6);

// 建立 StringDecoder 實例
let sd = new StringDecoder();

console.log(sd.write(a));
console.log(sd.write(b)); // 你好
複製代碼

上面代碼中使用了 string_decoder 後,截取的 Buffer 不能組成一個漢字的時候不打印,進行緩存,等到能夠正確解析時取出緩存,從新拼接後打印。

原文出自:https://www.pandashen.com

相關文章
相關標籤/搜索