Node.js底層知識 - 理解Buffer
一. 認識Buffer
1.1. 數(shù)據(jù)的二進制
計算機中所有的內(nèi)容:文字、數(shù)字、圖片、音頻、視頻最終都會使用二進制來表示。
JavaScript可以直接去處理非常直觀的數(shù)據(jù):比如字符串,我們通常展示給用戶的也是這些內(nèi)容。
不對啊,JavaScript不是也可以處理圖片嗎?
事實上在網(wǎng)頁端,圖片我們一直是交給瀏覽器來處理的; JavaScript或者HTML,只是負責告訴瀏覽器一個圖片的地址; 瀏覽器負責獲取這個圖片,并且最終將這個圖片渲染出來;
但是對于服務器來說是不一樣的:
服務器要處理的本地文件類型相對較多; 比如某一個保存文本的文件并不是使用 utf-8進行編碼的,而是用GBK,那么我們必須讀取到他們的二進制數(shù)據(jù),再通過GKB轉(zhuǎn)換成對應的文字;比如我們需要讀取的是一張圖片數(shù)據(jù)(二進制),再通過某些手段對圖片數(shù)據(jù)進行二次的處理(裁剪、格式轉(zhuǎn)換、旋轉(zhuǎn)、添加濾鏡),Node中有一個Sharp的庫,就是讀取圖片或者傳入圖片的Buffer對其再進行處理; 比如在Node中通過TCP建立長連接,TCP傳輸?shù)氖亲止?jié)流,我們需要將數(shù)據(jù)轉(zhuǎn)成字節(jié)再進行傳入,并且需要知道傳輸字節(jié)的大?。头诵枰鶕?jù)大小來判斷讀取多少內(nèi)容);
我們會發(fā)現(xiàn),對于前端開發(fā)來說,通常很少會和二進制打交道,但是對于服務器端為了做很多的功能,我們必須直接去操作其二進制的數(shù)據(jù);
所以Node為了可以方便開發(fā)者完成更多功能,提供給了我們一個類Buffer,并且它是全局的。
1.2. Buffer和二進制
我們前面說過,Buffer中存儲的是二進制數(shù)據(jù),那么到底是如何存儲呢?
我們可以將Buffer看成是一個存儲二進制的數(shù)組; 這個數(shù)組中的每一項,可以保存8位二進制: 00000000
為什么是8位呢?
在計算機中,很少的情況我們會直接操作一位二進制,因為一位二進制存儲的數(shù)據(jù)是非常有限的;
所以通常會將8位合在一起作為一個單元,這個單元稱之為一個字節(jié)(byte);
也就是說
1byte = 8bit,1kb=1024byte,1M=1024kb;比如很多編程語言中的int類型是4個字節(jié),long類型是8個字節(jié);
比如TCP傳輸?shù)氖亲止?jié)流,在寫入和讀取時都需要說明字節(jié)的個數(shù);
比如RGB的值分別都是255,所以本質(zhì)上在計算機中都是用一個字節(jié)存儲的;
也就是說,Buffer相當于是一個字節(jié)的數(shù)組,數(shù)組中的每一項對于一個字節(jié)的大?。?/p>
如果我們希望將一個字符串放入到Buffer中,是怎么樣的過程呢?
const buffer01 = new Buffer("why");
console.log(buffer01);

當然目前已經(jīng)不希望我們這樣來做了:

那么我們可以通過另外一個創(chuàng)建方法:
const buffer2 = Buffer.from("why");
console.log(buffer2);
如果是中文呢?
const buffer3 = Buffer.from("王紅元");
console.log(buffer3);
// <Buffer e7 8e 8b e7 ba a2 e5 85 83>
const str = buffer3.toString();
console.log(str);
// 王紅元
如果編碼和解碼不同:
const buffer3 = Buffer.from("王紅元", 'utf16le');
console.log(buffer3);
const str = buffer3.toString('utf8');
console.log(str); // ?s?~CQ
二. Buffer其他用法
2.1. Buffer的其他創(chuàng)建
Buffer的創(chuàng)建方式有很多:

來看一下Buffer.alloc:
我們會發(fā)現(xiàn)創(chuàng)建了一個8位長度的Buffer,里面所有的數(shù)據(jù)默認為00;
const buffer01 = Buffer.alloc(8);
console.log(buffer01); // <Buffer 00 00 00 00 00 00 00 00>
我們也可以對其進行操作:
buffer01[0] = 'w'.charCodeAt();
buffer01[1] = 100;
buffer01[2] = 0x66;
console.log(buffer01);
也可以使用相同的方式來獲?。?/p>
console.log(buffer01[0]);
console.log(buffer01[0].toString(16));
2.2. Buffer和文件讀取
文本文件的讀取:
const fs = require('fs');
fs.readFile('./test.txt', (err, data) => {
console.log(data); // <Buffer 48 65 6c 6c 6f 20 57 6f 72 6c 64>
console.log(data.toString()); // Hello World
})
圖片文件的讀?。?/p>
fs.readFile('./zznh.jpg', (err, data) => {
console.log(data); // <Buffer ff d8 ff e0 ... 40418 more bytes>
});
圖片文件的讀取和轉(zhuǎn)換:
將讀取的某一張圖片,轉(zhuǎn)換成一張200x200的圖片; 這里我們可以借助于 sharp庫來完成;
const sharp = require('sharp');
const fs = require('fs');
sharp('./test.png')
.resize(1000, 1000)
.toBuffer()
.then(data => {
fs.writeFileSync('./test_copy.png', data);
})
三. Buffer的內(nèi)存分配
事實上我們創(chuàng)建Buffer時,并不會頻繁的向操作系統(tǒng)申請內(nèi)存,它會默認先申請一個8 * 1024個字節(jié)大小的內(nèi)存,也就是8kb
node/lib/buffer.js:135行
Buffer.poolSize = 8 * 1024;
let poolSize, poolOffset, allocPool;
const encodingsMap = ObjectCreate(null);
for (let i = 0; i < encodings.length; ++i)
encodingsMap[encodings[i]] = i;
function createPool() {
poolSize = Buffer.poolSize;
allocPool = createUnsafeBuffer(poolSize).buffer;
markAsUntransferable(allocPool);
poolOffset = 0;
}
createPool();
假如我們調(diào)用Buffer.from申請Buffer:
這里我們以從字符串創(chuàng)建為例 node/lib/buffer.js:290行
Buffer.from = function from(value, encodingOrOffset, length) {
if (typeof value === 'string')
return fromString(value, encodingOrOffset);
// 如果是對象,另外一種處理情況
// ...
};
我們查看fromString的調(diào)用:
node/lib/buffer.js:428行
function fromString(string, encoding) {
let ops;
if (typeof encoding !== 'string' || encoding.length === 0) {
if (string.length === 0)
return new FastBuffer();
ops = encodingOps.utf8;
encoding = undefined;
} else {
ops = getEncodingOps(encoding);
if (ops === undefined)
throw new ERR_UNKNOWN_ENCODING(encoding);
if (string.length === 0)
return new FastBuffer();
}
return fromStringFast(string, ops);
}
接著我們查看fromStringFast:
這里做的事情是判斷剩余的長度是否還足夠填充這個字符串; 如果不足夠,那么就要通過 createPool創(chuàng)建新的空間;如果夠就直接使用,但是之后要進行 poolOffset的偏移變化;node/lib/buffer.js:428行
function fromStringFast(string, ops) {
const length = ops.byteLength(string);
if (length >= (Buffer.poolSize >>> 1))
return createFromString(string, ops.encodingVal);
if (length > (poolSize - poolOffset))
createPool();
let b = new FastBuffer(allocPool, poolOffset, length);
const actual = ops.write(b, string, 0, length);
if (actual !== length) {
// byteLength() may overestimate. That's a rare case, though.
b = new FastBuffer(allocPool, poolOffset, actual);
}
poolOffset += actual;
alignPool();
return b;
}
四. Stream
4.1. 認識Stream
什么是流呢?
我們的第一反應應該是流水,源源不斷的流動; 程序中的流也是類似的含義,我們可以想象當我們從一個文件中讀取數(shù)據(jù)時,文件的二進制(字節(jié))數(shù)據(jù)會源源不斷的被讀取到我們程序中; 而這個一連串的字節(jié),就是我們程序中的流;
所以,我們可以這樣理解流:
是連續(xù)字節(jié)的一種表現(xiàn)形式和抽象概念; 流應該是可讀的,也是可寫的;
在之前學習文件的讀寫時,我們可以直接通過 readFile或者 writeFile方式讀寫文件,為什么還需要流呢?
直接讀寫文件的方式,雖然簡單,但是無法控制一些細節(jié)的操作; 比如從什么位置開始讀、讀到什么位置、一次性讀取多少個字節(jié); 讀到某個位置后,暫停讀取,某個時刻恢復讀取等等; 或者這個文件非常大,比如一個視頻文件,一次性全部讀取并不合適;
事實上Node中很多對象是基于流實現(xiàn)的:
http模塊的Request和Response對象; process.stdout對象;
官方:另外所有的流都是EventEmitter的實例:
我們可以看一下Node源碼中有這樣的操作:

流(Stream)的分類:
Writable:可以向其寫入數(shù)據(jù)的流(例如fs.createWriteStream())。Readable:可以從中讀取數(shù)據(jù)的流(例如fs.createReadStream())。Duplex:同時為Readable和的流Writable(例如net.Socket)。Transform:Duplex可以在寫入和讀取數(shù)據(jù)時修改或轉(zhuǎn)換數(shù)據(jù)的流(例如zlib.createDeflate())。
這里我們通過fs的操作,講解一下Writable、Readable,另外兩個大家可以自行學習一下。
4.2. Readable
之前我們讀取一個文件的信息:
fs.readFile('./foo.txt', (err, data) => {
console.log(data);
})
這種方式是一次性將一個文件中所有的內(nèi)容都讀取到程序(內(nèi)存)中,但是這種讀取方式就會出現(xiàn)我們之前提到的很多問題:
文件過大、讀取的位置、結束的位置、一次讀取的大?。?/section>
這個時候,我們可以使用 createReadStream,我們來看幾個參數(shù),更多參數(shù)可以參考官網(wǎng):
start:文件讀取開始的位置; end:文件讀取結束的位置; highWaterMark:一次性讀取字節(jié)的長度,默認是64kb;
const read = fs.createReadStream("./foo.txt", {
start: 3,
end: 8,
highWaterMark: 4
});
我們?nèi)绾潍@取到數(shù)據(jù)呢?
可以通過監(jiān)聽data事件,獲取讀取到的數(shù)據(jù);
read.on("data", (data) => {
console.log(data);
});
我們也可以監(jiān)聽其他的事件:
read.on('open', (fd) => {
console.log("文件被打開");
})
read.on('end', () => {
console.log("文件讀取結束");
})
read.on('close', () => {
console.log("文件被關閉");
})
甚至我們可以在某一個時刻暫停和恢復讀?。?/p>
read.on("data", (data) => {
console.log(data);
read.pause();
setTimeout(() => {
read.resume();
}, 2000);
});
4.3. Writable
之前我們寫入一個文件的方式是這樣的:
fs.writeFile('./foo.txt', "內(nèi)容", (err) => {
});
這種方式相當于一次性將所有的內(nèi)容寫入到文件中,但是這種方式也有很多問題:
比如我們希望一點點寫入內(nèi)容,精確每次寫入的位置等;
這個時候,我們可以使用 createWriteStream,我們來看幾個參數(shù),更多參數(shù)可以參考官網(wǎng):
flags:默認是 w,如果我們希望是追加寫入,可以使用a或者a+;start:寫入的位置;
我們進行一次簡單的寫入
const writer = fs.createWriteStream("./foo.txt", {
flags: "a+",
start: 8
});
writer.write("你好啊", err => {
console.log("寫入成功");
});
如果我們希望監(jiān)聽一些事件:
writer.on("open", () => {
console.log("文件打開");
})
writer.on("finish", () => {
console.log("文件寫入結束");
})
writer.on("close", () => {
console.log("文件關閉");
})
我們會發(fā)現(xiàn),我們并不能監(jiān)聽到 close 事件:
這是因為寫入流在打開后是不會自動關閉的; 我們必須手動關閉,來告訴Node已經(jīng)寫入結束了; 并且會發(fā)出一個 finish事件的;
writer.close();
writer.on("finish", () => {
console.log("文件寫入結束");
})
writer.on("close", () => {
console.log("文件關閉");
})
另外一個非常常用的方法是 end:
end方法相當于做了兩步操作:write傳入的數(shù)據(jù)和調(diào)用close方法;
writer.end("Hello World");
4.4. pipe方法
正常情況下,我們可以將讀取到的 輸入流,手動的放到 輸出流中進行寫入:
const fs = require('fs');
const { read } = require('fs/promises');
const reader = fs.createReadStream('./foo.txt');
const writer = fs.createWriteStream('./bar.txt');
reader.on("data", (data) => {
console.log(data);
writer.write(data, (err) => {
console.log(err);
});
});
我們也可以通過pipe來完成這樣的操作:
reader.pipe(writer);
writer.on('close', () => {
console.log("輸出流關閉");
})
備注:所有內(nèi)容首發(fā)于公眾號,之后會更新Flutter、TypeScript、React、Node、uniapp、mpvue、數(shù)據(jù)結構與算法等等一系列教程,也會更新一些自己的學習心得等,歡迎大家關注
