NodeJSすごいよw

nodeJSはサーバ側のプログラムもJSでかけるので、ブラウザ側の実装と同じものが使えて便利だ。それとGoogleChromeのV8EngineであのGoogle製の高速JavaScriptEngineで、C10K問題を解決するNoneBlockingI/Oなど等、話題性を掻っ攫って登場したのは、2009年の約9年前。
だけど現在はGitBookや、ReactCLIなどの基盤で使われることが多くあるが、普通にWebサーバで利用されることはあまりない状況である。
何故なのか!!ってのは簡単で、NonBlockingだから1プロセス1スレッドでしか動かないから、結局Enterpriseでは使えないってことらしい。あとはバージョン毎の変化が激しいからとか。
私がこれまでNodeJSをまともに使おうと全く考えなかったこと、それは「1プロセス1スレッド」動作だから、それじゃあ重い処理たとえば「DBアクセス」中における待ち時間の時間において「他の通信I/O部分」がすべて停止してしまうと言う問題があるわけで、それじゃあ使えないねって思っていたわkです。
ただ、これも解決方法があって、そんな重い処理のときだけ、別途別スレッドで動かせばよいと言う話があるわけですが、それを一々実装時において気にしながら作るってのも、まあつらいよねって思うわけで「使わない自由」を行使しようと思うわけです。
・・・・・・・・・・・・・・・・・・・・・・・
そんな拒否反応からNodeJSと言うものの存在を軽く見ていた私なのですが、甘かったと言うか「激アマ」だよねって思ったのが「Cluster」の存在なわけです。
一々Nginx立てて、HTTPサーバも面倒だと思い、簡単なJS実装があればたいていの環境でNodeJSを入れているから、簡単なHTTPサーバを簡易的に立てれるwwwってことで、作ったやつに、じゃあ簡易的にWebAPI的にJSON返却ができるもの的があれば、色々と簡単なものを作る場合に便利だよねって思い、2時間ぐらいでJSを実装、300行足らずで普通に動くHTTPサーバ+WebAPIサーバを作ったわけですが、せっかくだからとWebAPI側の呼び出しは「マルチスレッド」で動かそうかとしらべてみたのですが、そこでみつかったのが「Cluster」と言うもの。
そして、それが超簡単で、たったこれだけ。

  // クラスタ起動.
  var cluster = require('cluster');
  var MAX_SERVER = require('os').cpus().length;
  if (cluster.isMaster) {
    for (var i = 0; i < MAX_SERVER; ++i) {
      cluster.fork();
    }
    cluster.on('exit', function (worker, code, signal) {
      cluster.fork();
    });
  // サーバ起動.
  } else {
    // 普通にHTTPサーバの起動を行う系の処理を記載
  }

 
これだけでたとえば4コアCPUならば、4つのプロセスでNodeJSが起動して、しかも「Bindしたポートを共有してくれる」わけで、いやはや「凄い」と思った次第。
これで、NodeJSの欠点である1プロセス1スレッドこれの問題はなくなりそうです。
ちまみに、作ったNodeJSのプログラムを以下に公開します。
使い方は簡単。
ファイル名を[msful.js]として作成するとして、

$ node ./msful[port]
※[port]はオプション、省略すると3333がバインドポートとなる.

とすれば、起動できます。
使い方は単純でWebAPIにアクセスする場合は

http://localhost:3333/api/index.js

のようにパス[/api/]でアクセスすることで、WebAPIのJSファイルを実行してくれます。
index.jsの実装は普通に最終的にJSON型で返却したいものを返却すればOKです。

return {hoge: "moge"}

とすればレスポンスで {hoge: "moge"} と返却されます.
あとはファイル名を省略すると[index.js]が呼び出し対象となります.
 
通常のコンテンツファイルについては、WebAPIパス以外のパス名を利用することで、これは普通のNginxなどと同じようにI/Oできます。
これもファイル名を省略すると[index.html]が呼び出し対象となります。
 
次にフォルダ構成としては、

・msful.js
|
・[html]
|   |
|   L-- index.html など
|
・[api]
  |
  L-- index.js など

とmsful.js を実行するカレントパス以下のフォルダが対象で、[html]フォルダにはコンテンツ系のファイルを、[api]フォルダにはWebAPI実装のJSファイルを配置することで、WebAPIの返却実行が行われます。
 
以下ソースコード

