LoginSignup
2
2

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-06-07

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(戻り値) で結果を返します。

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()の引数の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が発生する場合のみ」という事も頭の片隅に覚えておく必要がありますね。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2