hakobera's blog

技術メモ。たまに雑談

Connect ソースコードリーディング(2) - 基本処理フロー

はじめに

今回は、以下の Connect を使ったコードが内部的にどのように初期化され、リクエストの処理がどのように行われていくのかを解説します。

var connect = require('connect');
var server = connect.createServer(
  connect.logger(),
  connect.static(__dirname + '/public')
);
server.listen(3000);

初期化処理フロー

Connect の初期化処理フローは以下のようになっています。

  1. require('connect') で connect 各種 export 処理
  2. createServer メソッドで HTTPServer のインスタンスを生成
  3. HTTPServer のコンストラクタで、middleware とリクエストハンドラを登録
  4. server.listen メソッドで HTTPServer を起動

require('connect') で connect 各種 export 処理

connect.js 79行目〜

// auto-load getters

exports.middleware = {};

/**
 * Auto-load bundled middleware with getters.
 */

fs.readdirSync(__dirname + '/middleware').forEach(function(filename){
  if (/\.js$/.test(filename)) {
    var name = filename.substr(0, filename.lastIndexOf('.'));
    exports.middleware.__defineGetter__(name, function(){
      return require('./middleware/' + name);
    });
  }
});

middleware はファイル名と同名の getter の export のみで、実際に呼び出された時に require されます。こうして、遅延ロードにすることで、初期化処理の高速化や不要なリソース確保を抑えています。

// expose utils

exports.utils = require('./utils');

// expose getters as first-class exports

exports.utils.merge(exports, exports.middleware);

// expose constructors

exports.HTTPServer = HTTPServer;
exports.HTTPSServer = HTTPSServer;

utils, HTTPServer, HTTPSServer は通常の export 処理です。

createServer() で HTTPServer のインスタンスを生成

http.js 65行目〜

function createServer() {
  if ('object' == typeof arguments[0]) {
    return new HTTPSServer(arguments[0], Array.prototype.slice.call(arguments, 1));
  } else {
    return new HTTPServer(Array.prototype.slice.call(arguments));
  }
};

createServer() は単純な HTTPServer のファクトリメソッドです。第1引数に SSL証明書を設定する object を指定すると HTTPSServer が生成されます。今回の例では引数は全て middleware (== 'function') なので、HTTPServer が生成されて返ります。

HTTPServer のコンストラクタで、middleware とリクエストハンドラを登録

http.js 37行目〜

var Server = exports.Server = function HTTPServer(middleware) {
  this.stack = [];
  middleware.forEach(function(fn){
    this.use(fn);
  }, this);
  http.Server.call(this, this.handle);
};

middleware を use して、継承している http.Server のコンストラクタを呼び出して、handle() メソッドをリクエストハンドラ (node/lib/http.js の引数名だと requestListener) として登録しています。

なので、実は以下の2つのコードは等価です。

var server = connect.createServer(
  connect.logger(),
  connect.errorHandler()
);
var server = connect.createServer();
server
  .use(connect.logger())
  .use(connect.errorHandler());
HTTPServer.prototype.use メソッド

Connect の middleware は HTTPServer.prototype.use メソッドに引数として呼んであげないと、リクエストを処理できません。

では、その HTTPServer.prototyep.use メソッドは何をやっているかというと、以下のようにな処理を行なっています。

http.js 91行目〜

Server.prototype.use = function(route, handle){
  this.route = '/';

  // default route to '/'
  if ('string' != typeof route) {
    handle = route;
    route = '/';
  }

  // wrap sub-apps
  if ('function' == typeof handle.handle) {
    var server = handle;
    server.route = route;
    handle = function(req, res, next) {
      server.handle(req, res, next);
    };
  }

  // wrap vanilla http.Servers
  if (handle instanceof http.Server) {
    handle = handle.listeners('request')[0];
  }

  // normalize route to not trail with slash
  if ('/' == route[route.length - 1]) {
    route = route.substr(0, route.length - 1);
  }

  // add the middleware
  this.stack.push({ route: route, handle: handle });

  // allow chaining
  return this;
};

色々やっていますが、肝となるのは以下の route (リクエストされた URL) に対応するハンドラ(middleware) を stack に登録している部分です。この stack は後ほど説明する handle メソッド利用されます。

  // add the middleware
  this.stack.push({ route: route, handle: handle });

use の第1引数の route はあまり使われているコードを見ませんが、middleware には route が設定できます(省略すると route = '/' として、登録されます)。なので、例えば以下のようにすると、/public 配下に public フォルダ、/public2 配下に public2 フォルダを静的ルーティングすることができます。

connect.createServer(
  connect.static('/public', __dirname + '/public'),
  connect.static('/public2', __dirname + '/public2')
)
リクエストハンドラの登録
http.Server.call(this, this.handle)

の部分は Node の http.js の Server コンストラクタ呼び出しです。Server のコンストラクタは以下のように実装されているので、この呼出しで Server の request イベントに HTTPServer.prototype.handle メソッドをリスナとして登録しています。これで、サーバへのリクエストがあった場合、HTTPServer.prototype.handle メソッドが呼ばれるようになります。

node/lib/http.js 1316 行目〜

function Server(requestListener) {
  if (!(this instanceof Server)) return new Server(requestListener);
  net.Server.call(this, { allowHalfOpen: true });

  if (requestListener) {
    this.addListener('request', requestListener);
  }

  // Similar option to this. Too lazy to write my own docs.
  // http://www.squid-cache.org/Doc/config/half_closed_clients/
  // http://wiki.squid-cache.org/SquidFaq/InnerWorkings#What_is_a_half-closed_filedescriptor.3F
  this.httpAllowHalfOpen = false;

  this.addListener('connection', connectionListener);
}
util.inherits(Server, net.Server);

このコードの意味が分からないという方は、Node の http.Server が EventEmitter を継承しているということと、以下の2つのコードが等価であるということがわかれば、イメージをつかめるのではないかと思います。

var http = require('http');
http.createServer(function(req, res) {
  res.writeHeader(200, { 'Content-Type': 'text/plain' });
  res.end('Hello World');
}).listen(3000);

上記は以下の省略表記

var http = require('http');
var server = http.createServer();
server.addListener('request', function(req, res) {
  res.writeHeader(200, { 'Content-Type': 'text/plain' });
  res.end('Hello World');
});
server.listen(3000);

server.listen() で HTTPServer を起動

HTTPServer は http.Server.prototype を継承しているラッパーなので、ここで呼び出されるlisten() メソッドは Node.js の http モジュールのサーバーが起動します。

リクエスト処理フロー

リクエストの処理フローは以下のようになっています。

  1. クライアントからリクエストがあると、HTTPServer.prototype.handle メソッドが呼び出される
  2. リクエストされた URL にマッチするミドルウェアを登録順(use を読んだ順)に呼び出す

ポイントとしては、Connect 自体はリクエストに対する処理をほぼ持たず、すべての処理を middleware に委譲しているという点です。つまり、Connect は Node の http.Server の極薄ラッパーで、その機能は URL と middleware のマッピング機能である、ということです。

なお、Connect 自体が持っているのは以下の最小限の機能です。

  • 処理されないエラーに対して500エラーを送出
  • URL に対応する middleware が1つも見つからない場合に 404エラーを送出

HTTPServer.prototype.handle メソッドが呼び出される

これは上記でも書いたように、Node の http.Server の機能です。この処理について、Connect は何も追加の実装はしていません。

リクエストURL にマッチするミドルウェアを登録順に呼び出す

http.js 133行目

Server.prototype.handle = function(req, res, out) {
  var writeHead = res.writeHead
    , stack = this.stack
    , removed = ''
    , index = 0;

  function next(err) {
    /* 後で解説するのでここでは省略 */
  }
  next();
};

handle メソッドはクライアントからのリクエストがあるたびに呼び出されるメソッドで、その実装は next メソッドの定義と next メソッドの呼び出しです。上記のように next メソッドの定義を取り除くと、メソッド変数の定義と next メソッドの呼び出しのみにシンプルになります。

そして、next の実装が Connect の肝となります。middleware は要するに リクエストフィルターで、next はフィルターチェーンメソッドです。middleware 内で next メソッドを呼ぶと、次の middleware が呼び出されますし、middleware で next メソッドを呼ばずに、レスポンスを返してしまうこともできます。

例えば、bodyParser はリクエストボディをパースして、その結果を request オブジェクトに保存して、next メソッドを呼び出します。static は対応するファイルをレスポンスとして返し、next メソッドは呼び出しません。

それでは、next の実装を見ていきましょう。

next メソッドの実装は大きく分けて3つあり、ソースコードの上から順に、グローバル例外処理、リクエスト URL から対応する middleware の取得、そして middleware の呼び出しです。

グローバル例外処理 (148行目〜)
  function next(err) {
    /* 省略 */
    layer = stack[index++]; // use メソッドで登録した middleware を順番に取り出す

    // all done
    if (!layer || res.headerSent) {
      // but wait! we have a parent
      if (out) return out(err);

      // error
      if (err) {
        var msg = 'production' == env
          ? 'Internal Server Error'
          : err.stack || err.toString();

        // output to stderr in a non-test env
        if ('test' != env) console.error(err.stack || err.toString());

        // unable to respond
        if (res.headerSent) return req.socket.destroy();

        res.statusCode = 500;
        res.setHeader('Content-Type', 'text/plain');
        if ('HEAD' == req.method) return res.end();
        res.end(msg);
      } else {
        res.statusCode = 404;
        res.setHeader('Content-Type', 'text/plain');
        if ('HEAD' == req.method) return res.end();
        res.end('Cannot ' + req.method + ' ' + req.url);
      }
      return;
    }
    /* 省略 */
}

上述した Connect の必要最低限の機能はここで実装されています。

  • 処理されないエラーに対して500エラーを送出
  • URL に対応する middleware が1つも見つからない場合に 404エラーを送出

express のエラー処理で以下のようなコードを見かけたことがあると思いますが、これ connect.errorHandle() を設定しない場合は、ここで処理されます。

if (err) { next(err) } 
middleware の呼び出し (177行目〜)
  function next(err) {
    /* 省略 */
    layer = stack[index++];

    /* 省略 */
    try {
      path = parse(req.url).pathname;
      if (undefined == path) path = '/';

      // skip this layer if the route doesn't match.
      if (0 != path.indexOf(layer.route)) return next(err);

      c = path[layer.route.length];
      if (c && '/' != c && '.' != c) return next(err);

      // Call the layer handler
      // Trim off the part of the url that matches the route
      removed = layer.route;
      req.url = req.url.substr(removed.length);

      // Ensure leading slash
      if ('/' != req.url[0]) req.url = '/' + req.url;
     
      /* 省略 */
    } catch (e) {
      if (e instanceof assert.AssertionError) {
        console.error(e.stack + '\n');
        next(e);
      } else {
        next(e);
      }
    }
  }
};

stack から middleware を取り出し、middleware.route と URL を比較し、一致した場合は、middleware.handle メソッドを、一致しない場合は next メソッドを呼び出して、後続の middleware に処理を移譲しています。

リクエスト URL から対応する middleware の取得 (195行目〜)
  function next(err) {
    /* 省略 */
    layer = stack[index++];
    /* 省略 */

   try {
      /* 省略 */
      var arity = layer.handle.length; // Function.prototype.length は引数の数を返す
      if (err) {
        if (arity === 4) {
          layer.handle(err, req, res, next);
        } else {
          next(err);
        }
      } else if (arity < 4) {
        layer.handle(req, res, next);
      } else {
        next();
      }
    } catch (e) {
      if (e instanceof assert.AssertionError) {
        console.error(e.stack + '\n');
        next(e);
      } else {
        next(e);
      }
    }

ここでは、middleware.handle メソッドの引数の数に応じて引数の順番を変えて、URL に対応した middleware を呼び出しています。middleware は以下のいずれかのシグネチャを持つ必要があることがわかります。

function(err, req, res, next) { ... }
function(req, res, next) { ... }

エラーがあった場合に何かする middleware (例えば、errorHandler)は上のシグネチャを、エラーがあった場合に呼び出されて欲しくない middleware (logger や static など)は下のシグネチャで宣言しておきます。

以上でリクエストの処理フローが完了します。

まとめ

  • Connect は URL と middleware をマッピングする Node の標準モジュール http.Server の薄いラッパー
  • リクエストの前後に処理を挟み込むためのフィルタ
  • middleware は登録した(use を呼び出した)順に呼び出される
  • 次の middleware を呼び出す場合は middleware 内で next メソッドを呼び出す。
  • next を呼び出さずに middleware 内でレスポンスを返してしまっても良い
  • Connect は最低限のグローバル例外処理のみ実装している

次回予告

次回はソースコードリーディングの趣旨とは少し外れますが、ここまでの知識を利用して、シンプルな URLルーターを独自 middleware として作成、利用する方法について解説します。