Node.js をクラウド環境での開発用のフルスタックとして使用する

コールバックによる非同期 I/O を使用する並行性モデルを採り入れて、チャット・サーバーを構築する

Web サーバーを始めとするスケーラブルなネットワーク・プログラムを作成するために設計された、Node.js の詳細を学んでください。Node.js は UNIX ライクなプラットフォーム上で動作する V8 JavaScript エンジン用のイベント駆動型 I/O フレームワークです。この記事では Node.js と、このフレームワークを中心としたエコシステム (クラウド・オファリングを含む) を詳しく探り、最後に Node.js でチャット・サーバーを構築する方法に関する包括的な例を紹介します。

Noah Gift, Associate Director Engineer, AT&T Interactive

author Noah GiftNoah Gift は、AT&T Interactive で経験豊富な技術リーダーを務めるソフトウェア開発者です。彼は興味深い問題を解決する手段として、Python/Iron Python、Erlang、F#、C#、JavaScript などのさまざまな言語を使用しています (Caltech、Disney Feature Animation、Sony Imageworks、および Weta Digital での勤務経験があります)。Python Software Foundation のメンバーである彼は、developerWorks に多数の記事を書いており、『Python For Unix and Linux System Administration』の共著者でもあります。カリフォルニア州立工科大学サンルイス・オビスポ校で栄養科学の学位を取得し、カリフォルニア州立大学ロサンゼルス校でコンピューター情報システムの修士号を取得しました。ビジネス分析、金融、起業家精神を専攻しているカリフォルニア州立大学デービス校では MBA 候補となっています。余暇は、ピアノ曲の作曲とマラソンで過ごしています。彼の Web サイトにアクセスしてください。また、Twitter でフォローすることも、Web で彼の書いた記事を読むこともできます。


developerWorks 貢献著者レベル

Jeremy Jones, Senior Systems Engineer, Predictix

Jeremy JonesJeremy Jones は Predictix のシニア・システム・エンジニアです。数々のオンライン記事の著者、そして『Python for UNIX and Linux System Administration』の共著者でもあります。普段は Python での問題に取り組んでいますが、bash、JavaScript、perl、C#、および Java にも手を伸ばしていることで知られています。彼は、さまざまな問題領域に潜むあからさまではない問題のトラブルシューティングと解決を楽しみとしています。余暇は趣味の木工と家族との時間で過ごしています。



2011年 6月 03日

爆発的に見える勢いで技術革新が進み続けるなか、なるほどと思えるような新しいアイデアが次々と生まれています。サーバー・サイド JavaScript は、そのようなアイデアの 1 つです。このサーバー・サイド JavaScript という優れたアイデアを実現するのが Node.js です。Node.js は、UNIX ライクなプラットフォーム上で動作する V8 JavaScript エンジン用のイベント駆動型 I/O フレームワークで、Web サーバーを始めとするスケーラブルなネットワーク・プログラムを作成することを目的としています。

Node.js を使用することで、JavaScript と奮闘しなくても、JavaScript をサーバー・サイドのコードからブラウザーに至るまでの開発用のフルスタックとして利用できるようになります。Node.js で採用している革新的なアイデアには、コールバックによる非同期 I/O を使用する並行性モデルもあります。

Node.js を使用したクラウド・プラットフォーム

Node.js フレームワークを使用することによるメリットは、Node.js をクラウド環境で使用する場合に顕著に現れてきます。アプリケーション開発者にとってクラウド環境を使用するということは、突き詰めるところ、PaaS (Platform as a Service) モデル、または IaaS (Infrastructure as a Service) モデルのどちらかを使用するということになります。より抽象化されていて、開発者にとってほぼ間違いなく利便性に優れているのは、PaaS プロバイダーを利用する方法です。図 1 に、大幅に簡略化した PaaS モデルと IaaS モデルの構造を示します。

図 1. PaaS と IaaS の構造
PaaS と IaaS の構造

最近、Cloud Foundry という画期的なオープンソース・プロジェクトが、Node.js を実行できるプライベート PaaS を作成するためのコードをリリースしました。これと同じホスト・エンジンは、パブリック・クラウドと商用クラウドでも使用することができて、これらのクラウドはソフトウェア・パッチを受け入れます。

