Node コールバック関数とノンブロッキングI/O の動作

Node.js の コールバック関数とノンブロッキングI/O の動作について、サンプルプログラムを書いて確認したメモです。

## 確認する問題
I/Oが発生する場合とメモリ上で完結する場合では、全く動作が異なるため注意しなければならない。 この注意点をプログラムの動作で判る様に書いたのが、次のスニペットです。これは a_plus_b() という 第一引数と第二引数を加えた結果を返す単純なコードです。 このコードを使って、ノンブロッキングI/Oの動作を確認します。

実行順番がわかる様に、最初と最後に start と end を表示して、計算結果を得るコールバック関数で結果の表示をします。このa_plus_b() の実装を変えて、実行順番を確認します。 a_plus_b()の中は、一瞬で終わるので、３秒のスリープを入れて実行時刻が明確に判る様にします。

~~~
console.log("start");

a_plus_b(1,2,function(rslt) {
    console.log("rslt=", rslt);
});

console.log("end ");
~~~

## メモリ上完結の場合の実行順序
最初に、メモリ上で a + b をして返すプログラムでの結果です。 以下の結果では、start -> rslt -> end の順番に表示されており、何ら普通のプログラム言語と変わらない結果です。a_plus_b()の関数で、3秒のスリープが入っていますから、startとrsltの間に３秒の時差があります。

~~~
$ ./t1_callback.js
2017-6-8 00:56:12 start 
2017-6-8 00:56:15 rslt= 3
2017-6-8 00:56:15 end 
~~~

## I/Oが発生する場合の実行順序
次の実行結果は、a_plus_b()の処理は、RESTサービスのサーバーへ足し算処理をリクエストして、結果を得た場合の実行順番です。 なんと！？ start -> end -> rslt の順番に実行されています。 驚いた事に、プログラムが順番通りに実行されていません。  ノンブロッキングI/O とは、外部への入出力のコードの完了を待たずに、メモリ内で完結する処理を実行するという実行モデルです。 a_plus_b()内部で、ネットワーク上のRESTサービスに、処理を要求して結果を得るというI/Oが入っているため、このa_plus_b()が終了する前に、endが表示される結果となります。

~~~
$ ./t4_rest_client.js
2017-6-8 00:59:11 start 
2017-6-8 00:59:11 end 
2017-6-8 00:59:14 rslt= 3
~~~

# 確認に使ったコード

## メモリ上で完結するコード
時刻を表示するための追加がありますが、基本は同じです。 関数 a_plus_b()の引数の３番目の callback は、この部分にコールバック関数を書くことを意味しています。そして、callback 関数の結果は、return ではなく、callback(戻り値) で結果を返します。

~~~file:t1_callback.js
x = require("sleep");
function a_plus_b(a,b,callback) {
    ans = a + b;
    x.sleep(3);
    callback(ans);
}

console.log(new Date().toLocaleString(),"start ");
a_plus_b(1,2,function(rslt) {
    console.log(new Date().toLocaleString(), "rslt=", rslt);
});
console.log(new Date().toLocaleString(), "end ");
~~~

ここで、callback の代わりに、return を書いたら、どんな結果になるか見てみましょう。 その場合のスニペットは以下になります。

~~~
function a_plus_b(a,b,callback) {
    ans = a + b;
    x.sleep(3);
    return ans;
}
~~~
では、結果を見てみましょう。 次の様に、a_plus_b()の引数の３番目のfunctionで定義されるコールバック関数は、実行されていません。 つまり、呼び出し先の関数でcallback をコールすることで、呼び出し元のコールバック関数を呼び出すことになっています。

~~~
$ ./t1_callback.js
2017-6-8 01:18:38 start 
2017-6-8 01:18:41 end 
~~~
もう一つ変数スコープについて見て見ます。 コールバック関数で受け取る値は、その後のコードで利用できるのでしょうか？　答えはノーです。確認のスニペットは以下です。

~~~
console.log("start ");
var rslt;
a_plus_b(1,2,function(rslt) {
    console.log("rslt=", rslt);
});

console.log("end ", rslt);
~~~
結果は、次の様になります。 endを表示する部分は、最後に実行されているのですが、この結果からコールバック関数の戻り値は、undefined となっており、コールバックの戻り変数は、その内だけでしか利用できないことがわかります。

~~~
$ ./t1_callback.js
2017-6-8 01:26:42 start 
2017-6-8 01:26:45 rslt= 3
2017-6-8 01:26:45 end  undefined
~~~

## ノンブロッキングI/Oを試す RESTのコード
以下のコードでは、RESTのサーバーにJSON形式のデータをPOSTして、結果をJSONで受け取るクライアントのコードです。RESTサーバーにPOSTするという行為がI/Oとなります。

~~~
var http = require("http");
var querystring = require("querystring");
var StringDecoder = require('string_decoder').StringDecoder;
var decoder = new StringDecoder('utf8');

function a_plus_b( a, b, callback) {
    var json_data = {
	a: a,
	b: b
    }
    var post_data = querystring.stringify(json_data);
    var options = {
	hostname: '127.0.0.1',
	port: 3000,
	path: '/acc',
	method: 'POST',
	headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
            'Content-Length': Buffer.byteLength(post_data)
	}
    };
    var req = http.request(options, function(res) {
	res.on('data', function(chunk){
            var rcv_text = querystring.parse(decoder.write(chunk))
            var rcv_json_text = JSON.stringify(rcv_text);
            var rcv_json = JSON.parse(rcv_json_text);
            //console.log("json result = ", rcv_json.ans);
	    callback(rcv_json.ans);
	});
	res.on('end',function(err){});
    });
    req.write(post_data);
    req.end();
}

// メイン
console.log(new Date().toLocaleString(),"start ");
a_plus_b(1,2,function(rslt) {
    console.log(new Date().toLocaleString(), "rslt=", rslt);
});
console.log(new Date().toLocaleString(), "end ");
~~~
次は、足し算を実行する最小のRESTサーバーです。 受けたJSON形式のデータを足し算して、返信しています。

~~~
x = require("sleep");
var http = require('http');
var querystring = require("querystring");
var StringDecoder = require('string_decoder').StringDecoder;
var decoder = new StringDecoder('utf8');
var server = http.createServer();

server.on('request',function(req,res) {
    req.on('data',function(chunk) {
	x.sleep(3);
        var rcv_data = querystring.parse(decoder.write(chunk))
        var rcv_text = JSON.stringify(rcv_data);
        var rcv_json = JSON.parse(rcv_text);
	var ans_json = { ans: parseInt(rcv_json.a) + parseInt(rcv_json.b)};
        res.writeHead(200,{'Content-Type': 'application/json'});
	res.write(querystring.stringify(ans_json));
        res.end();
    });
});
server.listen(3000);
~~~

## まとめ
コールバック関数でも、「I/Oが発生する場合のみ」という事も頭の片隅に覚えておく必要がありますね。