はじめてのNode.js:Node.js内でバイナリデータを扱うための「Buffer」クラス

 JavaScriptの標準規格であるECMA-262においては、ファイルシステムにアクセスするための機能やバイナリデータを処理するための機能については定義されていない。そのためNode.jsでは、ファイルに対する入出力およびファイルシステム操作を行うためのfsモジュールや、バイナリデータを扱うためのBufferクラスが用意されている。本記事では、Bufferクラスを使ったバイナリデータの処理について紹介する。

【連載】はじめてのNode.js

本記事について

 本記事は、3月13日にソフトバンク クリエイティブより発売された書籍「はじめてのNode.js -サーバーサイドJavaScriptでWebアプリを開発する-」から、「第7章 Node.jsアプリケーションのデバッグ方法」の一部を抜き出し再構成したものです。

 出版社ページ / Amazon.co.jpの商品ページ

 大型本: 384ページ、価格:3,045円(税込)、ISBN: 978-4797370904

文字列(String)とBuffer

 Node.jsでは、文字列とバイナリデータ(バイト列)はまったく別のものとして扱われる。前者を扱うためのクラスがStringで、後者を扱うNode.js独自のクラスがBufferだ。C言語など、文字列とバイナリ列を同じ型(もしくはクラス)で扱える言語もあるが、Node.jsでは文字列とバイト列は厳密に区別されており、文字列の格納にはStringクラスを、バイト列の格納にはBufferクラスを利用するようになっている。

Buffer型オブジェクトの作成——Bufferクラスのコンストラクタ

 BufferクラスはNode.jsにおいて任意のモジュールから利用できるグローバルなクラスであり、特定のモジュールをロードせずに利用できる。Buffer型のオブジェクトは、newキーワードを使ってBufferクラスのコンストラクタを実行することで作成できる。Bufferクラスのコンストラクタには3つの形式があり、1つめは作成するバッファサイズのみを指定するものだ。

new Buffer(size)

 size引数には作成するバッファのサイズを整数で指定する。たとえば128バイトのバッファを作成するには次のようにする。

> var buf = new Buffer(128)
undefined
> buf // 作成されたバッファは初期化されない
<Buffer 40 ff ff ff 48 8b 45 c0 48 89 45 a8 48 89 45 a0 48 63 39 48 83 c1 04 e9 f7 fe ff ff 83 fa 30 0f 8c d7 fe ff ff 83 fa 39 0f 8f ce fe ff ff 48 83 e9 04 89 ...>

 このようにBuffer型のオブジェクトをREPL内で評価すると、そのオブジェクトに格納されたバイナリ列が16進数で表示される。

 なお、この形のコンストラクタで作成されたバッファの値は未定義となる。そのため、必要に応じてfillメソッドなどで初期化しておく。fillメソッドは指定した値をバッファに書き込むメソッドだ。

buf.fill(value, [offset], [end])

 value引数には書き込む値を、offsetおよびend引数では書き込みを開始する位置および終了する位置を指定する。offsetのデフォルト値は0、endのデフォルト値はバッファのサイズを格納するbuffer.lengthプロパティの値となる。

> buf.fill(0) // バッファに0を書き込む
undefined
> buf
<Buffer 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ...>
> buf.fill(1, 10) // バッファの10バイト目から最後までに1を書き込む
undefined
> buf
<Buffer 00 00 00 00 00 00 00 00 00 00 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 ...>
> buf.fill(2, 20, 30) // バッファの20バイト目から30バイト目までに2を書き込む
undefined
> buf
<Buffer 00 00 00 00 00 00 00 00 00 00 01 01 01 01 01 01 01 01 01 01 02 02 02 02 02 02 02 02 02 02 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 ...>

 格納するデータを配列で与えてBufferクラスのインスタンスを作成することもできる。

new Buffer(array)

 array引数にはバッファに格納するデータを格納した配列を指定する。たとえば「0x00 0x01 0x02」というデータを格納したバッファを作成するには、次のようにする。

> var buf = new Buffer([0, 1, 2]);
undefined
> buf // 作成されたバッファに引数で与えたデータが格納される
<Buffer 00 01 02>

 また、文字列からバッファを作成することも可能だ。

new Buffer(str, [encoding])

 str引数にはバッファに格納する文字列を、encoding引数にはバッファに格納する際のエンコーディングを文字列で指定する。encoding引数が省略された場合「utf8」が使用される。

> var buf = new Buffer('これは文字列です');
undefined
> buf
<Buffer e3 81 93 e3 82 8c e3 81 af e6 96 87 e5 ad 97 e5 88 97 e3 81 a7 e3 81 99>

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

 Node.jsでは、文字列の入出力を行う際に自動的にエンコード/デコード処理を行うようになっている。Node.jsでは、表1で挙げている7つのエンコーディングが用意されている。

表1 Node.jsで利用できるエンコーディング
エンコーディング名説明
asciiASCII文字列
utf8UTF-8文字列
utf16leリトルエンディアンUTF-16(UTF-16LE)文字列
ucs2utf16lfと同じ
base64BASE64でエンコードされた文字列
binaryバイナリデータ(利用は推奨されない)
hex16進数で表記された文字列

 ただし、binaryエンコーディングについては廃止予定となっており、可能な限り利用を避けることが推奨されている。

 文字列からBufferクラスを作成する場合、指定したエンコーディングによってオブジェクトに格納されるデータは異なるものになる。たとえば次の例は、「0123abcd」という文字列から、異なるエンコーディングを指定してBufferクラスのオブジェクトを作成したものだ。