インフラストラクチャーを管理する大変な作業を (永遠に!) プロバイダーに委託すると、プロバイダーはソース・コードであろうと物理ハードウェア・リソースであろうと、規模の経済性をフルに活かしてインフラストラクチャーを管理するため、開発者にとっては実にエキサイティングな時代となっています。


Node.js シェルの使用方法

Node.js だけで作成するサンプル・アプリケーションに取り掛かる前に、まずはインタラクティブなシェルの使い方を説明しておきます。Node.js をまだインストールしていない場合は、インストール手順に従って Node.js をインストールしてください (「参考文献」セクションを参照)。または、インタラクティブなオンライン Node.js サイトを使用してブラウザーに直接コードを入力することもできます。

Node.js でインタラクティブに JavaScript 関数を作成するには、コマンド・プロンプトから、以下のように「node」と入力します。

lion% node
> var foo = {bar: 'baz'};
> console.log(foo);
{ bar: 'baz' }
>

上記の例では、オブジェクト foo を作成した後、console.log の呼び出しによって、このオブジェクトをコンソールに出力しました。これだけでも十分に強力で面白い機能ですが、真の面白さは、タブによる補完機能を使って foo を探索するところから始まります。以下の例のように、foo.bar. と入力して Tab キーを押すと、このオブジェクトで使用できるメソッドが表示されます。

> foo.bar.
[...output suppressed for space...]
foo.bar.toUpperCase           foo.bar.trim
foo.bar.trimLeft              foo.bar.trimRight

toUpperCase メソッドを試してみると面白そうのなので、このメソッドを実行したところ、以下の結果となりました。

> foo.bar.toUpperCase();
'BAZ'

ご覧のように、このメソッドは文字列を大文字に変換します。このようなインタラクティブな開発スタイルは、Node.js のようなイベント駆動型フレームワークで開発を行うには最適です。

記事の導入はここまでにして、これから実際の作成作業に移ります。


Node.js で構築するチャット・サーバー

Node.js では、イベント駆動型ネットワーク・サーバーを簡単に作成することができます。その一例として、これからいくつかのチャット・サーバーを作成します。最初に作成するのは、機能がないと言ってもよいぐらいの平凡なチャット・サーバーです。このチャット・サーバーは、例外処理をまったく行いません。

チャット・サーバーには複数のクライアントが接続することができます。各クライアントはメッセージを作成して、他のすべてのユーザーにそのメッセージをブロードキャストすることができます。以下は、この単純さを極めたチャット・サーバーのコードです。

net = require('net');

var sockets = [];

var s = net.Server(function(socket) {

    sockets.push(socket);

    socket.on('data', function(d) {

        for (var i=0; i < sockets.length; i++ ) {
            sockets[i].write(d);
        }
    });
});

s.listen(8001);

20 行足らずのコードで (実のところ、何かしらの処理を行うのは 8 行だけです)、実際に機能するチャット・サーバーを作成することができました。この単純なプログラムのフローは以下のとおりです。

  • ソケットが接続すると、そのソケット・オブジェクトを配列に追加します。
  • クライアントが接続に書き込みを行うと、そのデータをすべてのソケットに書き込みます。

ここでコードを詳しく追って、チャット・サーバーに対する要求として定義されている内容 (どのようなサーバーで何を実行するサーバーであるか) をこの例ではどのように実現しているかを説明します。

まず、最初の行で、net モジュールの内容にアクセスできるようにします。

net = require('net');

そして、このモジュールの Server を使用します。

データを書き込む際に、クライアントの接続すべてに書き込めるようにするためには、クライアントの接続のすべてを保持する場所が必要になります。以下は、クライアントのソケット接続のすべてを保持する変数です。

var sockets = [];

その次の行からは、各クライアントの接続時に行う処理内容を記述するコード・ブロックが始まります。

