Help us understand the problem. What is going on with this article?

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

More than 3 years have 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++での実装になる
hosomichi
粋になりたい
fringe81
Fringeは、最新のテクノロジーとプロフェッショナルによるサービスにより、社会課題に仮説を立てて市場に広げていくことで、数十年という長期的なスパンで価値を生み出し続け、より良い世界を創る集団です。 既存の領域に限らず、時流を読み、仮説を生み出し、テクノロジーの力で優れたサービスを生み出し続けます。
https://www.fringe81.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした