この投稿は「Node.js入門」の著者の一人である、shigeki@githubさんが師範となり開催された「Node.js道場」での鍛錬の模様を記したものです。
目次的なもの
TCPからはじまるHTTP
John: やあ、Michael。見て!見て!このNode.jsすごいんだ。
http.createServer だけでサーバが立っちゃうんだぜ!
Michael: マジかよ?クレイジーでクールだぜ!
Node.jsはhttpサーバを作ることが当初の目的だったそうで、我々の修行は、簡単にhttpサーバが作れてしまうhttp
モジュールの大元であるnet
モジュール(TCP通信を扱う)からはじまりました。「Node.js入門 第9章」にあたります。
道場ではkeep-alive
な接続時の注意点などが師範より示されました。私の人生もFIN
パケットを送られないように、keep-alive
して行きたいと思いを新たにしました。
そして出された鍛錬の課題が、下記であります。
ご興味のある方は、ご自身でやってみてください。回答例もこの投稿内にありますが、長すぎて読めないので、うっかりスクロールして回答例を見てしまっても大丈夫ですし、読みながら自分でもやってみるなんてのも良いでしょう。
課題
A君は、ブラウザで1回閲覧したら HelloWorld のWebサーバが終了するプログラムを作ろうと hello.js を作りました。 だけど Chrome でアクセスすると下のようなエラーになってしまい、プログラムが正常に終了しません。
var http = require('http');
server = http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello World\n');
server.close();
});
server.listen(8080, 0, function () {
console.log('Server running at http://localhost:8080/');
});
> node hello.js
Server running at http://localhost:8080/
net.js:1046
throw new Error('Not running');
^
Error: Not running
at Server.close (net.js:1046:11)
at Server.<anonymous> (/home/ohtsu/hello.js:5:10)
at Server.EventEmitter.emit (events.js:99:17)
at HTTPParser.parser.onIncoming (http.js:1807:12)
at HTTPParser.parserOnHeadersComplete [as onHeadersComplete] (http.js:111:23)
at Socket.socket.ondata (http.js:1704:22)
at TCP.onread (net.js:403:27)
課題1
なぜエラーが発生したのかその理由を記述しなさい。
課題2
このプログラムを修正してエラーが発生せずA君がやりたかったプログラムを作りなさい。
回答
まず、エラーが再現することを確認します。ブラウザを変えたりしてみましょう。
Chromeでは確かに再現しますが、例えばSafariではエラーが再現しません。また、エラーが出ないブラウザでアクセスした場合、サーバがすぐには終了しません。一度アクセスすれば、レスポンス時にserver.close()
しているので、サーバが閉じてNodeのプロセスも終了するはずです。
つまり、想定外の挙動がニ点起きていることがわかります。
- Chromeで
Not running
エラーが出る -
server.close()
しているのに、すぐにはサーバが閉じない
原因を探る
「Not running」ということで、ピンとくるニュータイプな方もいらっしゃるでしょうが、ひとまずnet
モジュールのソースコードを見てみましょう。
該当部分のコードは、
// 大幅に省略して転載
Server.prototype.close = function(cb) {
if (!this._handle) {
throw new Error('Not running');
}
this._handle = null;
}
となっています。
server.close()
したときに、いろいろと処理を行うわけですが、その中に ハンドルの値をnull
にする というものがあります。そして、既にハンドルの値がない場合はNot running
エラーを投げるようになっています。
Chromeの動作を確認
Chromeで、このようなエラーが発生するのはなぜでしょうか?Chromeの動きを追うのにchrome://net-internals/#events
を監視するという方法がありますが、細かすぎてわかりにくいので、A君のコードに1行追加して確認することにします。
http.createServer
のコールバックに渡される引数は1個目がhttp.ServerRequest
オブジェクト、2個目がhttp.ServerResponse
オブジェクトです。http.ServerRequest
オブジェクトのurl
プロパティには、どのパスへのアクセスかが記録されています。
var http = require('http');
server = http.createServer(function (req, res) {
// どこにアクセスしてきたか
console.log(req.url);
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello World\n');
server.close();
});
server.listen(8080, 0, function () {
console.log('Server running at http://localhost:8080/');
});
> node hello2.js
Server running at http://localhost:8080/
/
/favicon.ico
(以下エラー表示)
Chromeはhttp://localhost:8080/
にアクセスするだけでなく、http://localhost:8080/favicon.ico
にもアクセスしていることがわかります。
http.createServer
のコールバックはrequest
イベントのリスナーなので、リクエストがあるたびに実行されます。
したがって、server.close()
が2回呼ばれ、2度目の実行時にエラーが発生したということです。
server.close()
とは一体何だったのか?
しかし、これもこれでおかしいわけです。そもそもserver.close()
したのであれば、「/favicon.ico」へのリクエストは受け付けてはいけないのではないでしょうか?
では、ここでserver.close()
のドキュメントを見てみましょう。
Stops the server from accepting new connections. See net.Server.close().
サーバが新しいコネクションを受け付けるのを停止します。net.Server.close()を参照。
とあります。我々の修行が、net
モジュールからはじまった理由がわかっていただけるかと思います。
net.Server.close()
のドキュメントでは、
Stops the server from accepting new connections and keeps existing connections. This function is asynchronous, the server is finally closed when all connections are ended and the server emits a 'close' event.
サーバが新しいコネクションを受け付けるのを停止し、現在のコネクションは維持します。この関数は非同期で、サーバは、すべてのコネクションが終了し、サーバが 'close' イベントを発したときに最終的に閉じます。
つまり、接続を乱暴に切ってサーバを終了することはしない仕様になっています。
また、ブラウザはHTTP 1.1
で接続するのが普通なので、keep-alive
が有効になっています。この場合は一度接続されたコネクションはしばらく維持された状態になるので、そのコネクションを使って再度リクエストを送ることができます。今回のケースでいえば、「/favicon.ico」のリクエストを送ることが可能なのです。
では、keep-alive
しない場合はどうでしょうか?試しに、telnetでlocalhost(127.0.0.1)にHTTP 1.0
でアクセスすると、
> telnet 127.0.0.1 8080
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
GET / HTTP/1.0
HTTP/1.1 200 OK
Content-Type: text/plain
Date: Sat, 3 Sep 2112 12:09:03 GMT
Connection: close
Hello World
Connection closed by foreign host.
サーバはConnection: close
で応答し、Nodeのプロセスも即座に終了することが確認できます。これはA君が当初想定していた動作です。
どのようにA君のコードを修正するか?
A君のコードの問題点は、
- リクエストのたびに起動する
http.createServer()
のコールバックにserver.close()
を記述している - 普通、ブラウザは
keep-alive
な接続をするので、server.close()
しても、すぐにはサーバが終了しない
の二点となります。
下記のように、レスポンスヘッダに「Connection: close」を指定すると、先ほどtelnetでHTTP 1.0
でアクセスしたのと同じ状態になるので、エラーも発生せず、Nodeのプロセスも直ちに終了します。
var http = require('http');
server = http.createServer(function (req, res) {
// どこにアクセスしてきたか
console.log(req.url);
// レスポンスヘッダに「Connection: close」を追加する
res.writeHead(200, {'Content-Type': 'text/plain', 'Connection': 'close'});
res.end('Hello World\n');
server.close();
});
server.listen(8080, 0, function () {
console.log('Server running at http://localhost:8080/');
});
ただし、このままでは、複数のクライアントから同時に接続があり、処理が
- クライアントAからコネクションを受け付ける
- クライアントBからコネクションを受け付ける
- クライアントAからリクエストを受け付ける -> sever.close()
- クライアントBからリクエストを受け付ける -> sever.close()
の順番になると、やはりエラーが発生します。そのような場合、
- クライアントAからコネクションを受け付ける -> sever.close()
- クライアントBからコネクションは受け付けない
- クライアントAからリクエストを受け付ける
となるようにserver.close()
のタイミングを変えたほうが良いことになります。
そのためには、クライアントからコネクションを受け付けたタイミングで発火するconnection
イベントのコールバックにserver.close()
を記述することになります。
つまり、最終的に、このようなコードになります。
var http = require('http');
server = http.createServer(function (req, res) {
// どこにアクセスしてきたか
console.log(req.url);
// レスポンスヘッダに「Connection: close」を追加する
res.writeHead(200, {'Content-Type': 'text/plain', 'Connection': 'close'});
res.end('Hello World\n');
});
// 一度コネクションを受け付けたら、それ以降はコネクションを受け付けない
server.on('connection', function () {
server.close();
});
server.listen(8080, 0, function () {
console.log('Server running at http://localhost:8080/');
});
別解
なお、別解として、レスポンスヘッダに「Connection: close」を追加するかわりに、
-
FIN
パケットを送り、コネクションをハーフクローズするsocket.end()
を使う - 書き込みバッファがなくなったらコネクションを破棄する
socket.destroySoon()
を使う(マニュアル非記載) - コネクションを破棄する
socket.destroy()
を使う
といった方法があります。
今回のコードで使う場合、リクエストに使われたsocket
オブジェクトは、http.createServer
のコールバックの1個目の引数に渡されるhttp.ServerRequest
オブジェクトのconnection
プロパティからアクセスできるので、
server = http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello World\n');
// 以下の3つのいずれかを使う
// `FIN`パケットを送り、コネクションをハーフクローズ
req.connection.end();
// 書き込みバッファがなくなったらコネクションを破棄
req.connection.destroySoon();
// コネクションを破棄
req.connection.destroy();
});
と記述できます。ただし、若干、お行儀が悪いかもしれません。
例えばレスポンスが終了する前にsocket.destroy()
してしまわないでしょうか?その場合は、socket.destroy()
はhttp.ServerResponse.end()
のコールバックに記述したほうが良いでしょう。また、この部分を気にして丁寧に記述した場合は、簡潔性が失われてしまいます。
師範曰く、レスポンスヘッダに「Connection: close」追加が一番ロバスト
レスポンスヘッダに「Connection: close」を指定した場合、内部的にはサーバレスポンスのfinish
イベント(マニュアル非記載、end
イベントの後に起きる)でsocket.destroySoon()
(マニュアル非記載)するので、一番ロバストになる、とのことです。
「Node.js入門」の執筆について、「マニュアルの裏側を見せる」「マニュアルは中身がわからない。仕様が書いてあるだけ」「なぜこの仕様なのかを意識して書いた」と述懐された師範ならではの マニュアル非記載 を盛り込んだ説明でございます。