こんにちはほそ道です。
今回はnodeでつくるWebアプリケーションについて細い道をまさぐっていきます。
プレーンなWebページ
しのごの言う前にやってみましょう
下記のjsを作って実行してみます。
var http = require('http');
http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello Hosomichi!\n');
}).listen(1337, '127.0.0.1');
それではブラウザでアクセスしてみます。
出ました。
Webアプリといっておきつつ、只のページになっちゃっている何も無いページですが軽く引くくらいの速さで作れます。
http
というモジュールをロードして、ポートをリスニングしているんですね。
特筆すべきは上記jsファイル以外に、apacheもnginxもtomcatも設定ファイルも何も用いていません。
外部の仕組みで作られたページや数値、グラフなんかを閲覧するくらいのページ作りにはもってこいではないでしょうか。
逆にここまでプレーンの状態だとしっかりしたアプリケーションを作り込むのは爆速とは行かないでしょうが。
Expressフレームワーク
今度はフレームワークを使ってもうちょっとアプリケーションぽい構造のWebを作ってみます。
まずはプロジェクトディレクトリを作成し、その中で前回取り上げたnpmでフレームワークモジュールをローカルインストールします。
mkdir hosoweb
cd hosoweb
npm install express
npm install express-generator
続いて骨組みを作ります。コマンド一発です。
$ ./node_modules/.bin/express
destination is not empty, continue? yes
create : .
create : ./package.json
create : ./app.js
create : ./public
create : ./public/javascripts
create : ./public/images
create : ./public/stylesheets
create : ./public/stylesheets/style.css
create : ./routes
create : ./routes/index.js
create : ./routes/users.js
create : ./views
create : ./views/index.jade
create : ./views/layout.jade
create : ./views/error.jade
create : ./bin
create : ./bin/www
install dependencies:
$ cd . && npm install
run the app:
$ DEBUG=node_http ./bin/www
下記の様な構造が出来上がります。
おぉ、なんかまともなWebアプリが作れそうですね。
.
├── app.js
├── bin
│ └── www
├── package.json
├── public
│ ├── images
│ ├── javascripts
│ └── stylesheets
│ └── style.css
├── routes
│ ├── index.js
│ └── users.js
└── views
├── error.jade
├── index.jade
└── layout.jade
それでは起動してみましょう
npm install
npm start
続いてブラウザアクセス。
出ました!1分とかかりません。
当然、ここからそれなりのWebアプリケーションに作り込むのは時間がかかりますが。
とりあえず入り口のハードルがここまで低いのは珍しいんではないかと思う訳です。
この先は各種設定やルーティングルール、テンプレートの活用等あるんですが、特に触れません。
今回の本筋は スタートアップが爆速!! という所なので。
いったんまとめ
というわけでさくっと作れるWebアプリケーション。
ほそ道的にはExpressを使ってない生々しい方のコードに惹かれました。
だって実装ファイル一個だけ、しかも5行くらいでおわっちゃうんですもん!
そんで本当にちょっとしたWeb作りたいだけの時に、色々環境周りで苦慮するの面倒くさいじゃないですか!
ココから先は奥の細道編という事でhttpモジュールを追跡してみました。
長いですので興味ない人はここまででお疲れさまでした!
次回はNodeとりあえず最終章のソケット通信を扱う予定です。
【奥の細道】httpモジュールの実体に興味ある
ほそ道的にはこのhttp
モジュールが超気になりどころです。
なんでかというとプログラマーにココまで必要最小限のシンプルな構造を提供できるってことにちょっと感動したので、
中はどうなっているのか、
結局外部のサーバウェアを取り込んだりしているのか?
とか気になって夜も眠れません。
というわけで、改めて今回記事の一番最初のサンプルを見てみます。
var http = require('http');
http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello Hosomichi!\n');
}).listen(1337, '127.0.0.1');
うーむ、記述量が少ない。
ワタクシがそそられたのはコールバックの外側、下記の2つ
-
http.createServer
で生成されている Server -
Server
が呼び出す listen関数
どういう役割分担で動いているんでしょう?
HTTPサーバを追跡する
今回のnodeバージョンはv0.10.26でしたので、同バージョンのソースをダウンロードし、中を見てみる事にしました。
http.createServerについては下記のようなコーディング
exports.Server = Server;
exports.createServer = function(requestListener) {
return new Server(requestListener);
};
Serverオブジェクトを返してますね。
このServerは同じファイル内で下記のように宣言されてます。
function Server(requestListener) {
if (!(this instanceof Server)) return new Server(requestListener);
net.Server.call(this, { allowHalfOpen: true });
if (requestListener) {
this.addListener('request', requestListener);
}
this.httpAllowHalfOpen = false;
this.addListener('connection', connectionListener);
this.addListener('clientError', function(err, conn) {
conn.destroy(err);
});
this.timeout = 2 * 60 * 1000;
}
util.inherits(Server, net.Server);
なるほどリクエストを受け取るとServerの引数のコールバックが実行されるようにしてるっぽいですね。
ところで、このモジュールにlisten関数はいませんねぇ。
そして、util.inherits(Server, net.Server);
とありまして、これは継承的な動きをしている模様。
Serverオブジェクトはnet.Server
の振る舞いを継承すると。
本当の実体はnet.Server
...ということでnet.jsを参照してみます。
function Server(/* [ options, ] listener */) {
if (!(this instanceof Server)) return new Server(arguments[0], arguments[1]);
events.EventEmitter.call(this);
var self = this;
var options;
if (typeof arguments[0] == 'function') {
options = {};
self.on('connection', arguments[0]);
} else {
options = arguments[0] || {};
if (typeof arguments[1] == 'function') {
self.on('connection', arguments[1]);
}
}
this._connections = 0;
Object.defineProperty(this, 'connections', {
get: util.deprecate(function() {
if (self._usingSlaves) {
return null;
}
return self._connections;
}, 'connections property is deprecated. Use getConnections() method'),
set: util.deprecate(function(val) {
return (self._connections = val);
}, 'connections property is deprecated. Use getConnections() method'),
configurable: true, enumerable: true
});
this._handle = null;
this._usingSlaves = false;
this._slaves = [];
this.allowHalfOpen = options.allowHalfOpen || false;
}
util.inherits(Server, events.EventEmitter);
exports.Server = Server;
ミョーにNode.jsっぽすぎるというかシンプルなイベント駆動オブジェクトの様相ですねぇ。
ネットワーク周りのコアな部分は持ってないかんじ。
は!
という事はServerオブジェクトについてはhttpやtcpのお作法をガンガン定義して
イベントとコールバックの管理とハンドリングを行うのがメインタスクってことっぽいですね。
つまり、ネットワーク経由でパケットが入ってきてからがお仕事ってことか。
Server.listen関数を追跡
ということはネットワーク周りの受付のコアなお仕事を行っているのはlisten
関数様と予想!
net.jsで探してみたらおりました。
function listen(self, address, port, addressType, backlog, fd) {
if (!cluster) cluster = require('cluster');
if (cluster.isMaster) {
self._listen2(address, port, addressType, backlog, fd);
return;
}
cluster._getServer(self, address, port, addressType, fd, function(handle,
err) {
// EACCESS and friends
if (err) {
self.emit('error', errnoException(err, 'bind'));
return;
}
if (port && handle.getsockname && port != handle.getsockname().port) {
self.emit('error', errnoException('EADDRINUSE', 'bind'));
return;
}
self._handle = handle;
self._listen2(address, port, addressType, backlog, fd);
});
}
clusterモジュールを呼んでますね。これは複数プロセスを作る場合に使ったりするらしい。
isMaster
で分岐しているのでマスタースレーブ構成向けですかね。
もちろん今回は一台構成なので「マスター」の分岐に入るはず。
_listen2
関数...!?うわめんどくさ、と思いつつさらにほじくります。
Server.prototype._listen2 = function(address, port, addressType, backlog, fd) {
debug('listen2', address, port, addressType, backlog);
var self = this;
var r = 0;
if (!self._handle) {
debug('_listen2: create a handle');
self._handle = createServerHandle(address, port, addressType, fd);
if (!self._handle) {
var error = errnoException(process._errno, 'listen');
process.nextTick(function() {
self.emit('error', error);
});
return;
}
} else {
debug('_listen2: have a handle already');
}
self._handle.onconnection = onconnection;
self._handle.owner = self;
r = self._handle.listen(backlog || 511);
if (r) {
var ex = errnoException(process._errno, 'listen');
self._handle.close();
self._handle = null;
process.nextTick(function() {
self.emit('error', ex);
});
return;
}
this._connectionKey = addressType + ':' + address + ':' + port;
process.nextTick(function() {
self.emit('listening');
});
};
前々回に触れた
nextTick
を呼んでますのでイベントループをまわしてますね。それっぽくなってきた。
あとはself._handle = createServerHandle(...);
のところが深みを感じさせます。
そろそろ疲れてきました。
が、ここまで来て負けられません!
というわけで突入!
var createServerHandle = exports._createServerHandle =
function(address, port, addressType, fd) {
var r = 0;
// assign handle in listen, and clean up if bind or listen fails
var handle;
if (typeof fd === 'number' && fd >= 0) {
try {
handle = createHandle(fd);
}
catch (e) {
// Not a fd we can listen on. This will trigger an error.
debug('listen invalid fd=' + fd + ': ' + e.message);
process._errno = 'EINVAL'; // hack, callers expect that errno is set
return null;
}
handle.open(fd);
handle.readable = true;
handle.writable = true;
return handle;
} else if (port == -1 && addressType == -1) {
handle = createPipe();
if (process.platform === 'win32') {
var instances = parseInt(process.env.NODE_PENDING_PIPE_INSTANCES);
if (!isNaN(instances)) {
handle.setPendingInstances(instances);
}
}
} else {
handle = createTCP();
}
if (address || port) {
debug('bind to ' + address);
if (addressType == 6) {
r = handle.bind6(address, port);
} else {
r = handle.bind(address, port);
}
}
if (r) {
handle.close();
handle = null;
}
return handle;
};
そろそろ深淵に近づいてきた気がします。
最初のif分岐について今回のサンプルコードはfd
引数を指定していないので
下の方のhandle = createTCP();
に入って行くはず。
そして次のif分岐はr = handle.bind(address, port);
に入るはず。
addressType
変数で分岐してますがIPアドレスがv4かv6で分岐している模様。
文脈を読むとcreateTCP
でつくられたTCPがホスト情報をbind
するぜってことなんでしょうね。
このbind
のところでサーバが起動していると予測。
というわけでcreateTCP
もみてみます。
function createTCP() {
var TCP = process.binding('tcp_wrap').TCP;
return new TCP();
}
おぉ、process.binding
にたどり着きました。
そんなモジュールないぜとググったところ、この先はC++実装らしいんですよ。
ここはjsブログなんですが旅は道連れ。ちょっとだけ見てみましょう。
おそらくこのTCPがbind変数を持っているということですね。
Handle<Value> TCPWrap::Bind(const Arguments& args) {
HandleScope scope;
UNWRAP(TCPWrap)
String::AsciiValue ip_address(args[0]);
int port = args[1]->Int32Value();
struct sockaddr_in address = uv_ip4_addr(*ip_address, port);
int r = uv_tcp_bind(&wrap->handle_, address);
// Error starting the tcp.
if (r) SetErrno(uv_last_error(uv_default_loop()));
return scope.Close(Integer::New(r));
}
Bind
関数がいました。
予想通りIPv4的なアプローチが書いてありました(ご満悦)。
uv_default_loop
のところでポートリスニングのループが始まっているんではないかと...おそらく。
長かったですがなんとなーく繋がりと役割が見えました。
まとめ
というわけで、ほそ道が得た学びを一応まとめておきます。
- Node.jsの爆速で作れるWebは完全に自家生成で構成されておる
- サーバはhttpやtcpのお作法を実装しており、listen関数がリクエストを受付/監視ループを行う
- やっぱりデバイスやOSとの連絡部分はJSではなくC++での実装になる