> new Buffer('0123abcd', 'ascii')
<Buffer 30 31 32 33 61 62 63 64>
> new Buffer('0123abcd', 'utf8')
<Buffer 30 31 32 33 61 62 63 64>
> new Buffer('0123abcd', 'utf16le')
<Buffer 30 00 31 00 32 00 33 00>
> new Buffer('0123abcd', 'base64')
<Buffer d3 5d b7 69 b7 1d>
> new Buffer('0123abcd', 'hex')
<Buffer 01 23 ab cd>

 ここでは引数にASCII文字列のみを与えているため、asciiエンコーディングおよびutf8エンコーディングを指定した場合の結果は同じとなり、それ以外の場合はそれぞれ異なる結果となっていることが分かる。

 なお、非ASCII文字が含まれる文字列に対しasciiエンコーディングを指定された場合、各文字の下位7ビット部分のみが使われる。たとえば「あ」という文字はUTF-8では「0xE3 0x81 0x82」という3バイトで表されるが、これをasciiエンコーディングでバイト列化すると「0x42」という1バイトのデータとなってしまう。

> new Buffer('あ', 'utf8');
<Buffer e3 81 82>
> new Buffer('あ', 'ascii')
<Buffer 42>

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

 StringクラスもBufferクラスも、複数バイトのデータを格納するという点では似通っている。また、どちらも格納しているデータをインデックスで指定して部分的に抜き出す操作が可能な点も似ている。しかし、これらのクラス間で決定的に異なる点は、Stringクラスは文字単位で、Bufferクラスはバイト単位でデータを管理することだ。

 たとえば、「あいうえお」という文字列と、その文字列から作成したバイナリデータについてlengthプロパティでその長さを取得してみると、それぞれ異なる値が得られる。Stringクラスではlengthプロパティの値は文字数となるのに対し、Bufferクラスではバイト数になるからだ。

> var str = 'あいうえお'
'あいうえお'
> var buf = new Buffer('あいうえお')
<Buffer e3 81 82 e3 81 84 e3 81 86 e3 81 88 e3 81 8a>
> str.length
5
> buf.length
15

 また、どちらのクラスも、インデックスを指定して値を取り出すことができる。ただし、文字列の場合インデックスの単位は1文字になるが、Buffer型オブジェクトの場合は1バイトになる。たとえば、文字列を格納したstr変数に対し「str[2]」とすると文字列の2文字目を取得できるが、Buffer型のbufオブジェクトに対し「buf[2]」とすると、バイト列の2バイト目の値が整数に変換されて取り出される。

> var str = 'あいうえお'
'あいうえお'
> var buf = new Buffer('あいうえお')
<Buffer e3 81 82 e3 81 84 e3 81 86 e3 81 88 e3 81 8a>
> str[2]
'う'
> buf[2]
130
「2文字目」について

 ここでは、文字列の一番最初の文字を0文字目、その次の文字を1文字目、……とカウントすることにする。バイト列についても同様に一番最初の8ビットを0バイト目、次の8ビットを1バイト目、……とカウントしている。

 また、Stringクラスのオブジェクトはimmutableであり、オブジェクトの作成後にその値を書き換えることはできない。

> var str = 'あいうえお'
'あいうえお'
> str[0] = 'か'
'か'
> str
'あいうえお'
「immutable」について

 オブジェクトの生成後にその値を変更できないオブジェクトをこう呼ぶ。たとえばJavaのString型オブジェクトはimmutableだ。いっぽう、生成後にその値を変更できるオブジェクトはmutableと呼ばれる。

 いっぽう、Bufferクラスのオブジェクトはmutableであり、[]演算子などを使ってその値を直接変更できる。

> var buf = new Buffer('あいうえお')
<Buffer e3 81 82 e3 81 84 e3 81 86 e3 81 88 e3 81 8a>
> buf[0] = 0
0
> buf
<Buffer 00 81 82 e3 81 84 e3 81 86 e3 81 88 e3 81 8a>

 そのほか、Stringクラスには格納している文字列から指定した文字列を検索するindexOfメソッドやmatchメソッドおよびsearchメソッド、置換を行うreplaceメソッド、部分文字列を取り出すsubstringメソッドなどが用意されているが、Bufferクラスではこのようなメソッドは用意されていない。ただし、指定した位置のデータを取り出すsliceメソッドは用意されている。こちらはStringクラスのsliceメソッドと同じように利用できる。たとえば以下の例は、文字列の2文字目から3文字目と、バイト列の2バイト目から3バイト目を取りだすものだ。

> var str = 'あいうえお'
'あいうえお'
> var buf = new Buffer('あいうえお')
<Buffer e3 81 82 e3 81 84 e3 81 86 e3 81 88 e3 81 8a>
> str.slice(2, 4)
'うえ'
> buf.slice(2, 4)
<Buffer 82 e3>

 なお、sliceメソッドで取り出したバッファを変更すると、元のバッファも変更される。sliceメソッドではバイナリデータを格納しているメモリ領域をコピーせずに共有して使用するからだ。たとえば下記の例では、「あいうえお」という文字列から作成したバイト列の2バイト目から3バイト目を取りだしてsubBuf変数に格納した後、subBuf変数の0バイト目を0に設定している。このとき、buf変数に格納されているバイト列も変更されていることが分かる。

> var buf = new Buffer('あいうえお')
<Buffer e3 81 82 e3 81 84 e3 81 86 e3 81 88 e3 81 8a>
> subBuf = buf.slice(2, 4)
<Buffer 82 e3>
> subBuf[0] = 0
0
> subBuf
<Buffer 00 e3>
> buf
<Buffer e3 81 00 e3 81 84 e3 81 86 e3 81 88 e3 81 8a>