Node中Buffer 常用API解讀

煎蛋面__cq發表於2018-09-11

Buffer 概述

在 ES6 引入 TypedArray 之前,JavaScript 語言沒有讀取或操作二進位制資料流的機制。 Buffer 類被引入作為 NodeJS API 的一部分,使其可以在 TCP 流或檔案系統操作等場景中處理二進位制資料流。
Buffer 屬於 Global 物件,使用時不需引入,且 Buffer 的大小在建立時確定,無法調整。

建立 Buffer

在 NodeJS v6.0.0 版本之前,Buffer 例項是通過 Buffer 建構函式建立的,即使用 new 關鍵字建立,它根據提供的引數返回不同的 Buffer,但在之後的版本中這種宣告方式就被廢棄了,替代 new 的建立方式主要有以下幾種。

1、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 使得記憶體的分配非常快,但已分配的記憶體段可能包含潛在的敏感資料,有明顯效能優勢的同時又是不安全的,所以使用需格外 “小心”。

2、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 個十六進位制陣列成。

陣列成員為字串型別的數字
1
2
3
複製程式碼
let buf = Buffer.from(["1", "2", "3"]);

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

傳入的陣列成員可以是任何進位制的數值,當成員為字串的時候,如果值是數字會被自動識別成數值型別,如果值不是數字或成員為是其他非數值型別的資料,該成員會被初始化為 00

建立的 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 的常用方法

1、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');
複製程式碼

2、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
複製程式碼

3、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
複製程式碼

4、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];
    }
}
複製程式碼

5、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;
}
複製程式碼

6、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


相關文章