JavaScript
Node.js
Express.js

[Node.js] Webサーバ+アプリ構築が速すぎる件 〜 JSおくのほそ道 #004

More than 1 year has passed since last update.

こんにちはほそ道です。

今回はnodeでつくるWebアプリケーションについて細い道をまさぐっていきます。

目次はこちら


プレーンなWebページ

しのごの言う前にやってみましょう

下記のjsを作って実行してみます。


hosoweb.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');

それではブラウザでアクセスしてみます。

スクリーンショット 2014-05-15 0.41.58.png

出ました。

Webアプリといっておきつつ、只のページになっちゃっている何も無いページですが軽く引くくらいの速さで作れます。

httpというモジュールをロードして、ポートをリスニングしているんですね。

特筆すべきは上記jsファイル以外に、apacheもnginxもtomcatも設定ファイルも何も用いていません。

外部の仕組みで作られたページや数値、グラフなんかを閲覧するくらいのページ作りにはもってこいではないでしょうか。

逆にここまでプレーンの状態だとしっかりしたアプリケーションを作り込むのは爆速とは行かないでしょうが。


Expressフレームワーク

今度はフレームワークを使ってもうちょっとアプリケーションぽい構造のWebを作ってみます。

まずはプロジェクトディレクトリを作成し、その中で前回取り上げたnpmでフレームワークモジュールをローカルインストールします。


shell

mkdir hosoweb

cd hosoweb
npm install express
npm install express-generator

続いて骨組みを作ります。コマンド一発です。


shell

$ ./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アプリが作れそうですね。


hosoweb構造

.

├── app.js
├── bin
│   └── www
├── package.json
├── public
│   ├── images
│   ├── javascripts
│   └── stylesheets
│   └── style.css
├── routes
│   ├── index.js
│   └── users.js
└── views
├── error.jade
├── index.jade
└── layout.jade

それでは起動してみましょう


shell

npm install

npm start

続いてブラウザアクセス。

スクリーンショット 2014-05-15 1.11.25.png

出ました!1分とかかりません。

当然、ここからそれなりのWebアプリケーションに作り込むのは時間がかかりますが。

とりあえず入り口のハードルがここまで低いのは珍しいんではないかと思う訳です。

この先は各種設定やルーティングルール、テンプレートの活用等あるんですが、特に触れません。

今回の本筋は スタートアップが爆速!! という所なので。


いったんまとめ

というわけでさくっと作れるWebアプリケーション。

ほそ道的にはExpressを使ってない生々しい方のコードに惹かれました。

だって実装ファイル一個だけ、しかも5行くらいでおわっちゃうんですもん!

そんで本当にちょっとしたWeb作りたいだけの時に、色々環境周りで苦慮するの面倒くさいじゃないですか!

ココから先は奥の細道編という事でhttpモジュールを追跡してみました。

長いですので興味ない人はここまででお疲れさまでした!

次回はNodeとりあえず最終章のソケット通信を扱う予定です。


【奥の細道】httpモジュールの実体に興味ある

ほそ道的にはこのhttpモジュールが超気になりどころです。

なんでかというとプログラマーにココまで必要最小限のシンプルな構造を提供できるってことにちょっと感動したので、

中はどうなっているのか、

結局外部のサーバウェアを取り込んだりしているのか?

とか気になって夜も眠れません。

というわけで、改めて今回記事の一番最初のサンプルを見てみます。


hosoweb.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');

うーむ、記述量が少ない。

ワタクシがそそられたのはコールバックの外側、下記の2つ



  • http.createServerで生成されている Server


  • Serverが呼び出す listen関数

どういう役割分担で動いているんでしょう?


HTTPサーバを追跡する

今回のnodeバージョンはv0.10.26でしたので、同バージョンのソースをダウンロードし、中を見てみる事にしました。

http.createServerについては下記のようなコーディング


lib/http.js[1

exports.Server = Server;

exports.createServer = function(requestListener) {
return new Server(requestListener);
};

Serverオブジェクトを返してますね。

このServerは同じファイル内で下記のように宣言されてます。


lib/http.js[2

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を参照してみます。


lib/net.js[1

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で探してみたらおりました。


lib/net.js[2

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関数...!?うわめんどくさ、と思いつつさらにほじくります。


lib/net.js[3


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(...);のところが深みを感じさせます。

そろそろ疲れてきました。

が、ここまで来て負けられません!

というわけで突入!


lib/net.js[4

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もみてみます。


lib/net.js[5

function createTCP() {

var TCP = process.binding('tcp_wrap').TCP;
return new TCP();
}

おぉ、process.bindingにたどり着きました。

そんなモジュールないぜとググったところ、この先はC++実装らしいんですよ。

ここはjsブログなんですが旅は道連れ。ちょっとだけ見てみましょう。

おそらくこのTCPがbind変数を持っているということですね。


src/tcp_wrap.cc

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++での実装になる