menu

node.js の byte について

公開日:2019年06月19日 カテゴリー:JavaScript, Node.js, 開発

今回は業務の中で感じた「非効率なプログラミング」を元に、どうすればよかったのかを振り返り、今後に活かしていこうとまとめを行ってもらいました。

社内では、こうすればよかったのにとか、なんで調べなかったんだ等「悪かったこと」や「原因」を突き詰めるのではなく、振り返った結果、次をどうすればよいのか、それを考えるようにしています。

今回は振り返りから「Buffer オブジェクト」についてまとめてもらいました。

はじめに

以前、node.js を使う業務でマーシャリングを行うライブラリを書きました。

その当時は、node.js の Buffer オブジェクトをよく理解していなかったため、非効率なプログラミングをしていました。

その反省として Buffer オブジェクトの学習をしたので、それをまとめてみました。

要件

要件は、「特定の形式を持つ 16 進数表記の byte 列を、JSON オブジェクトと相互変換したい」というものです。

byte 列の内、特定の部分がその型を表しており、それによって変換の形式が異なっていました。

反省点

入出力される 16 進数表記の byte 列は文字列で扱われるのだけれど、内部でもそれをそのまま文字列として扱ってしまいました。

そのため、不必要に文字列の操作を行うコードが散在し、可読性の低いコードになってしまいました。

本来であれば、内部的にはバイト操作のためのバイナリバッファなどを用いるべきだったと思います。

具体的に言うと、入力された文字列をバリデートしたのちに、バイナリバッファに変換するなどです。

出力時には、バイナリバッファを文字列へ変換するといったことをするだけで、随分違ったはずだと思います。

node.js におけるバイト列の操作

Node.js におけるバイナリデータは、Buffer クラスで扱うことができます。

Buffer クラスはグローバルなクラスのため、任意のモジュールで利用できます。

Buffer オブジェクトの生成

Buffer(size)コンストラクタを使うことで、Buffer クラスのインスタンスを生成できます。

Buffer オブジェクトの値埋め

Buffer#fill(value, [offset], [end])メソッドにより、Buffer を任意の値で埋めることが出来ます。

特定の値で初期化した Buffer オブジェクトの生成

Buffer(array)コンストラクタを使うことで、格納する初期値を配列で与えることができます。

また、Buffer(str, [encoding])を用いることで、文字列からバッファを生成することもできます。

第 2 引数の encoding を省略した場合、utf8が使用されます。

第 2 引数の encoding に指定する値によって、格納されるデータは異なります。

文字列とエンコーディング

Node.js では、以下の 7 つのエンコーディングが用意されています。

ascii ASCII 文字列
utf8 UTF-8 文字列
utf16le リトルエンディアン UTF-16(UTF-16LE)文字列
ucs2 utf16lf と同じ
base64 BASE64 でエンコードされた文字列
binary バイナリデータ(利用は推奨されない)
hex 16 進数で表記された文字列

String クラスと Buffer クラスの違い

どちらも複数バイトのデータを格納し、インデックス指定で部分的な抜き出しが可能な点は似通っています。

しかし、String クラスは文字単位で、Buffer クラスはバイト単位でデータを管理する点が異なります。それにより、以下のような違いが生まれます。

length プロパティ

String クラスの場合の length プロパティは、文字数を表します。

Buffer クラスの場合の length プロパティは、バイト数を表します。

インデックス

String クラスから index 指定で値を取り出すとき、index の単位は 1 文字となります。

Buffer クラス index 指定で値を取り出すとき、index の単位は 1 バイトとなります。
Buffer クラスから index 指定で取り出された値は、整数となります。

なお、直感通り String も Byte も、その内容の最初の要素の index は 0 となります。

不変性

String クラスは immutable だが、Buffer クラスは mutable です。
そのため、Buffer クラスは[]演算子などでその値を直接変更できます。

メソッド

String クラスには、格納している内容を操作するメソッドが用意されています。

例えば、indexOf, match, search, replace, substring など。

しかし、Buffer クラスにはこのようなメソッドはありません。
ただし、指定した位置のデータを取り出すための slice メソッドは用意されています。

これは String クラスの slice メソッドと同じように利用できます。

なお、slice メソッドで取り出した Buffer を変更すると、もとの Buffer も変更されます。

Buffer と String の相互変換

Buffer -> String の変換

Buffer#toString を用いることで、格納されたデータを文字列に変更できます。

encoding が省略された場合、utf8が指定されたものとして扱われます。

start 引数には開始する位置を、end 引数には変換の開始位置と終了位置をバイト単位で指定します。

String -> Buffer の変換

文字列は Byte 列として扱いたい場合、Buffer のコンストラクタを使います。

また、既存の Buffer クラスに文字列を書き込みたい場合には、Buffer#write を使います。

string 引数には変換したい文字列、offset 引数と length 引数で変換後のデータの格納位置を指定します。

変換後の文字列は、格納されてるバイト列の最初から offset バイト目から、offset+length バイト目の位置に格納されます。

offset のデフォルト値は 0, length の初期値は buffer.length – offset となります。

StringDecoder を使う場合