var s = net.Server(function(socket) {

Server に渡す引数は、各クライアントの接続に対して呼び出される関数だけです。この関数の中で、クライアントの接続の全リストにクライアントの接続を追加します。

sockets.push(socket);

上記に続く次のコードは、イベント・ハンドラーをセットアップして、クライアントがデータを送信したときの処理内容を記述します。

socket.on('data', function(d) {

    for (var i=0; i < sockets.length; i++ ) {
        sockets[i].write(d);
    }
});

socket.on() 呼び出しはイベント・ハンドラーをノードに登録することで、特定のイベントが発生したときに実行すべき内容をノードが認識できるようにします。Node.js がこの特定のイベント・ハンドラーを呼び出すのは、クライアントからデータを受信したときです。その他のイベント・ハンドラーには、connectendtimeoutdrainerror、および close があります。

socket.on() 呼び出しの構造は、前述の Server() 呼び出しと同様です。この両方の呼び出しに関数を渡すと、イベント発生時にはその関数が呼び出されることになります。このコールバック手法は、非同期ネットワーク・フレームワークでは一般的な手法です。そしてこれが、手続き型プログラミングの経験者が Node.js のような非同期フレームワークに着手する際に最も手こずらされる難問となっています。

この例では、任意のクライアントがサーバーにデータを送信すると、この匿名関数が呼び出されて、データが関数に渡されます。この動作は、これまでに集めたソケット・オブジェクトのリストに対して繰り返し行われ、そのすべてのオブジェクトに対して同じデータが送信されます。したがって、各クライアントの接続がこのデータを受信するというわけです。

このチャット・サーバーはかなり単純なもので、極めて基本的な機能がいくつか欠けています。例えば、各メッセージの送信元を識別する機能や、クライアントの切断時に対処するなどの機能です (クライアントがこのチャット・サーバーから切断すると、誰かがメッセージを送信した場合、チャット・サーバーはクラッシュしてしまいます)。

そこで、「問題となる状況」(クライアントの切断など) に対処するために強化した機能とコードによって改善された後のソケット・サーバーのソース・コードを以下に記載します (ダウンロード・サンプルに含まれる chat2.js という名前のファイルです)。

net = require('net');

var sockets = [];
var name_map = new Array();
var chuck_quotes = [
    "There used to be a street named after Chuck Norris, but it was changed because 
     nobody crosses Chuck Norris and lives.",
    "Chuck Norris died 20 years ago, Death just hasn't built up the courage to tell 
     him yet.",
    "Chuck Norris has already been to Mars; that's why there are no signs of life.",
    "Some magicians can walk on water, Chuck Norris can swim through land.",
    "Chuck Norris and Superman once fought each other on a bet. The loser had to start 
     wearing his underwear on the outside of his pants."
]

function get_username(socket) {
    var name = socket.remoteAddress;
    for (var k in name_map) {
        if (name_map[k] == socket) {
            name = k;
        }
    }
    return name;
}

function delete_user(socket) {
    var old_name = get_username(socket);
    if (old_name != null) {
        delete(name_map[old_name]);
    }
}

function send_to_all(message, from_socket, ignore_header) {
    username = get_username(from_socket);
    for (var i=0; i < sockets.length; i++ ) {
        if (from_socket != sockets[i]) {
            if (ignore_header) {
                send_to_socket(sockets[i], message);
            }
            else {
                send_to_socket(sockets[i], username + ': ' + message);
            }
        }
    }
}

function send_to_socket(socket, message) {
    socket.write(message + '\n');
}

function execute_command(socket, command, args) {
    if (command == 'identify') {
        delete_user(socket);
        name = args.split(' ', 1)[0];
        name_map[name] = socket;
    }
    if (command == 'me') {
        name = get_username(socket);
        send_to_all('**' + name + '** ' + args, socket, true);
    }
    if (command == 'chuck') {
        var i = Math.floor(Math.random() * chuck_quotes.length);
        send_to_all(chuck_quotes[i], socket, true);
    }
    if (command == 'who') {
        send_to_socket(socket, 'Identified users:');
        for (var name in name_map) {
            send_to_socket(socket, '- ' + name);
        }
    }
}

function send_private_message(socket, recipient_name, message) {
    to_socket = name_map[recipient_name];
    if (! to_socket) {
        send_to_socket(socket, recipient_name + ' is not a valid user');
        return;
    }
    send_to_socket(to_socket, '[ DM ' + get_username(socket) + ' ]: ' + message);
}

var s = net.Server(function(socket) {
    sockets.push(socket);
    socket.on('data', function(d) {
        data = d.toString('utf8').trim();
        // check if it is a command
        var cmd_re = /^\/([a-z]+)[ ]*(.*)/g;
        var dm_re = /^@([a-z]+)[ ]+(.*)/g;
        cmd_match = cmd_re.exec(data)
        dm_match = dm_re.exec(data)
        if (cmd_match) {
            var command = cmd_match[1];
            var args = cmd_match[2];
            execute_command(socket, command, args);
        }
        // check if it is a direct message
        else if (dm_match) {
            var recipient = dm_match[1];
            var message = dm_match[2];
            send_private_message(socket, recipient, message);
        }
        // if none of the above, send to all
        else {
            send_to_all(data, socket);
        };

    });
    socket.on('close', function() {
        sockets.splice(sockets.indexOf(socket), 1);
        delete_user(socket);
    });
});
s.listen(8001);

もう少し高度な例: チャット・サーバーのロード・バランシング

クラウドにデプロイする理由として、負荷の増加に応じてスケールアップするという目的が含まれることは珍しくありません。そのようなデプロイメントには、何らかのロード・バランシング・メカニズムが必要です。

nginx や lighttpd をはじめ、大抵の軽量 Web サーバーは複数の HTTP サーバーの間でロード・バランシングを実行することができます。ただし、非 HTTP サーバー間でのロード・バランシングが必要な場合には、nginx は選択肢から外れます。また、汎用の TCP ロード・バランサーはあるものの、これらのロード・バランサーで使用するロード・バランシング・アルゴリズムが気に入らない場合や、必要な機能が組み込まれていない場合も考えられます。あるいは、独自のロード・バランサーを動作させてみたいという場合もあるでしょう。

以下に、可能な限り単純にしたロード・バランサーのコードを記載します。このロード・バランサーにフェイルオーバー機能はなく、負荷の分散先となるすべての宛先サーバーが使用可能であることを前提にしています。また、エラー処理を行うこともないシンプルなロード・バランサーです。基本概念としては、このロード・バランサーはクライアントからソケット接続を受け取ると、無作為に宛先サーバーを選んでそのサーバーに接続し、接続したサーバーにクライアントからのすべてのデータを転送します。一方、サーバーからそのクライアントに対するすべてのデータの転送も行います。

net = require('net');

var destinations = [
    ['localhost', 8001],
    ['localhost', 8002],
    ['localhost', 8003],
]

var s = net.Server(function(client_socket) {
    var i = Math.floor(Math.random() * destinations.length);
    console.log("connecting to " + destinations[i].toString() + "\n");
    var dest_socket = net.Socket();
    dest_socket.connect(destinations[i][1], destinations[i][0]);

    dest_socket.on('data', function(d) {
        client_socket.write(d);
    });
    client_socket.on('data', function(d) {
        dest_socket.write(d);
    });
});
s.listen(9001);

destinations を定義している部分では、ロード・バランシングを行うバックエンド・サーバーを構成しています。destinations は配列からなる配列に過ぎず、配列を構成する各配列には、最初の要素としてホスト名、2 番目の要素としてポート番号が指定されています。

Server() の定義では、チャット・サーバーの例と同じく、ソケット・サーバーを作成して、そのサーバーにポートでリッスンさせます。ただしこの場合、サーバーがリッスンするのはポート 9001 です。

Server() に対するコールバックの定義では、まず、接続する宛先を無作為に選択します。

var i = Math.floor(Math.random() * destinations.length);

ラウンド・ロビンを使うことも、追加作業を行って「最小接続」アルゴリズムを実践することも可能ですが、ここではできるだけ単純にしてあります。

この例で名前を指定しているソケット・オブジェクトは、client_socketdest_socket の 2 つです。

  • client_socket は、ロード・バランサーとクライアントとの間の接続です。
  • dest_socket は、ロード・バランサーとロード・バランシング対象のサーバーとの間の接続です。

この 2 つのソケットがそれぞれに、1 つのイベントを処理します。そのイベントとは、データの受信です。どちらか一方のソケットがデータを受信すると、そのソケットは受信したデータを他方のソケットに書き込みます。

以下に、クライアントがロード・バランサーを介して汎用ネットワーク・サーバーに接続してから、データを送信し、データを受信するまでの処理サイクル全体を説明します。

  1. クライアントがロード・バランサーに接続すると、Node.js はそのクライアントと Node.js 自体との間のソケットを作成します。ここでは、このソケットを client_socket と呼びます。
  2. 接続が確立された後、ロード・バランサーは宛先を選び出し、その宛先とのソケット接続を作成します。このソケットについては、ここでは dest_socket と呼びます。
  3. クライアントがデータを送信すると、ロード・バランサーはそのデータを宛先サーバーに転送します。
  4. 宛先サーバーが応答し、データを dest_socket に書き込むと、ロード・バランサーは client_socket を介してそのデータをクライアントに転送します。

このロード・バランサーに対する改善内容としては、エラー処理、同じプロセスにもう 1 つ宛先サーバーを組み込んで動的に宛先の追加/削除を行うこと、複数の異なるロード・バランシング・アルゴリズムを追加すること、そして何らかの耐障害性を追加することなどが考えられます。


外部のソリューション: Express Web フレームワーク

Node.js には HTTP サーバーの機能が備わっていますが、それらの機能は低いレベルのものです。Node.js で Web アプリケーションを構築することを検討している場合には、Node.js 用に作成された Web アプリケーション開発フレームワーク、 Express を調べてみることをお勧めします。

次の例では、Node.js をそのまま使用するのではなく、Express を使用することでもたらされる明らかなメリットに注目します。その 1 つは、リクエストのルーティングです。そして、get や post などの HTTP の動詞タイプに対するイベントの登録もメリットの 1 つとして挙げられます。

以下に、極めて単純な Web アプリケーションを記載します。このアプリケーションが行うのは、Express の基本的な機能のいくつかを具体的に示すことだけです。

var app = require('express').createServer();

app.get('/', function(req, res){
  res.send('This is the root.');
});

app.get('/root/:id', function(req, res){
  res.send('You sent ' + req.params.id + ' as an id');
});

app.listen(7000);

app.get() で始まる 2 つの行は、GET リクエストを受け取るとトリガーされるイベント・ハンドラーです。どちらの呼び出しにしても、1 番目の引数はカスタマーが渡すことのできる URL を指定する正規表現で、2 番目の引数が、実際にリクエストを処理する関数となります。

正規表現の引数は、ルーティング・メカニズムを表しています。受け取ったリクエストのタイプ (GET、POST など) が一致した上で、リソースが正規表現 (/、/root/123) とマッチする場合には、ハンドラー関数が呼び出されます。最初の app.get() 呼び出しでは、リソースとして / が指定されているだけです。2 番目の呼び出しでは、/root が指定された後に ID が続きます。この URL マッピングの正規表現でリソースの前に置かれているコロン (:) 文字が、この ID の部分を以降で使用可能なパラメーターとして識別します。

このハンドラー関数は、リクエストのタイプが一致した上で、リソースの正規表現がマッチすると呼び出され、引数としてリクエスト (req) とレスポンス (res) を取ります。前述のパラメーターは、リクエスト・オブジェクトに関連付けられます。そしてユーザーに返される Web サーバーのメッセージは、レスポンス・オブジェクトに渡されます。

これは至って単純な例ですが、「実際のアプリケーション」がこのフレームワークを使用するメリットを生かして一層リッチで充実した機能のバンドルを作成できることは一目瞭然です。テンプレート・システムと何らかのデータ・エンジン (従来のデータベースまたは NoSQL データベースのいずれか) を接続すれば、実際のアプリケーションの要件を満たす一連の機能を簡単に作成することができます。

Express に備わっている特性には、ハイパフォーマンスであることも含まれます。この点が、Web アプリケーション開発を迅速化する他のフレームワークに共通の特性と併せ、ハイパフォーマンスと大規模なスケーラビリティーが不可決なクラウド・デプロイメントの分野で Express を優位に位置付けています。


最後に付け加えておくべきこと

以下の 2 つの概念/傾向も認識しておく必要があります。

  • キー・バリュー型データベースの人気の急増
  • その他の非同期 Web パラダイム

キー・バリュー型データベースの人気が急増している理由

JavaScript は Web の共通語であるため、JavaScript に関連する話題には、JSON (JavaScript Object Notation) が登場することもよくあります。JSON は、JavaScript とその他の言語との間でデータを交換する手段として最もよく使われています。JSON は基本的にキー・バリュー型のストアであることから、JavaScript および Node.js の開発者がキー・バリュー型データベースに興味を示すのは当然のことです。結局のところ、JSON 形式でデータを保管できれば、JavaScript 開発者の作業は遥かに楽になります。

それほど関連性はないにしても、キー・バリュー型データベースは NoSQL データベースのコンテキストでも話題に上ります。ブリュワーの定理としても知られる CAP 定理では、分散システムが一貫性、可用性、ネットワークの分断耐性という 3 つの特性をすべて同時に備えること (CAP の形式的証明) は不可能であるとしています。この定理は、(通常は) 可用性を高めるのと引き換えに、従来のリレーショナル・データベースの一部の特徴を犠牲にする正当な理由となっており、NoSQL 推進の原動力の 1 つとなっています。よく使用されている数少ないキー・バリュー型データベースには、Riak、Cassandra、CouchDB、および MongoDB があります。

非同期 Web のパラダイム

イベント駆動型の非同期 Web フレームワークには、かなり長い歴史があります。そのなかで比較的新しく、よく使用されている非同期 Web フレームワークとして挙げられるのは、Tornado です。Python 言語で作成されている Tornado は、Facebook の内部で使用されています。以下は、Tornado で作成した hello_world の一例 (ダウンロードに含まれる hello_tornado.py という名前のファイル) です。

import tornado.ioloop
import tornado.web

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("Hello, world")

application = tornado.web.Application([
    (r"/", MainHandler),
])

if __name__ == "__main__":
    application.listen(8888)
    tornado.ioloop.IOLoop.instance().start()

同じく Python で作成されている Twisted.web もこれと極めてよく似た方法で機能します。

最後に実際の Web サーバー自体に関して言うと、nginx があります。nginx は Apache とは異なり、スレッドを使用しません。代わりにイベント駆動型 (非同期) アーキテクチャーを使用してリクエストを処理します。非同期 Web フレームワークがその Web サーバーとして nginx を使用する例は、かなり一般的となっています。


まとめ

Node.js には確かに、Web 開発者を引き付ける魅力があります。Node.js を使用すれば、開発チームがクライアント・サイドとサーバー・サイドの両方で JavaScript を作成できるだけでなく、JQuery、V8、JSON、そしてイベント駆動型プログラミングといったJavaScript エコシステムで使用できる強力な技術を利用することもできます。さらに、Express Web フレームワークなど、Node.js をベースとしたエコシステムも進化しています。

Node.js は魅力的であると同時に、いくつかの欠点があることも触れておかなければなりません。その 1 つは、CPU バウンドの場合には、Node.js のノンブロッキング I/O によるメリットは得られないことです。Node.js の各インスタンスを実行するプロセスのプールを分けるなどして、この問題に対処するアーキテクチャーはあるので、Node.js を実装するかどうかは開発者の一存ということになります。


ダウンロード

内容ファイル名サイズ
Sample code for this articlenodejs_src.zip6KB

参考文献

学ぶために

製品や技術を入手するために

  • IBM Smart Business Development and Test on the IBM Cloud で使用できる製品イメージを調べてください。

議論するために

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Cloud computing
ArticleID=664876
ArticleTitle=Node.js をクラウド環境での開発用のフルスタックとして使用する
publish-date=06032011