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にアクセスする場合は
のようにパス[/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)