// micro json server. version 0.0.1.
(function (_g) {
  var fs = require('fs');
  var http = require('http');
  var PORT = 3333;
  var SERVER_NAME = "msful";
  var HTML_DIR = "./html";
  var API_DIR = ".";

  // ポート番号が指定されている場合は、そのポートを割り当てる.
  if (process.argv.length > 2) {
    var port = null;
    try {
      port = parseInt(process.argv[2]);
      if (port > 0 && port < 65535) {
        PORT = port;
      }
    } catch (e) {
    }
  }

  // クラスタ起動.
  var cluster = require('cluster');
  var MAX_SERVER = require('os').cpus().length;
  if (cluster.isMaster) {
    for (var i = 0; i < MAX_SERVER; ++i) {
      cluster.fork();
    }
    cluster.on('exit', function (worker, code, signal) {
      cluster.fork();
    });
    console.info("## listen: " + PORT);
  // サーバ起動.
  } else {
    
    // httpサーバ生成.
    var createHttp = function (call) {
      return http.createServer(function (req, res) {
        var data = "";
        req.on("data", function (chunk) {
          data += chunk;
        });
        req.on("end", function () {
          res = call(req, res, data);
        });
      })
    }

    // パラメータ取得.
    var getPms = function (request, response, data) {
      var ret = {};
      var m = request.method.toLowerCase();
      if (m == "get") {
        ret = _get(request.url);
      } else if (m == "POST") {
        ret = _post(request, data);
      }
      return ret;
    }
    
    // GETパラメータを処理.
    var _get = function (url) {
      var p = url.indexOf("?");
      if (p == -1) {
        return {};
      }
      return _analysisParams(url.substring(p + 1));
    }

    // POSTパラメータ処理.
    var _post = function (request, data) {
      var c = request.headers["content-type"];
      if (c.indexOf("/json") != -1) {
        return JSON.parse(data);
      }
      return _analysisParams(data);
    }

    // パラメータ解析.
    var _analysisParams = function (n) {
      var list = n.split("&");
      var len = list.length;
      var ret = {};
      for (var i = 0; i < len; i++) {
        n = list[i].split("=");
        if (n.length == 1) {
          ret[n[0]] = '';
        } else {
          ret[n[0]] = decodeURIComponent(n[1]);
        }
      }
      return ret;
    }

    // HTTP実行.
    var exec = function (req, res, data) {
      var url = getUrl(req);
      // WebApi返却.
      if (url.indexOf("/api/") == 0) {
        readApi(req, res, data, url);
        // ファイル返却.
      } else {
        readFile(req, res, url);
      }
    }

    // api読み込み.
    var readApi = function (req, res, data, url) {
      if (url.lastIndexOf("/") == url.length - 1) {
        url += "index.js";
      }
      var name = API_DIR + url;
      fs.readFile(name, function (err, src) {
        var status = 200;
        var headers = { 'Server': SERVER_NAME };
        var body = "";
        var bodyLength = 0;
        try {
          if (err) throw err;
          src = "return (function(_g) {\n" +
            "var request = args.req\n" +
            "var response = args.res\n" +
            "var params = getPms(request, response, args.data)\n" +
            "args = null\n" +
            "getPms = null\n" +
            src +
            "\n})(global)";
          var eres = new Function('args', 'getPms', src)(
            { req: req, res: res, data: data }, getPms)
          src = null;
          body = JSON.stringify(eres);
          eres = null;
          bodyLength = utf8Length(body);
          headers['Content-Type'] = 'application/json; charset=utf-8;';
          headers['Connection'] = 'close';
        } catch (e) {
          // 例外が発生した場合は、エラー返却.
          // ただし、ファイルが存在しない場合は、404返却.
          if (e.code && e.code == 'ENOENT') {
            status = 404;
          } else {
            status = 500;
            console.error(e, e);
          }
          headers['Content-Type'] = 'application/json; charset=utf-8;';
          headers['Connection'] = 'close';
          body = "{\"error\": " + status + "}";
          bodyLength = utf8Length(body);
        }
        headers['Date'] = toRfc822(new Date());
        headers['Content-Length'] = bodyLength;
        
        // 返却処理.
        res.writeHead(status, headers);
        res.end(body);
      });
    }

    //  ファイル読み込み.
    var readFile = function (req, res, url) {
      if (url.lastIndexOf("/") == url.length - 1) {
        url += "index.html";
      }
      var name = HTML_DIR + url;
      fs.stat(name, function (err, stat) {
        var status = 200;
        var headers = { 'Server': SERVER_NAME };
        var body = "";
        var bodyLength = 0;
        try {
          if (err) throw err;
          headers['Content-Type'] = mimeType(url);
          headers['Last-Modified'] = toRfc822(new Date(stat.mtime));
          // リクエストヘッダを見て、If-Modified-Sinceと、ファイル日付を比較.
          if (req.headers["if-modified-since"] != undefined) {
            if (isCache(stat.mtime, req.headers["if-modified-since"])) {
              status = 304;
            }
          }
          if (status == 200) {
            // ファイル情報は非同期で読む.
            headers['Content-Length'] = stat.size;
            headers['Date'] = toRfc822(new Date());
            
            // 返却処理.
            res.writeHead(status, headers);
            var readableStream = fs.createReadStream(name);
            readableStream.on('data', function (data) {
              res.write(data);
            });
            readableStream.on('end', function () {
              res.end();
            });
            return;
          }
        } catch (e) {
          // 例外が発生した場合は、エラー返却.
          // ただし、ファイルが存在しない場合は、404返却.
          if (e.code && e.code == 'ENOENT') {
            status = 404;
          } else {
            status = 500;
            console.error(e, e);
          }
          headers['Content-Type'] = "text/html; charset=utf-8";
          headers['Connection'] = 'close';
          body = "error: " + status;
          bodyLength = utf8Length(body);
        }
        headers['Date'] = toRfc822(new Date());
        headers['Content-Length'] = bodyLength;
        
        // 返却処理.
        res.writeHead(status, headers);
        res.end(body);
      });
    }

    // キャッシュ条件かチェック.
    var isCache = function (a, b) {
      return parseInt(new Date(a).getTime() / 1000) == parseInt(new Date(b).getTime() / 1000);
    }

    // 正しいURLを取得.
    var getUrl = function (req) {
      var u = req.url;
      var p = u.indexOf("?");
      if (p == -1) {
        return u;
      }
      return u.substring(0, p);
    }

    // ファイル拡張子からMimeTypeを返却.
    var mimeType = function (name) {
      var p = name.lastIndexOf(".");
      if (p == -1) {
        return "text/plain";
      }
      var n = name.substring(p + 1)
      switch (n) {
        case "htm": case "html": return "text/html; charset=utf-8;";
        case "xhtml": case "xht": return "application/xhtml+xml; charset=utf-8;";
        case "js": return "text/javascript; charset=utf-8;";
        case "css": return "text/css; charset=utf-8;";
        case "rtf": return "text/rtf";
        case "tsv": return "text/tab-separated-values";
        case "gif": return "image/gif";
        case "jpg": case "jpeg": return "image/jpeg";
        case "png": return "image/png";
        case "svg": return "image/svg+xml";
        case "rss": case "xml": case "xsl": return "application/xml";
        case "pdf": return "application/pdf";
        case "doc": return "application/msword";
        case "xls": return "application/vnd.ms-excel";
        case "ppt": return "application/vnd.ms-powerpoint";
        case "docx": return "application/vnd.openxmlformats-officedocument.wordprocessingml.document docx";
        case "xlsx": return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx";
        case "pptx": return "application/vnd.openxmlformats-officedocument.presentationml.presentation pptx";
        case "dtd": return "application/xml-dtd";
        case "sh": return "application/x-sh";
        case "tar": return "application/x-tar";
        case "zip": return "application/zip";
        case "jar": return "application/java-archive";
        case "swf": return "application/x-shockwave-flash";
        case "mpga": case "mp2": case "mp3": return "audio/mpeg";
        case "wma": return "audio/x-ms-wma";
        case "wav": return "audio/x-wav";
        case "3gp": return "video/3gpp";
        case "3g2": return "video/3gpp2";
        case "mpeg": case "mpg": case "mpe": return "video/mpeg";
        case "qt": case "mov": return "video/quicktime";
        case "mxu": case "m4u": return "video/vnd.mpegurl";
        case "asf": case "asx": return "video/x-ms-asf";
        case "avi": return "video/x-msvideo";
        case "wmv": return "video/x-ms-wmv";
        case "flv": return "video/x-flv";
        case "ogg": return "application/ogg";
        case "mpg4": return "video/mp4";
      }
      return "application/octet-stream";
    }

    // 日付情報をRFC-822に変換.
    var toRfc822 = function (n) {
      if (typeof (n) == "number") {
        n = new Date(parseInt(n));
      }
      return n.toUTCString();
    }

    // UTF8文字列のバイナリ長を取得.
    var utf8Length = function (n) {
      var c;
      var ret = 0;
      var len = n.length;
      for (var i = 0; i < len; i++) {
        if ((c = n.charCodeAt(i)) < 128) {
          ret++;
        } else if ((c > 127) && (c < 2048)) {
          ret += 2;
        } else {
          ret += 3;
        }
      }
      return ret;
    }

    // サーバー生成.
    var server = createHttp(exec);
    
    // 指定ポートで待つ.
    server.listen(PORT);
  }
})(global)