Node.js の コールバック関数とノンブロッキングI/O の動作について、サンプルプログラムを書いて確認したメモです。
確認する問題
I/Oが発生する場合とメモリ上で完結する場合では、全く動作が異なるため注意しなければならない。 この注意点をプログラムの動作で判る様に書いたのが、次のスニペットです。これは a_plus_b() という 第一引数と第二引数を加えた結果を返す単純なコードです。 このコードを使って、ノンブロッキングI/Oの動作を確認します。
実行順番がわかる様に、最初と最後に start と end を表示して、計算結果を得るコールバック関数で結果の表示をします。このa_plus_b() の実装を変えて、実行順番を確認します。 a_plus_b()の中は、一瞬で終わるので、3秒のスリープを入れて実行時刻が明確に判る様にします。
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の間に3秒の時差があります。
$ ./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()の引数の3番目の callback は、この部分にコールバック関数を書くことを意味しています。そして、callback 関数の結果は、return ではなく、callback(戻り値) で結果を返します。
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()の引数の3番目の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が発生する場合のみ」という事も頭の片隅に覚えておく必要がありますね。