1. 程式人生 > >Node.js的Buffer那些你可能不知道的用法

Node.js的Buffer那些你可能不知道的用法

ins min 來看 imu write work and sse ase

在大多數介紹Buffer的文章中,主要是圍繞數據拼接和內存分配這兩方面的。比如我們使用fs模塊來讀取文件內容的時候,返回的就是一個Buffer:

fs.readFile(‘filename‘, function (err, buf) {
  // <Buffer 2f 2a 2a 0a 20 2a 20 53 75 ... >
});

在使用nethttp模塊來接收網絡數據時,data事件的參數也是一個Buffer,這時我們還需要使用Buffer.concat()來做數據拼接:

var bufs = [];
conn.on(‘data‘, function (buf) {
  bufs.push(buf);
});
conn.on(‘end‘, function () {
  // 接收數據結束後,拼接所有收到的Buffer對象
  var buf = Buffer.concat(bufs);
});

還可以利用Buffer.toString()來做轉換base64或十六進制字符的轉換,比如:

console.log(new Buffer(‘hello, world!‘).toString(‘base64‘));
// 轉換成base64字符串:aGVsbG8sIHdvcmxkIQ==

console.log(new Buffer(‘aGVsbG8sIHdvcmxkIQ==‘, ‘base64‘).toString());
// 還原base64字符串:hello, world!

console.log(new Buffer(‘hello, world!‘).toString(‘hex‘));
// 轉換成十六進制字符串:68656c6c6f2c20776f726c6421

console.log(new Buffer(‘68656c6c6f2c20776f726c6421‘, ‘hex‘).toString());
// 還原十六進制字符串:hello, world!

一般情況下,單個Node.js進程是有最大內存限制的,以下是來自官方文檔中的說明:

What is the memory limit on a node process?

Currently, by default v8 has a memory limit of 512MB on 32-bit systems, and 1.4GB on 64-bit systems. The limit can be raised by setting --max_old_space_size to a maximum of ~1024 (~1 GB) (32-bit) and ~4096 (~4GB) (64-bit), but it is recommended that you split your single process into several workers if you are hitting memory limits.

由於Buffer對象占用的內存空間是不計算在Node.js進程內存空間限制上的,因此,我們也常常會使用Buffer來存儲需要占用大量內存的數據:

// 分配一個2G-1字節的數據
// 單次分配內存超過此值會拋出異常 RangeError: Invalid typed array length
var buf = new Buffer(1024 * 1024 * 1024 - 1);

以上便是Buffer的幾種常見用法。然而,閱讀Buffer的API文檔時,我們會發現更多的是readXXX()writeXXX()開頭的API,具體如下:

  • buf.readUIntLE(offset, byteLength[, noAssert])
  • buf.readUIntBE(offset, byteLength[, noAssert])
  • buf.readIntLE(offset, byteLength[, noAssert])
  • buf.readIntBE(offset, byteLength[, noAssert])
  • buf.readUInt8(offset[, noAssert])
  • buf.readUInt16LE(offset[, noAssert])
  • buf.readUInt16BE(offset[, noAssert])
  • buf.readUInt32LE(offset[, noAssert])
  • buf.readUInt32BE(offset[, noAssert])
  • buf.readInt8(offset[, noAssert])
  • buf.readInt16LE(offset[, noAssert])
  • buf.readInt16BE(offset[, noAssert])
  • buf.readInt32LE(offset[, noAssert])
  • buf.readInt32BE(offset[, noAssert])
  • buf.readFloatLE(offset[, noAssert])
  • buf.readFloatBE(offset[, noAssert])
  • buf.readDoubleLE(offset[, noAssert])
  • buf.readDoubleBE(offset[, noAssert])
  • buf.write(string[, offset][, length][, encoding])
  • buf.writeUIntLE(value, offset, byteLength[, noAssert])
  • buf.writeUIntBE(value, offset, byteLength[, noAssert])
  • buf.writeIntLE(value, offset, byteLength[, noAssert])
  • buf.writeIntBE(value, offset, byteLength[, noAssert])
  • buf.writeUInt8(value, offset[, noAssert])
  • buf.writeUInt16LE(value, offset[, noAssert])
  • buf.writeUInt16BE(value, offset[, noAssert])
  • buf.writeUInt32LE(value, offset[, noAssert])
  • buf.writeUInt32BE(value, offset[, noAssert])
  • buf.writeInt8(value, offset[, noAssert])
  • buf.writeInt16LE(value, offset[, noAssert])
  • buf.writeInt16BE(value, offset[, noAssert])
  • buf.writeInt32LE(value, offset[, noAssert])
  • buf.writeInt32BE(value, offset[, noAssert])
  • buf.writeFloatLE(value, offset[, noAssert])
  • buf.writeFloatBE(value, offset[, noAssert])
  • buf.writeDoubleLE(value, offset[, noAssert])
  • buf.writeDoubleBE(value, offset[, noAssert])

這些API為在Node.js中操作數據提供了極大的便利。假設我們要將一個整形數值存儲到文件中,比如當前時間戳為1447656645380,如果將其當作一個字符串存儲時,需要占用11字節的空間,而將其轉換為二進制存儲時僅需6字節空間即可:

var buf = new Buffer(6);