StringDecorder は、Buffer オブジェクトを文字列に変換するためのオブジェクトです。

なお、StringDecorder は明示的に require する必要があります。

Buffer から文字列への変換には、StringDecorder#write を使用します。

Buffer と Number の相互変換

JavaScript の数値はすべて Number 型として扱われます。
しかし、入出力の際に数値の型を意識する必要がある場合があります。

Buffer クラスには、それらに対応したメソッドが用意されています。

これらは、指定した位置のバイナリデータを JavaScript の Number 型で読み出したり、Number 型のデータを指定した型で Buffer に格納する際に用います。

これらのメソッドのインターフェースはどれも同じため、readUInt8 および writeUInt8 のみの使い方を書いてみます。

Buffer -> Number への変換

バッファ内のデータを指定した形式で取り出す際には、以下の形式のメソッドを使います。

offset 引数には、取得するデータの位置を指定します。
noAssert には、offset の値を検証するかどうかを bool 型で指定します。

noAssert はデフォルトでは false に設定されており、offset に Buffer の終端を超える値が指定された場合に RangeError を発生させます。

noAssert に true を設定した場合、offset に Buffer の終端を超える値が指定された場合、undefined を返します。

Number -> Buffer への変換n

Number 型のオブジェクトを指定した型で Buffer に格納するには、以下の形式のメソッドを使います。

vallue には書き込みたいデータを、offset には書き込む位置を指定します。

noAssert は offset の値を検証するかを指定します。

value に指定した型では扱えない範囲の値が指定された場合、例外が発生します。

数値との相互変換メソッドの一覧

buf.readUInt8(offset, [noAssert]) 8 ビット符合無し整数
buf.writeUInt8(value, offset, [noAssert])
buf.readUInt16LE(offset, [noAssert]) 16 ビット符合無し整数(リトルエンディアン)
buf.writeUInt16LE(value, offset, [noAssert])
buf.readUInt16BE(offset, [noAssert]) 16 ビット符合無し整数(ビッグエンディアン)
buf.writeUInt16BE(value, offset, [noAssert])
buf.readUInt32LE(offset, [noAssert]) 32 ビット符合無し整数(リトルエンディアン)
buf.writeUInt32LE(value, offset, [noAssert])
buf.readUInt32BE(offset, [noAssert]) 16 ビット符合無し整数(ビッグエンディアン)
buf.writeUInt32BE(value, offset, [noAssert])
buf.readInt8(offset, [noAssert]) 8 ビット符合あり整数
buf.writeInt8(value, offset, [noAssert])
buf.readInt16LE(offset, [noAssert]) 16 ビット符合あり整数(リトルエンディアン)
buf.writeInt16LE(value, offset, [noAssert])
buf.readInt16BE(offset, [noAssert]) 16 ビット符合無し整数(ビッグエンディアン)
buf.writeInt16BE(value, offset, [noAssert])
buf.readInt32LE(offset, [noAssert]) 32 ビット符合あり整数(リトルエンディアン)
buf.writeInt32LE(value, offset, [noAssert])
buf.readInt32BE(offset, [noAssert]) 32 ビット符合あり整数(ビッグエンディアン)
buf.writeInt32BE(value, offset, [noAssert])
buf.readFloatLE(offset, [noAssert]) 単精度浮動小数点数(リトルエンディアン)
buf.writeFloatLE(value, offset, [noAssert])
buf.readFloatBE(offset, [noAssert]) 単精度浮動小数点数(ビッグエンディアン)
buf.writeFloatBE(value, offset, [noAssert])
buf.readDoubleLE(offset, [noAssert]) 倍精度浮動小数点数(リトルエンディアン)
buf.writeDoubleLE(value, offset, [noAssert])
buf.readDoubleBE(offset, [noAssert]) 倍精度浮動小数点数(ビッグエンディアン)

Buufer オブジェクトのコピー

Buffer オブジェクトに格納されているデータをコピーするには、Buffer#copy を使用します。

targetBuffer には書き込み先 Buffer オブジェクトを指定します。

targetStart で書き込み先の位置を指定します。

書き込むデータの範囲は、sourceStart と sourceEnde で指定できます。

targetStart 及び sourceStrart のデフォルト値は 0, sourceEnd の default 値は length と同じになっています。

Buffer のクラスメソッド

  • Buffer.isBuffer
  • Buffer.byteLength
  • Buffer.concat

オブジェクトが Buffer かを判定する

渡したオブジェクトが Buffer の場合 true を、そうでない場合 false を返します。

文字列のバイト長を計算する

渡した文字列のバイト長を計算します。
文字列の長さではなく、Buffer に変換した場合の長さを返します。

複数の Buffer を結合する

複数の Buffer を結合します。

list には結合する Buffer を格納した配列を、totalLength には結合結果の Buffer のサイズを指定します。

totalLength が指定されなかった場合、結果の Buffer は list で指定した各 Buffer の合計となります。

 

いかがでしたか?

こうして記録に残すことによって、自分にも、そしてこれから携わる人にもプラスになるのではないでしょうか?

読んでくださった皆様にも、何かプラスになっていれば幸いです。

最後までお読みくださり、ありがとうございました。

2019

参考資料