NodeJS —— Buffer 解讀

在這裏插入圖片描述


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


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
// 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 的每一項。

傳入字符串和字符編碼:

// 傳入字符串和字符編碼
let buf = Buffer.from("hello", "utf8");

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

傳入數組:

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

console.log(buf); // <Buffer 01 02 03>
// 數組成員爲十六進制數
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 個十六進制數組成。

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

console.log(buf); // <Buffer 01 02 03>

傳入的數組成員可以是任何進制的數值,當成員爲字符串的時候,如果值是數字會被自動識別成數值類型,如果值不是數字或成員爲是其他非數值類型的數據,該成員會被初始化爲 00

創建的 Buffer 可以通過 toString 方法直接指定編碼進行轉換,默認編碼爲 UTF-8

傳入 Buffer:

// 傳入一個 Buffer
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 類比二維數組
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 的長度。
let buf = Buffer.alloc(3);

buf.fill(1);
console.log(buf); // <Buffer 01 01 01>
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 的異常。

let buf = Buffer.alloc(6);

buf.fill("abc", 1, 5);
console.log(buf); // <Buffer 00 61 62 63 61 00>
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。

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

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 長度充足
let targetBuf = Buffer.alloc(6);
let sourceBuf = Buffer.from("你好", "utf8");

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

console.log(targetBuf.toString()); // 你好
// 容器 Buffer 長度不足
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
// 數組
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 方法
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 的長度,那麼返回值存儲拼接後前規定長度個字節。

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
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,返回布爾值。

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 方法
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;
    }

    // 處理剩下的部分
    result.push(this.slice(start));

    // 返回結果
    return result;
}

驗證 split 方法:

// 驗證 split
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 編碼中漢字所佔字節數不同,所以讀出的內容無法解析,即爲亂碼。

// 引入依賴
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

如果想正確的讀出其他編碼格式文件的內容,上面代碼應該更改爲:

// 引入依賴
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 頭
// 引入依賴
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 頭的方法
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 頭的方法
// 引入依賴
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

// 產生亂碼問題
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
// 引入依賴
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 不能組成一個漢字的時候不打印,進行緩存,等到可以正確解析時取出緩存,重新拼接後打印。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章