buf.writeUIntBE(1447656645380, 0, 6);
// <Buffer 01 51 0f 0f 63 04>

buf.readUIntBE(0, 6);
// 1447656645380

在使用Node.js編寫一些底層功能時,比如一個網絡通信模塊、某個數據庫的客戶端模塊,或者需要從文件中操作大量結構化數據時,以上Buffer對象提供的API都是必不可少的。

接下來將演示一個使用Buffer對象操作結構化數據的例子。

操作結構化數據

假設有一個學生考試成績數據庫,每條記錄結構如下:

學號課程代碼分數
XXXXXX XXXX XX

其中學號是一個6位的數字,課程代碼是一個4位數字,分數最高分為100分。

在使用文本來存儲這些數據時,比如使用CSV格式存儲可能是這樣的:

100001,1001,99
100002,1001,67
100003,1001,88

其中每條記錄占用15字節的空間,而使用二進制存儲時其結構將會是這樣:

學號課程代碼分數
3字節 2字節 1字節

每一條記錄僅需要6字節的空間即可,僅僅是使用文本存儲的40%!下面是用來操作這些記錄的程序:

// 讀取一條記錄
// buf    Buffer對象
// offset 本條記錄在Buffer對象的開始位置
// data   {number, lesson, score}
function writeRecord (buf, offset, data) {
  buf.writeUIntBE(data.number, offset, 3);
  buf.writeUInt16BE(data.lesson, offset + 3);
  buf.writeInt8(data.score, offset + 5);
}

// 寫入一條記錄
// buf    Buffer對象
// offset 本條記錄在Buffer對象的開始位置
function readRecord (buf, offset) {
  return {
    number: buf.readUIntBE(offset, 3),
    lesson: buf.readUInt16BE(offset + 3),
    score: buf.readInt8(offset + 5)
  };
}

// 寫入記錄列表
// list  記錄列表,每一條包含 {number, lesson, score}
function writeList (list) {
  var buf = new Buffer(list.length * 6);
  var offset = 0;
  for (var i = 0; i < list.length; i++) {
    writeRecord(buf, offset, list[i]);
    offset += 6;
  }
  return buf;
}

// 讀取記錄列表
// buf  Buffer對象
function readList (buf) {
  var offset = 0;
  var list = [];
  while (offset < buf.length) {
    list.push(readRecord(buf, offset));
    offset += 6;
  }
  return list;
}

我們可以再編寫一段程序來看看效果:

var list = [
  {number: 100001, lesson: 1001, score: 99},
  {number: 100002, lesson: 1001, score: 88},
  {number: 100003, lesson: 1001, score: 77},
  {number: 100004, lesson: 1001, score: 66},
  {number: 100005, lesson: 1001, score: 55},
];
console.log(list);

var buf = writeList(list);
console.log(buf);
// 輸出 <Buffer 01 86 a1 03 e9 63 01 86 a2 03 e9 58 01 86 a3 03 e9 4d 01 86 a4 03 e9 42 01 86 a5 03 e9 37>

var ret = readList(buf);
console.log(ret);
/* 輸出
[ { number: 100001, lesson: 1001, score: 99 },
  { number: 100002, lesson: 1001, score: 88 },
  { number: 100003, lesson: 1001, score: 77 },
  { number: 100004, lesson: 1001, score: 66 },
  { number: 100005, lesson: 1001, score: 55 } ]
*/

lei-proto模塊介紹

上面的例子中,當每一條記錄的結構有變化時,我們需要修改readRecord()writeRecord(),重新計算每一個字段在Buffer中的偏移量,當記錄的字段比較復雜時很容易出錯。為此我編寫了lei-proto模塊,它允許你通過簡單定義每條記錄的結構即可生成對應的readRecord()和`writeRecord()函數。

首先執行以下命令安裝此模塊:

$ npm install lei-proto --save

使用lei-proto模塊後,前文的例子可以改為這樣:

var parsePorto = require(‘lei-proto‘);

// 生成指定記錄結構的數據編碼/解碼器
var record = parsePorto([
  [‘number‘, ‘uint‘, 3],
  [‘lesson‘, ‘uint‘, 2],
  [‘score‘, ‘uint‘, 1]
]);

function readList (buf) {
  var list = [];
  var offset = 0;
  while (offset < buf.length) {
    list.push(record.decode(buf.slice(offset, offset + 6)));
    offset += 6;
  }
  return list;
}

function writeList (list) {
  return Buffer.concat(list.map(record.encodeEx));
}

運行與上文同樣的測試程序,可看到其結果是一樣的:

<Buffer 01 86 a1 03 e9 63 01 86 a2 03 e9 58 01 86 a3 03 e9 4d 01 86 a4 03 e9 42 01 86 a5 03 e9 37>
[ { number: 100001, lesson: 1001, score: 99 },
  { number: 100002, lesson: 1001, score: 88 },
  { number: 100003, lesson: 1001, score: 77 },
  { number: 100004, lesson: 1001, score: 66 },
  { number: 100005, lesson: 1001, score: 55 } ]

關於lei-proto模塊的詳細使用方法可訪問該模塊的主頁瀏覽:https://github.com/leizongmin/node-lei-proto

對此感興趣的讀者也可研究一下其實現原理。

Node.js的Buffer那些你可能不知道的用法