とあるWeb屋の備忘録

とあるWeb屋の備忘録。たまに雑記。

Node.jsのストリームAPIについて

Node.jsのストリームAPIについて

Nodeの勉強をしてストリームAPIを扱う処理がよくわからなかったので調べました。今日はストリームAPIについて書こうと思います。

Node.jsには大きいデータを扱うとき、ストリームAPIを利用することでメモリの消費を抑えることができます。
ストリームAPIは、データを少しずつ読み込んだり、書き込んだり、変換したりできます。

ストリームAPIをざっくり言うと、
・「データの流れ」を抽象化するためのインターフェース
・ストリームはオブジェクト
・すべてのストリームはEventEmitterのインスタンス
・ストリームの内部データはバッファ。(内部バッファにデータを格納していく。)

ストリームを理解するまえにEventEmitterとバッファについて整理する必要があったのでまとめました。

EventEmitter

EventEmitterはオブジェクトに関数をアタッチすることができるオブジェクト。(イベントを発火させることができる機能)
EventEmitterはNode.jsのeventモジュールの中に入っているオブジェクトなので以下で呼び出せる。

var EventEmitter = require('event').EventEmitter;

EventEmitterはイベントを発行する使い方(emit)とイベントを受け付ける使い方(on)の2通りの使い方がある。 例えば以下のソース

var EventEmitter = require('event').EventEmitter;

function hogeFunc() { //イベントを受け取る
    var ev = new EventEmitter;
    ev.on('data', function(data) {
        console.log('on', data)
    });
}

ev.emit('data', 1); //イベントを発行する。dataイベントを発行して1を渡す
// => on 1

EventEmitterを継承することでemitやonを使えるようになる。
Node.jsではイベントを生成するすべてのオブジェクトはEventEmitterのインスタンスである。
そして重要ポイントとして、streamはEventEmitterを継承している!!
一瞬ストリームの話に戻りますが、ストリームには
ReadableStreamとWritebleStreamがある。

そしてReadableStreamによるファイル読み込み処理は次のように書く。 EventEmitterのインスタンスなのでonをつかってイベントを受け取れる。
そして'data'イベントで少しずつデータが流れてくるのが特徴!'data'イベントはデータ読み込み時に発生して、発生するごとにデータの破片がコールバックに渡されます。この破片をchunkという。
chunkについて、こちらも少しまとめる必要があったので詳しくはバッファを解説するときに書きます。

var readableStream = fs.createReadStream('filename');
readableStream.on('data', function(chunk) {
  console.log(chunk);
});

ここで以下のソースを例としてあげる。
これは'data'イベントで少しずつデータが流れてくるときに、データを結合する処理である。詳しくはバッファの解説で説明します。

onはEventEmitterのインスタンスでしか使えないはずなのに、
reqがonを使ってイベントを受け取っているのはなぜ可能か。

req.on("data", function(chunk) {
    data += chunk;
});

reqはhttp.IncomingMassageクラスのインスタンスであり、ReadableStreamインターフェースを持っている。
ReadableStreamインタフェースを持つので情報を取得する場合は'data'イベントを追加すれば取得できる。
したがってここではonと'data'イベントを使っている。

バッファについて

EventEmitterのreadableStreamを使って情報を取得するために'data'イベントを使うが、そのときにバッファのことも理解する必要があるので合わせてこちらにまとめました。

・バッファとはメモリ上に確保されるバイナリデータを格納するための変数
・Node.jsではBufferクラスとして提供されている
・バッファに一時的に情報を保存することをバッファリングという
・内部バッファにデータを格納していく

例として上の説明にも出てきた以下のソースを解説していく。

req.on("data", function(chunk) {
    data += chunk;
});

まず'data'イベントが発生するごとにデータの破片であるchunkがコールバックに渡されます。
このchunkは'data'イベントがデフォルトで提供するBuffer型オブジェクト(bufferクラスのコンストラクタ)になります。
確認方法は以下

console.log('typeof chunk:', typeof chunk); //typeof chunk: object
console.log(Buffer.isBuffer(chunk)); //true

そしてBuffer型オブジェクトをconsole.logで確認すると、そのオブジェクトに格納されたバイナリ列が16進数で表示されます。

console.log('chunk:', chunk); 
//chunk: <Buffer 6e 61 6d 65 3d 25 45 33 25 38 33 25 38 30 25 45 33 25 38 33 25 39 46 25 45 33 25 38 33 25 42 43 25 45 33 25 38 33 25 38 36 25 45 33 25 38 32 25 41 44 ... >

これらを踏まえたうえで以下のソースを説明します。

var data = ""; //文字列型変数として定義
var posts[]; //後ほど使います
req.on("data", function(chunk) {
    data += chunk; //バイナリ列が格納されているBufferオブジェクトのchunkをdataに格納していく。こうすることでBufferを文字列にキャストして結合されていく。
    console.log('data:', data); //data: name=%E3%83%80%E3%83%9F%E3%83%BC%E3%83%86%E3%82%AD%E3%82%B9%E3%83%88
});

バイナリデータが無事にキャストされて文字列としてdataに格納されました。
ここまでだとデータの扱いが難しいのでさらに以下の処理でデータを扱いやすくします。

req.on("end", function(){
    var query = qs.parse(data); //queryStringを使ってdataをオブジェクト形式にパースします。
    console.log(query); //query: { name: 'ダミーテキスト' }
    posts.push(query.name); //これで投稿データをposts配列に格納できる
    renderForm(posts, res); //postsに格納されたデータをrenderFormメソッドに渡してejsでレンダリング
});

Buuferからstringへのキャスト、パースしてオブジェクト形式に変換するあたりは理解するためにtetatailで質問しました。
https://teratail.com/questions/126265

以下は参考サイト

https://www.tutorialspoint.com/nodejs/nodejs_event_emitter.htm

はじめてのNode.js:Node.jsのイベントシステムを知る 3ページ | OSDN Magazine

[Javascript] イベント駆動型の設計ができるEventEmitterに入門 - YoheiM .NET

Stream今昔物語 - from scratch

http://info-i.net/node-js-stream

Node.jsのStream APIの概要

Node.js Streamのリファレンスを読んでみた(Node.js v7.1.0) - M12i.

Stream | Node.js v10.3.0 Documentation