Node.js道場1stシーズン課題プレイバック（序の段）

この投稿は「[Node.js入門](http://ascii.asciimw.jp/books/books/detail/978-4-04-870367-3.shtml)」の著者の一人である、[shigeki@github](http://qiita.com/users/shigeki@github)さんが師範となり開催された「Node.js道場」での鍛錬の模様を記したものです。

#### 目次的なもの

- 序の段　<-　イマココ！
- [弐の段：前編](http://qiita.com/items/f4fe9b1573e30d087f16)
- [弐の段：後編](http://qiita.com/items/d32732387f7322819e02)

## TCPからはじまるHTTP

```
John: やあ、Michael。見て！見て！このNode.jsすごいんだ。
      http.createServer だけでサーバが立っちゃうんだぜ！
  
Michael: マジかよ？クレイジーでクールだぜ！
```

Node.jsはhttpサーバを作ることが当初の目的だったそうで、我々の修行は、簡単にhttpサーバが作れてしまう`http`モジュールの大元である`net`モジュール（TCP通信を扱う）からはじまりました。「[Node.js入門 第9章](http://ascii.asciimw.jp/books/books/detail/978-4-04-870367-3.shtml)」にあたります。

道場では`keep-alive`な接続時の注意点などが師範より示されました。私の人生も`FIN`パケットを送られないように、`keep-alive`して行きたいと思いを新たにしました。

そして出された鍛錬の課題が、下記であります。

ご興味のある方は、ご自身でやってみてください。回答例もこの投稿内にありますが、長すぎて読めないので、うっかりスクロールして回答例を見てしまっても大丈夫ですし、読みながら自分でもやってみるなんてのも良いでしょう。

## 課題

A君は、ブラウザで1回閲覧したら HelloWorld のWebサーバが終了するプログラムを作ろうと hello.js を作りました。 だけど Chrome でアクセスすると下のようなエラーになってしまい、プログラムが正常に終了しません。

```js:hello.js
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)
```

### 課題１

なぜエラーが発生したのかその理由を記述しなさい。

### 課題２

このプログラムを修正してエラーが発生せずA君がやりたかったプログラムを作りなさい。

## 回答

まず、エラーが再現することを確認します。ブラウザを変えたりしてみましょう。

Chromeでは確かに再現しますが、例えばSafariではエラーが再現しません。また、エラーが出ないブラウザでアクセスした場合、サーバがすぐには終了しません。一度アクセスすれば、レスポンス時に`server.close()`しているので、サーバが閉じてNodeのプロセスも終了するはずです。

つまり、想定外の挙動がニ点起きていることがわかります。

1. Chromeで`Not running`エラーが出る
2. `server.close()`しているのに、すぐにはサーバが閉じない

## 原因を探る

「Not running」ということで、ピンとくるニュータイプな方もいらっしゃるでしょうが、ひとまず`net`モジュールのソースコードを見てみましょう。

該当部分のコードは、

```js:node/lib/net.js
// 大幅に省略して転載
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`プロパティには、どのパスへのアクセスかが記録されています。

```js:hello2.js
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()`の[ドキュメント](http://nodejs.org/api/http.html#http_server_close_callback)を見てみましょう。

> Stops the server from accepting new connections. See net.Server.close().
> サーバが新しいコネクションを受け付けるのを停止します。net.Server.close()を参照。

とあります。我々の修行が、`net`モジュールからはじまった理由がわかっていただけるかと思います。

`net.Server.close()`の[ドキュメント](http://nodejs.org/api/http.html#http_server_close_callback)では、

> 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君のコードの問題点は、

1. リクエストのたびに起動する`http.createServer()`のコールバックに`server.close()`を記述している
2. 普通、ブラウザは`keep-alive`な接続をするので、`server.close()`しても、すぐにはサーバが終了しない

の二点となります。

下記のように、レスポンスヘッダに「Connection: close」を指定すると、先ほどtelnetで`HTTP 1.0`でアクセスしたのと同じ状態になるので、エラーも発生せず、Nodeのプロセスも直ちに終了します。

```js:hello3.js
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/');
});
```

ただし、このままでは、複数のクライアントから同時に接続があり、処理が

1. クライアントAからコネクションを受け付ける
2. クライアントBからコネクションを受け付ける
3. クライアントAからリクエストを受け付ける -> sever.close()
4. クライアントBからリクエストを受け付ける -> sever.close()

の順番になると、やはりエラーが発生します。そのような場合、

1. クライアントAからコネクションを受け付ける -> sever.close()
2. クライアントBからコネクションは受け付けない
3. クライアントAからリクエストを受け付ける

となるように`server.close()`のタイミングを変えたほうが良いことになります。

そのためには、クライアントからコネクションを受け付けたタイミングで発火する`connection`イベントのコールバックに`server.close()`を記述することになります。

つまり、最終的に、このようなコードになります。

```js:hello4.js
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」を追加するかわりに、

1. `FIN`パケットを送り、コネクションをハーフクローズする`socket.end()`を使う
2. 書き込みバッファがなくなったらコネクションを破棄する`socket.destroySoon()`を使う（マニュアル非記載）
3. コネクションを破棄する`socket.destroy()`を使う

といった方法があります。

今回のコードで使う場合、リクエストに使われた`socket`オブジェクトは、`http.createServer`のコールバックの1個目の引数に渡される`http.ServerRequest`オブジェクトの`connection`プロパティからアクセスできるので、

```js
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入門](http://ascii.asciimw.jp/books/books/detail/978-4-04-870367-3.shtml)」の執筆について、「マニュアルの裏側を見せる」「マニュアルは中身がわからない。仕様が書いてあるだけ」「なぜこの仕様なのかを意識して書いた」と述懐された師範ならではの **マニュアル非記載** を盛り込んだ説明でございます。

## まとめ：「1stシーズン序の段」における師範の教え

### 一つ、モジュールの使いかたを覚えることがNodeの使いかたを覚えることになる
### 一つ、どのイベントで、どのメソッドを使うとロバストになるのかを意識せよ
### 一つ、マニュアルだけではわからないことがある