ラベル node の投稿を表示しています。 すべての投稿を表示
ラベル node の投稿を表示しています。 すべての投稿を表示

2014年2月11日火曜日

Node.jsってなんじゃ?(gm:GraphicsMagickで画像合成)

今回はnode.jsを利用して、画像合成をしてみたいと思います。
この手の画像生成にはImageMagickがよく使われますが、ImageMagickから派生したGraphicMagickがImageMagickよりもパフォーマンスが優れているのでこちらの方を利用します。

node.js内からは、GraphicMagick/ImageMagickを使用できるgmというモジュールを利用します。

今回node.jsは久しぶりなので、nodeのインストールから始めてみたいと思います。
他のLL言語などでもそうですが、nvmやenv系などインストール環境管理ツールが乱立しているようです。
今回はnodebrewが便利そうなので、nodebrewを使ってみました。


GraphickMagickのインストール


yumでインストールします。
# yum install gcc-c++ GraphicsMagick -y



nodebrewを使ったnode.jsとモジュールのインストール


nodebrewでは、インストール実行した場所だけにファイルが作られるようで環境を汚さず、rootでなくともインストールが可能なことが特徴のようです。
インストールを行い、PATHを通すだけで完了なので、とても簡単です。
# cat /etc/profile.d/nodebrew.sh
#!/bin/bash

export NODE_PATH=$HOME/.nodebrew/current/node_modules
export PATH=$HOME/.nodebrew/current/bin:$PATH

$ source /etc/profile
$ cd ~/
$ curl https://raw.github.com/hokaccha/nodebrew/master/nodebrew | perl - setup

$ nodebrew install latest
$ nodebrew use latest

$ node -v
v0.11.11

$ npm install gm
$ npm install argv



実装



image.js

画像合成のメイン処理です。引数の画像URLをダウンロードして画像合成します。



gmcomposite.js

合成を子プロセスで実行する箇所をモジュール化したものです。



画像部品の配置



今回の合成ではfacebookのプロフィール写真がどうにも無愛想なので、かわいくしてみたいと思います。 合成素材用のディレクトリ、合成後の出力先のディレクトリを作成します。
$ mkdir -p assets rslt/convert rslt/composite rslt/download
$ tree ~/
/home/memorycraft/
|-- assets
|   `-- frame.png //合成素材(マスク+ベース兼用)
|-- gmcomposite.js 
|-- image.js
|-- node_modules
|   |-- argv
|   `-- gm
|-- rslt
|   |-- composite
|   |-- convert
|   `-- download
`-- tmp

また、httpdサーバのドキュメントルートから出力先ディレクトリにリンクします。
# chmod 755 /home/memorycraft/
# cd /var/www/html
# ln -s /home/memorycraft/rslt rslt


assetsディレクトリにマスク素材を配置します。

frame.png



実行



それでは実行します。 オプション引数にfacebookの画像URLと、facebook ID(画像名につかうだけなので何でも良い)を渡します。
$ node image.js -u http://fbcdn-sphotos-b-a.akamaihd.net/hphotos-ak-prn1/t1/1525482_10202990719234672_615625235_n.jpg -f memocra
create complete !
無事出力できたようです。



確認


それでは出力された画像をブラウザで確認してみます。


。。。。ちょっと思った感じと違いますが、とりあえず合成できたのでよしとします。
今回は以上です。

2013年2月7日木曜日

Node.jsってなんじゃ?(Socket.IOとELBのまとめ)

過去の記事でnode.jsの話題をいくつか取り上げて来ましたが、node.jsではAmazonのELBと併用した時の問題があり、その注意点をまとめました。

Socket.IO


Socket.IOはnode.jsでwebsocketを使用するときのデファクトといっていいライブラリです。
他のSocket.IOが人気になったのは、以下のような利点のためです。

  • websocketをサポートしていないブラウザでは、自動的にxhrなどのポーリング使い通信できる
  • 接続に失敗しても再接続などを自動的に行う


ここではSocket.IOを使用する前提で、ELBを経由して複数のnodeサーバーをホストする場合についてまとめてみました。

インストールに関しては以下に書いてありますので割愛します。
WebSocketってなんじゃ?(Node編2 Socket.IOでプッシュ通信)

nodeサーバーには以下のようにファイルを配置します。上記の記事などで使用したチャットアプリです。
publicをhttpdのドキュメントルートにしておきます。
app
├── node
│   └── server.js
└── public
    ├── assets
    │   └── js
    │       └── client.js
    ├── health.txt
    └── index.html

public/health.txtはELB用のヘルスチェックファイルで、中身はありません。
その他の各ファイルの内容は以下の通りです。

server.js
index.html
client.js



ポートは3000番を利用します。
ここで、httpdを起動しておきます。
また、
node server.js

などで、nodeを起動しておきます。


AWS


単体構成



まず最初の例として、AWSは例として以下のような構成だとします。
nodeサーバーのEC2インスタンスはhtmlのホストとwebsocketの両方を行うので、80と3000のポートをセキュリティグループで開放しておきます。

画面を開いてみます。

普通に成功します。
通信をみてみると、websocketで通信されていることがわかります。


ELB


次に、nodeサーバーのインスタンスをもう一台追加し、新規作成したELB配下に2つのインスタンスをおきます。



ELBのリスナーを以下のように80番と3000番を設定します。


そしてELBのエンドポイントのURLをブラウザで開きます。


すると、xhr-pollingになり、接続と切断が繰り返されます。
また、画面を2つ開いてメッセージを送信しても相手に届かない場合があります。

注意点1:ELBはhttpではなくtcpでポートを設定


websocket通信ではクライアントとサーバーの間のハンドシェイクにUpgradeヘッダを送信します。
しかし、ELBはhttpリスナーの場合Upgradeヘッダを削ってしまうようです。
RFC6455 — The WebSocket Protocol 日本語訳
ELBでHTTPリスナーだとWebSocketは使えない

そのため、Socket.IOはwebsocketが使えないと判断し、次善策の一つとして通常のhttp通信でxhr-pollingで接続することになります。これはajaxの通信と同じです。
そこで、ELBでは上記リンクの通り、websocket用のリスナーはhttpの3000番ではなくtcpの3000番を設定する必要があります。



注意点2:Redisでセッション共有を行う


また、接続や切断が繰り返されるのはxhrのポーリングのたびにハンドシェイクが確立したサーバーとは別のサーバーに接続にいくからのようです。これはELBのリスナー設定をtcpにした場合も同様で、一度websocket通信が確立したかのように見えても、次に接続した時に別のサーバーにつながると、接続が切れたもしくはwebsocketに失敗したと判断し xhr-pollingなど他の方法で通信しようとするようです。

また、そもそもの問題として2つのnodeサーバーの間で接続情報(セッション)が共有されないので、ELBを通してnode1とnode2にそれぞれ接続したクライアント間ではメッセージのやりとりができません。

そこで、nodeサーバーのバックエンドとして、redisを利用してセッション共有を行う方法が有効です。
socket.ioはセッションを保持する方式としてローカルメモリを使用するMemoryStoreを使用しますが、オプションでRedisにを使用RedisStoreが選べます
Configuring Socket.IO

これを使います。
redisサーバーを追加し、redisを起動しておきます。
インスタンスにはセキュリティグループなどでredisで使用するポートを開放しておきます。



そして、以下のようにserver.jsを変更します。

server.js

これで、redisサーバーでnode1,node2のセッションを共有できます。


何度か試しましたが、うまく通信できているようです。


まとめ


注意点としては、ELBをつかった場合はtcpでリッスンすることと、ELBにかぎらずnodeサーバーをスケールする場合は、redisサーバーでセッション共有する必要があります。

以上です。

2012年12月21日金曜日

AWS SDK for Node.js ってなんじゃ?

遅ればせながら、AWS SDK for Node.js が出ました。

簡単に触ってみます。

インストール

npm install aws-sdk


S3にアクセス

test.js
/*
AWSオブジェクトを初期化します。
*/
var AWS = require('aws-sdk');

/*
アカウント情報やリージョンなどを設定します。
*/
AWS.config.update({accessKeyId: 'xxxxxxxxxxxxx',
                   secretAccessKey: 'yyyyyyyyyyyyyyyyyyyyyyy',
                   region: 'ap-northeast-1'});

/*
 このようにしてもOKです。
 AWS.config.loadFromPath('./credentials.json');
*/


/*
S3オブジェクトを作ります。
*/
var s3 = new AWS.S3();

/*
バケットのオブジェクト一覧を取得するには以下のようにします。
*/
s3.client.listObjects({Bucket:"memorycraft-log"}, function(err,data){
  if(err){
    console.log(err);
  }
  else{
    for(var i=0;i<data.Contents.length;i++){
      console.log(data.Contents[i].Key);
    }
  }
});


基本的には、呼び出し→コールバックという一般的なnode.jsの作法ですね。
結果は以下のように正常に取得できます。

# node test.js
logs/
logs/20121221%3F--num-instances_$folder$
logs/20121221/
logs/20121221/ip-10-128-12-239-04_0.gz
logs/20121221/ip-10-128-12-239-04_1.gz
logs/20121221/ip-10-128-12-239-04_10.gz
logs/20121221/ip-10-128-12-239-04_2.gz
logs/20121221/ip-10-128-12-239-04_3.gz
logs/20121221/ip-10-128-12-239-04_4.gz
logs/20121221/ip-10-128-12-239-04_5.gz
logs/20121221/ip-10-128-12-239-04_6.gz
logs/20121221/ip-10-128-12-239-04_7.gz
logs/20121221/ip-10-128-12-239-04_8.gz
logs/20121221/ip-10-128-12-239-04_9.gz
logs/20121221/ip-10-128-12-239-04_98.gz
logs/20121221/ip-10-128-12-239-04_99.gz
logs/20121221/ip-10-157-2-63-04_0.gz
logs/20121221/ip-10-157-2-63-04_1.gz
logs/20121221/ip-10-157-2-63-04_10.gz
logs/20121221/ip-10-157-2-63-04_100.gz
logs/20121221/ip-10-157-2-63-04_2.gz
logs/20121221/ip-10-157-2-63-04_3.gz
logs/20121221/ip-10-157-2-63-04_4.gz
logs/20121221/ip-10-157-2-63-04_5.gz
logs/20121221/ip-10-157-2-63-04_6.gz
logs/20121221/ip-10-157-2-63-04_7.gz
logs/20121221/ip-10-157-2-63-04_8.gz
logs/20121221/ip-10-157-2-63-04_9.gz

Dynamoにアクセス
/*
AWSオブジェクトを初期化します。
*/
var AWS = require('aws-sdk');

/*
DynamoDBオブジェクトをつくります。
*/
var ddb = new AWS.DynamoDB();

/*
投入するデータは以下のようにします。
*/
var data = {
    id:{S:'msg_'+(new Date()).getTime()},
    msg:{S:'hoge'},
    datetime:{S:(new Date())}
};

/*
投入します
*/
ddb.client.putItem({
    TableName:'nodetest',
        Item:data
    },
    function(err, res) {
        if(err){
            console.log("Error [" + err.code + "] id=" + data.id.S);
        }
        else{
            console.log("Done id=" + data.id.S);
        }
});


結果は以下のようになります。
# node test.js 
Done id=msg_1356091073919


AWSコンソールでみると


正常に登録されたことがわかります。

他にもEC2とSWFにアクセスできますが、他のサービスはまだまだこれからのようです。
今後に期待ですね。

以上です。


2012年12月11日火曜日

CDPってなんじゃ?(通知に関するオレオレパターンでCloudwatchからサイレンをならす)


CDP Advent Calendar 2012へ参加しました。

CDPのパターンリストを見ていると純粋に勉強になったり、知らずに実践していたパターンなどがあったり、見ていて楽しいです。

何を書くか迷いましたが、オレオレパターンを定義してみました。

普段運用などでCloudWatchからのアラートがよく飛び交っていますが、うっかり気づかなかったりすると困ることがあります。
SNSを使用した通知はインフラ内で使用されることが多いような気がしますが、HTTP Notificationを使用してアプリケーションサーバーからクライアントまで含めたプッシュ通知の可能性を探ってみたいと思います。

名前をつけるとしたらDeep Notificationでしょうか。
構成は以下の様なイメージです。



今回は例として、CloudWatchでアラートが発生したら、管理画面にプッシュしてWebAudioでサイレンを鳴らしてみたいと思います。

 まず、SNSに"sound"というトピックを立てます。




次にEC2インスタンス内で、HTTP通知先になるWEBアプリをつくります。
今回はFuelPHPで、/monitor/alert のアクションを通知先とします。

SNSのメール通知では購読確認はメールのリンクを踏めばOKですが、HTTP通知では登録後通知先のURLにSNSがアクセスし、その際のJSONパラメータに含まれる情報を元に、SNSの認証APIをリクエストするという方式のようです。

/monitor/alert のアクションで、以下のようにJSONリクエストからTopicArnとTokenを抽出し、SNSに購読確認を行うようにしておきます。

fuel/app/class/controller/monitor.php
     public function action_alert()
     {

          $sns = new AmazonSNS(array('key'=>'xxxxxxxxxxxxxxxxxx',                                               
                                      'secret'=>'xxxxxxxxxxxxxxxxxxxx'));
          $sns->set_region(AmazonSNS::REGION_APAC_NE1);
          $input = file_get_contents('php://input');
          $input =json_decode($input, true);

          if ($input["Type"] === 'SubscriptionConfirmation') {
               $response = $sns->confirm_subscription(
                                $input["TopicArn"], 
                                $input["Token"]);
          }
     }


次に、SNSの"sound"トピックで、購読者の登録を行います。
プロトコルはHTTP、エンドポイントは/monitor/alert アクションのURLとします。



すると、すぐにSNSがエンドポイントにアクセスし、認証が完了すると、以下のようにSubscriptin IDにsnsのarn値がセットされます。
これで認証が完了しました。

すでに認証が完了しているので、このコードは不要なのでコメントアウトし、実際に通知があったときの処理を書きます。
SNSから送信されたJSONデータをredisにPublishするようにします。

fuel/app/class/controller/monitor.php
     public function action_alert()
     {

/*
          $sns = new AmazonSNS(array('key'=>'xxxxxxxxxxxxxxxxxx',
                                     'secret'=>'xxxxxxxxxxxxxxxxxxxx'));
          $sns->set_region(AmazonSNS::REGION_APAC_NE1);
          $input = file_get_contents('php://input');
          $input =json_decode($input, true);

          if ($input["Type"] === 'SubscriptionConfirmation') {
               $response = $sns->confirm_subscription(
                              $input["TopicArn"], 
                              $input["Token"]);
          }
*/

          $input = file_get_contents('php://input');

          $redis = Redis::instance('default');
          $redis->publish('alert', $input);

          $this->template->title = 'Monitor » Alert';
          $this->template->content = View::forge('monitor/alert');
     }


次に、node.jsで、redis経由でSubscribeしたJSONデータをユーザーにプッシュするようにします。

node/server.js
var server = require('http').createServer(function(req, res){
  res.writeHead(200, {'Content-Type': 'text/html'});
  res.end('server connected');
});
server.listen(3000);

var io = require('socket.io').listen(server);
var opts = {host:'127.0.0.1', port:6379};

var redis = require('redis');
var sub = redis.createClient(opts.port, opts.host);
sub.subscribe('alert');
sub.on("message", function(channel, message){
  io.sockets.emit('alert', JSON.parse(message));
});


また、アラート通知画面を別途用意し、alertイベントを受信したら情報を表示してWebAudioを鳴らすようにします。

/fuel/app/views/monitor/index.php
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js" type="text/javascript"></script>
<script src="/assets/js/client.js" type="text/javascript"></script>

<div class="container">
<div class="hero-unit">
<span id="timestamp" style="color: red;"></span>
<h3 id="subject" style="color: red;"></h3>
<dl class="dl-horizontal" id="message"></dl>
<button class="btn" id="stop" type="button">Stop</button>
  </div>
</div>


/public/assets/js/client.js
$(function(){

  var context = new webkitAudioContext();
  var band = 24000;
  var buf = context.createBuffer(1, band, band);
  var data = buf.getChannelData(0);
  for (var i = 0;i < data.length;i++) {
        data[i] = i > band*0.5 ? 0 : ((i % 100) < 50 ? 1 : 0);
  }
  var src = context.createBufferSource();
  src.buffer = buf;

  $.getScript("http://"+location.hostname+":3000/socket.io/socket.io.js", function(){
    var socket = io.connect('http://'+location.hostname+':3000/');
    socket.on('connect', function() {
    });

    socket.on('alert', function(data){
      $("#timestamp").text(data.Timestamp);
      $("#subject").text(data.Subject);
      $("#message").empty();
      var msg = JSON.parse(data.Message);
     for(var p in msg){
        $("#message").append("<dt>"+p+"<dt><dd>"+msg[p]+"</dd>");
     }
      src.connect(context.destination);
      src.loop = true;
      src.noteOn(0);
    });

  });

  $("#stop").click(function(){
    src.disconnect();
  });
});



そして、試しにCloudWatchで監視対象のインスタンスの10%のCPUでアラートがでるようにしておきます。



それでは監視対象のインスタンスで無限ループを動かして、様子を見てみます。


10%を超えたラインでしばらくするとプッシュ通知され、サイレンが鳴りました!!

運用だけでなくバッチの完了通知やGrowlへの通知など、HTTP通知を使ってSNSをアプリケーション・サーバーのほうに一歩踏み込ませることで、通知に関する自由度が一気に高まり、運用効率などに効果があるのではないかと思います。

以上です。

2012年12月2日日曜日

Node.jsってなんじゃ?(knox:S3にアクセス)


AWS界隈でもAdvent Calendarが流行っているようで、CloudpackでもAdvent Calendarが始まりました。

cloudpack Advent Calendar 2012

社内でやると、ただのブログの催促のような気もしますが、1日目のsuz-labさんに続いて、2日目を担当します。

最近node.jsを実案件で利用することがいくつかあり、AWSサービスをnodeから利用したいケースが結構あります。そこで今回はnodeからS3へファイルをアップロードしてみたいと思います。

nodeでS3アクセスするには、knoxというモジュールがあります。
他にもs3-clientというknoxをもとにした簡易機能のモジュールがありますが、簡単さと引換にヘッダーを付けられないなどの制限があるので、いろいろやりたい場合はknoxがよいかと思います。

今回は、redisからのpublishをトリガーにしてメッセージ内容をS3へJSONファイルとしてアップロードしてみたいと思います。

まず、S3にはアップロード用のバケットを用意しておきます。
ここではmemocra-jsonというバケットにしました。
内容を確認しやすいように、webサイト設定をしておきます。




そしてEC2側では、既にnode, redis本体はインストールされている前提で、
knox, redisのnodeモジュールを追加でインストールします。
# npm install -g knox
# npm install -g redis


次にコードを書きます。
knox.createClientでアクセスキー、シークレットキーと対象バケットを指定し、クライアントオブジェクトを作成します。
var knox = require('knox');var s3Client = knox.createClient({
    key:'xxxxxxxxxxxxxx',
    secret:'yyyyyyyyyyyyyyyyyyyyyyyyyyyy',
    bucket:'memocra-json'
});


また、redisサーバーのjson_createというチャンネルをsubscribeするようにします。
var redis = require('redis');
var sub = redis.createClient({host:'10.0.0.200', port:6379});
sub.subscribe('json_create');


受信時には、受信内容と日時をjson化して、その文字列をknoxクライアントのputメソッドでS3へアップロードします。
この際、ヘッダーにpublic-readをつけることで、WEBアクセスが可能になります。
sub.on('message', function(channel, message){
    var json = JSON.stringify({msg:message, datetime:(new Date())});
    var s3_path = 'json/'+message+'.json';
    var headers = {'Content-Length': json.length,
                    'Content-Type': 'application/json',
                    'x-amz-acl': 'public-read'};
    var req = s3Client.put(s3_path, headers);
    req.on('response', function(res){
        if(res.statusCode != 200){
            console.log("Error! code="+res.statusCode);
        }
        else{
            console.log("Done!");
        }
    });
    req.on('error', function(err){
        console.log('Error='+err);
    });
    req.end(json);
});


ここまで書けたら、起動します。
$ node sample.js


次にredis-cliを起動し、json_createチャンネルでpublishします。
# redis-cli
redis 127.0.0.1:6379> publish json_create hello
(integer) 1
redis 127.0.0.1:6379> publish json_create konnichiwa
(integer) 1


すると、node側では完了メッセージが2つ表示され、2件の処理が終わったことがわかります。
$ node sample.js
Done!
Done!


バケットを見ると、ちゃんと2つファイルができています。





ブラウザで中身を見てみます。




内容も正しいようです。


もちろんこれ以外にも、ファイルをそのままアップロード、ダウンロード、削除する機能などもあり、
nodeでもS3のファイルを操作することができます。

以上です。

2012年11月14日水曜日

Node.jsってなんじゃ?(DynamoDBにアクセス)

いままでチャットプログラムをサンプルとして使用していましたが、やりとりしているメッセージは基本的に揮発性のものです。サーバーが止まればなくなってしまう、もしくは参照できなくなるデータです。

今回は、AmazonDynamoDBをメッセージの保存場所としてみます。
node.js でDynamoDBを利用するには、dynodeというモジュールを使用します。

dynodeはDynamoDBへのAPIアクセスをラップしてnodeライクに使うことができます。

では早速触ってみたいと思います。
まずdynodeをインストールします。
# npm install -g dynode

ロードと設定は以下のように行います。
var dynode = require('dynode');
dynode.auth({region:"ap-northeast-1",
                accessKeyId:"xxxxxxxxxxxxxxxxxx",
                secretAccessKey:"yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"});


また、各メソッドは以下のようにコールバック式で行います。
dynode.putItem("chat", {id:id, msg:msg, date:(new Date()).toString()}, function(err, res){
  console.log(err);
});


ここでは、メッセージの保存先として、idというプライマリキーと、dateをレンジキーをもつchatというテーブルを作成します。


サンプルとして以前のチャットプログラムを使用します。

server.js

connectionハンドラで、接続が確率したときにchatテーブルから過去のメッセージをスキャンしてarchiveイベントに乗せてクライアントにemitします。
また、msgハンドラでは、いままで単純にemitしていたところを、dynode.putItemで保存してからemitするようにしました。
var server = require('http').createServer(function(req, res){
  res.writeHead(200, {'Content-Type': 'text/html'});
  res.end('server connected');
});
server.listen(3001);

var dynode = require('dynode');
dynode.auth({region:"ap-northeast-1",
                accessKeyId:"xxxxxxxxxxxxxxxxxx",
                secretAccessKey:"yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"});

var io = require('socket.io').listen(server);
var RedisStore = require('socket.io/lib/stores/redis');
opts = {host:'10.0.0.200', port:6379};
io.set('store', new RedisStore({redisPub:opts, redisSub:opts, redisClient:opts}));

io.sockets.on('connection', function (socket) {

  var id = socket.id;
 //メッセージ履歴をscanします
  dynode.scan("chat", function(err, res){
  //dateでソートします
  res.sort(function(a,b){
     if(a.date < b.date)return -1;
      if(a.date > b.date)return 1;
      return 0;
    });
    //archiveイベントに送信します。
    io.sockets.emit('archive', { id:id, archive: res });
    io.sockets.emit('info', { id:id, msg: 'welcome '+id });
  });

  socket.on('msg', function (msg) {
    io.sockets.emit('msg', {id:id, msg: msg});<
              //保存します。
    dynode.putItem("chat", {id:id, msg:msg, date:(new Date()).toString()}, function(err, res){
        if(err){
            console.log(err);
            console.log(res);
        }
    });
  });
  socket.on('disconnect', function(){
    io.sockets.emit('info', {id:id, msg: 'bye '+id});
  })
});


client.js

クライアント側では、新たにarchiveハンドラを作成し、メッセージをまとめて展開できるようにしておきます。

$(function(){
    var socket = io.connect('http://'+hostname+':3001/');
    socket.on('connect', function() {
      $("#log").html($("#log").html() + "<br />" + 'connected');
      socket.on('info', function (data) {
        $("#log").html($("#log").html() + "<br />" + data.msg);
      });
      socket.on('archive', function(data){
                     //メッセージ履歴を表示します。
      for(var i=0;i<data.archive.length;i++){
      $("#log").html($("#log").html() + "<br />" + "<b>" +       data.archive[i].id + ":</b>" + data.archive[i].msg);
      }
      });

      socket.on('msg', function(data){
        $("#log").html($("#log").html() + "<br />" + "<b>" + data.id + ":</b>" + data.msg);
      });

      $("#send").click(function(){
        var msg = $("#msg").val();
        if(!msg){
          alert("input your message");
          return;
        }
        socket.emit('msg', msg);
      });
    });
  });


これで完了です。それでは動かして見ましょう。
以下のように、メッセージを投稿すると、



以下のようにDynamoに登録されているのがわかります。


ひとしきりメッセージを送ったあと、別のブラウザで接続すると、過去のメッセージが表示されるようになりました。


このように、nodeでもAWSに接続できるモジュールもいくつかあるので、幅が広がりますね。
以上です。

2012年11月12日月曜日

redisってなんじゃ?(FuelPHPの管理画面からSocket.IOで全員にPush)

node.js、とりわけSocket.IOでは、ユーザー同士のリアルタイム通信が簡単にに行えることがわかってきました。
ですが、時には管理画面や、バッチの処理によってユーザーに何か通知をしたいことがあるかもしれません。
今度はredisを利用して、システム管理者とユーザーの間でリアルタイム通信を行なってみます。

以前紹介したFuelPHPもまた、キャッシュストアや、クライアントインターフェースとしてRedisをサポートしています。
そこで、FuelPHPで管理画面をつくり、ボタンを押すとチャット中のユーザーに管理者からのメッセージを表示させたいと思います。

チャットプログラムや構成は前々回の構成のまま、node1, node2という2つのサーバーがredisサーバーにつながってセッション共有されている状態とします。
今回は、以下のように、さらにadminサーバーを追加し、管理画面を置きます。

    +----------+       +---------+
    | admin    |       | redis   |
    |----------| pub   |---------|
    | fuelphp  +------->         |
    |          | sub   | redis   |
    | admin.js <-------+         |
    +-----+----+       +---------+
          |
          +-----------------+
          |      emit       |
    +-----v----+       +----v----+
    | node1    |       | node2   |
    |----------|       |---------|
    | chat.js  |       | chat.js |
    +----------+       +---------+

上図のように、fuelphpからredisにpublishしたものを同じadminサーバー上のsocket.ioでsubscribeし、チャットサーバーへブロードキャストするイメージです。
チャットサーバーが直接subscribeすることも可能ですが、そうすると、チャットサーバーの数だけsubscribe→emitが発生して、メッセージが重複してしまいます。subscribe→emit役は1つである必要があるので、adminサーバーに兼任させます。
なので、既存のチャットサーバーには手を加える必要がなく、adminサーバーだけ用意して実装すればいいわけです。

それでは実際に用意してみます。


まず必要なユーザーやライブラリを用意します。
# useradd appadmin
# passwd appadmin
# yum install -y vim git wget php  gcc gcc-c++ make
# curl get.fuelphp.com/oil | sh


FuelPHPの最新版(v1.4)では、タイムゾーンの指定が必至になったのでphp.iniで設定します。
# vim /etc/php.ini
---
date.timezone = Asia/Tokyo
--


次にアプリユーザーのホームディレクトリをApacheからアクセス可能にします。
# chmod 755 /home/appadmin


アプリを作成します。
publishボタンを配置する画面(index)とpublishボタンの押下(publish)の2つのアクションを用意します。
# su - appadmin
$ oil create app
$ cd app
$ oil g controller greeting index publish


次に、DocumentRootをFuelPHPのpublicディレクトリに向けます。
$ exit
# cd /var/www/ 
# mv html html.org
# ln -s /home/appadmin/app/public html


そして、Apacheの設定をシンボリックリンクをたどり、.htaccessを許可するように変更し、起動します。
# vim /etc/httpd/conf/httpd.conf
---

<Directory "/var/www/html">
~略~
    Options FollowSymLinks
~略~
    AllowOverride All
~略~
</Directory>

---
# /etc/init.d/httpd start 


次にFuelPHPのconfig/db.phpおよび、viewとcontrollerを以下のように実装します。

fuel/app/config/db.php
redisの接続先を指定します。チャットサーバーが参照しているのと同じサーバーに接続させるようにします。
$ vim fuel/app/config/db.php
<?php

/**
 * Use this file to override global defaults.
 *
 * See the individual environment DB configs for specific config information.
 */

return array(

    'redis' => array(
        'default' => array(
            'hostname' => '10.0.0.200',
            'port'     => 6379
        )
    ),

);


fuel/app/views/greeting/index.php
Publishボタンを配置します。
$ vim fuel/app/views/greeting/index.php
---
<p>Index</p>

<?php echo Form::open('greeting/publish'); ?>
<?php echo Form::submit('publish', 'publish'); ?>
<?php echo Form::close(); ?>
---


fuel/app/classes/controller/greeting.php
publishアクション内で、Redisクラスをインスタンス化しています。
ここでの引数defaultは、db.phpで設定したラベルを指定します。
そして、publishメソッドを呼び出すことにより、redisサーバーに「greeting」というチャンネルでpublishを行います。
$ vim fuel/app/classes/controller/greeting.php
---
<?php

class Controller_Greeting extends Controller_Template
{

    public function action_index()
    {
        $this->template->title = 'Greeting &raquo; Index';
        $this->template->content = View::forge('greeting/index');
    

    public function action_publish()
    {

        $redis = Redis::instance('default');
        $redis->publish('greeting', 'おはよう諸君!!');

        $this->template->title = 'Greeting &raquo; Publish';
        $this->template->content = View::forge('greeting/publish');
    }
}
---


ここまでで、管理画面自体は完成です。
以下のようにPublishボタンがあるだけのシンプルな画面になっています。




つづいて、同じadminサーバーに、node.jsを入れて動かします。
モジュールはforever, socket.io, redisを入れます。
socket.ioにもredisが含まれているのですが、requireのしやすさやバージョンが新しさなどのため、別途いれておきます。
# cd /usr/local/src
# wget http://nodejs.org/dist/v0.8.14/node-v0.8.14.tar.gz
# tar xzvf node-v0.8.14.tar.gz
# cd node-v0.8.14
# ./configure  
# make
# make install
# curl https://npmjs.org/install.sh | sh
# npm install -g socket.io
# npm install -g forever
# npm install -g redis


これでnodeのインストールができました。
それでは、subscribe→emit用にnodeのスクリプトを実装します。
# su - appadmin
$ mkdir -p /home/appadmin/admin/node
$ cd /home/appadmin/admin/node
$ vim admin.js
---

var server = require('http').createServer(function(req, res){
  res.writeHead(200, {'Content-Type': 'text/html'});
  res.end('server connected');
});
server.listen(3001);

var io = require('socket.io').listen(server);
var RedisStore = require('socket.io/lib/stores/redis');
var opts = {host:'10.0.0.200', port:6379};
io.set('store', new RedisStore({redisPub:opts, redisSub:opts, redisClient:opts}));

var redis = require('redis');
var sub = redis.createClient(opts.port, opts.host);
sub.subscribe('greeting');
sub.on("message", function(channel, message){
  io.sockets.emit('msg', {msg:message});
});

---

ポイントは、チャット用のスクリプトと同じようにredisサーバーをRedisStoreとしてstore登録することと、
それとは別にsubscribe用のredisクライアントを作り、「greeting」チャンネルをsubscribeし、受信したメッセージをチャット参加者全員にemitするようにするところです。

これを起動します。
$ forever start admin.js

それでは、実際に動かしてみましょう。
まず、前回と同様、node1とnode2のチャットウィンドウを開いて、適当にチャットしてみます。


それでは、ここで先程の管理画面でPublishボタンを押してみます。



そして、チャット画面をみてみます。


おお!メッセージが表示されました。
これで管理画面からのコントロールも可能になりました。

以上です。

2012年11月10日土曜日

Node.jsってなんじゃ?(redisでSocket.IOをスケール)


前回は、redisをインストールして生でつかってみました。
今回はnode.jsでredisを利用してみたいと思います。

マルチユーザーのサーバーでのプッシュ配信はSocket.IOが定番ですが、
サーバーが増えた時にある問題が生じます。
例えばサーバーを2つに増やして、サーバーAでブロードキャストしても
サーバーBのクライアントでは受信できないのです。

以前の記事で作成したチャットプログラムを例にしてみます。

サーバー側のjs
$ cat /home/appadmin/chat/node/chat.js
var server = require('http').createServer(function(req, res){
  res.writeHead(200, {'Content-Type': 'text/html'});
  res.end('server connected');
});
server.listen(3001);

var io = require('socket.io').listen(server);
io.sockets.on('connection', function (socket) {
  socket.emit('info', { msg: 'welcome' });
  socket.on('msg', function (msg) {
    io.sockets.emit('msg', {msg: msg});
  });
  socket.on('disconnect', function(){
    socket.emit('info', {msg: 'bye'});
  });
});


クライアント側のjs

$ cat /home/appadmin/chat/public/assets/js/client.js
$(function(){
    var socket = io.connect('http://'+location.hostname+':3001/');
    socket.on('connect', function() {
      $("#log").html($("#log").html() + "<br />" + 'connected');
      socket.on('info', function (data) {
        $("#log").html($("#log").html() + "<br />" + data.msg);
      });
      socket.on('msg', function(data){
        $("#log").html($("#log").html() + "<br />" + "<b>" + data.msg + "</b>");
      });
      $("#send").click(function(){
        var msg = $("#msg").val();
        if(!msg){
          alert("input your message");
          return;
        }
        socket.emit('msg', msg);
      });
    });
  });


画面
$ cat /home/appadmin/chat/public/index.html
<!DOCTYPE html>
<html>
<head>
     <meta charset="UTF-8">
     <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js" type="text/javascript" charset="utf-8"></script>
     <script type="text/javascript">
     $(function(){
         function load(){
             $.getScript("assets/js/client.js");
         }
         $.getScript("http://" + location.hostname + ":3001/socket.io/socket.io.js", function(){
             load();
         });
     });
     </script>
     <title>Node A</title>
</head>
<body>
     <input id="msg" type="text" style="width:400px;"></input>
     <input id="send" type="button" value="send" /></br >
     <div id="log" style="width:400px;height:400px;overflow:auto;border:1px solid #000000;"></div>
</html>


サーバー側のjsをforeverで起動します。
$ forever start chat.js

これをサーバーAとします。
同じ内容を別のサーバーBに配置し、同じようにnodeを起動します。

2つのサーバーにアクセスすると、以下のように同じ画面が表示されます。




サーバーAで「a」と入力します。
サーバー側では接続した全ユーザーに投稿内容をブロードキャストし、ユーザーの画面に「a」が表示されます。
しかし、サーバーBには何も表示されません。
おなじようにサーバーBで、「b」と投稿してもサーバーAに接続した画面にはなにも表示されません。
接続がサーバーAとサーバーBで共有されていないためです。

ここで登場するのがredisです。
Socket.IOではデフォルトで接続情報をローカルメモリに保存しています。
これをMemoryStoreと呼びますが、Socket.IOにはRedisStoreというredisに接続情報を保存するオプションも存在します。
このオプションを選択することで、分散されたnodeサーバーが同じredisサーバーを参照し
各nodeの接続情報を共有することができます。


それではサーバー側のjsを修正してRedisStoreを使ってみます。
接続するredisは前回設定したredisサーバーにします。

var server = require('http').createServer(function(req, res){
  res.writeHead(200, {'Content-Type': 'text/html'});
  res.end('server connected');
});
server.listen(3001);

var io = require('socket.io').listen(server);

//RedisStoreを読み込みます
var RedisStore = require('socket.io/lib/stores/redis');
//redisサーバーの接続先情報を定義します
opts = {host:'10.0.0.200', port:6379};
//storeをRedisStoreにし、redisPub, redisSub, redisClientをredisサーバーに向けます
io.set('store', new RedisStore({redisPub:opts, redisSub:opts, redisClient:opts}));

io.sockets.on('connection', function (socket) {
  socket.emit('info', { msg: 'welcome' });
  socket.on('msg', function (msg) {
    io.sockets.emit('msg', {msg: msg});
  });

  socket.on('disconnect', function(){
    socket.emit('info', {msg: 'bye'});
  });
});


これで再起動します。
$ forever restart chat.js


再度2つの画面をリロードして、サーバーAの画面に「a」サーバーBの画面に「b」と入力してみます。




おお、両方の画面に「a」「b」が表示されました!
これで台数が増えてもすべてのユーザーが同じ空間でコミュニケーションすることができます。

以上です。

2012年11月7日水曜日

Fluentdってなんじゃ?(Mongo&Node編 ブラウザでtail)

前回までで、fluentdを利用してApacheのログをmongoDBに保存することができました。

構成はこのようになっていました。
  +----------------------------+   +------------------------+
  | web server (10.0.0.8)      |   | mongo server(10.0.0.16)|
  |----------------------------|   |------------------------|
  |          fluentd           |   |                        |
  |                            |   |                        |
  | +---------+     +--------+ |   |      +----------+      |
  | | input   |     | output | |   |      |  fluent  |      |
  | |---------|+--> |--------| +--------->|----------|      |
  | |  tail   |     |  mongo | |   |      |   test   |      |
  | +---------+     +--------+ |   |      +----------+      |
  +----------------------------+   +------------------------+

今回はnode.jsを使用して、mongoDBに保存されたログをブラウザでリアルタイムにtailしてみます。
構成は以下のとおりです。
  +----------------------------+   +-------------------------+
  | web server (10.0.0.8)      |   | mongo server(10.0.0.16) |
  |----------------------------|   |-------------------------|
  |          fluentd           |   |         mongod          |
  |                            |   |                         |
  | +---------+     +--------+ |   |      +----------+       |
  | | input   |     | output | |   |      |  fluent  |       |
  | |---------|+--> |--------|+---------->|----------|       |
  | |  tail   |     |  mongo | |   |      |   logs   |       |
  | +---------+     +--------+ |   |      +-----+----+       |
  +----------------------------+   +------------|------------+
                                                |
                                 +--------------+
                                 | +-------------------------+
                                 | | node server(10.0.0.100) |
                                 | |-------------------------|
                                 | |         node.js         |
                                 | |                         |
                                 | |      +-----------+      |
                                 +------->| mongoose  |      |
                                   |      |-----------|      |
                                   |      | socket.io +----------> Brower
                                   |      +-----------+      |
                                   +-------------------------+


今回、web serverとmongo serverは、ほとんど前回の設定のままですが、
新規nodeサーバーでmongodbにアクセスするmongooseはデフォルトで複数系の名前のコレクションにアクセスするようになっています。
そこで、webサーバーのtd-agentのmongo outputプラグインの書き出し先をlogsという名前に変更しておきます。
また、リアルタイムに表示させたいのでflush_intervalを0sに設定しておきます。
<source>
  type tail
  format apache
  path /var/log/httpd/access_log
  tag apache.access
</source>

<match apache.access="apache.access">
  type mongo
  flush_interval 0s
  database fluent
  collection logs

  host 10.0.0.16
  user memorycraft
  password *******
</match>


それでは、nodeサーバーを設定します。
新規にEC2インスタンスをたちあげてnodeとhttpdをインストールします。
# yum install httpd -y
# /etc/init.d/httpd start

# yum install -y wget
# cd /usr/local/src/
# wget http://nodejs.org/dist/v0.8.14/node-v0.8.14.tar.gz
# tar xzvf node-v0.8.14.tar.gz 
# cd node-v0.8.14
# ./configure
# make
# make install


次にnpmをインストールしてからsocket.io, log, forever, mongooseをnpmインストールします。
# curl https://npmjs.org/install.sh | sh
# npm install -g socket.io
# npm install -g forever


nodeを動かすためのユーザーを作成します
# useradd appadmin
# passwd appadmin
# chmod 755 /home/appadmin
# su - appadmin


ノードモジュールへのパスを通すために環境変数を設定します。
$ vi ~/.bash_profile 
$ source ~/.bash_profile 
---
export NODE_PATH=/usr/local/lib/node_modules
---

$ source ~/.bash_profile


ここで、nodeサーバーとHTML用のコンテンツ置き場を用意します。
ディレクトリ構成は以下のとおりです。
cd ~/nodetest
tree
.
|-- node
|   |-- logs
|   |   `-- app.log
|   `-- server.js (nodeサーバー)
`-- public
    |-- assets
    |   |-- css
    |   |-- img
    |   `-- js
    |       `-- client.js (nodeクライアント)
    `-- index.html (tail用の画面)


また、publicをhttpdのDocumentRootにするため、以下のように設定します。
$ su -
# cd /var/www
# mv html html.org
# ln -s /home/appadmin/nodetest/public html
# vi /etc/httpd/conf/httpd/conf
---
~略~
<directory html="html" var="var" www="www">
    Options FollowSymLinks
    AllowOverride All
~略~
---
</directory>

# /etc/init.d/httpd restart


そして、httpdとnodeに接続させるため、SecurityGroupを以下のように設定します。



次にサーバーとコンテンツを実装してみます。 それぞれの内容は以下の通りです。

node/server.js
//3001番でlistenします
var server = require('http').createServer(function(req, res){
  res.writeHead(200, {'Content-Type': 'text/html'});
  res.end('server connected');
});
server.listen(3001);

//logsコレクションのスキーマ定義をしておきます。
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var AccessSchema = new Schema({
    host: String,
    user: String,
    method: String,
    path: String,
    code: Number,
    size: Number,
    referer: String,
    agent: String,
    time : Date
});
mongoose.model('log', AccessSchema);

//fluentデータベースに接続します。
mongoose.connect('mongodb://10.0.0.16/fluent');
var Access = mongoose.model('log');

//socket.ioを起動します。
var io = require('socket.io').listen(server);
io.sockets.on('connection', function (socket) {
    /**
     * クライアント接続時に、初期データとして、
     * 保存されているlogsコレクションをすべてemitします。
     */
    Access.find({}).sort({time:1}).exec(function(e, docs){
        if(e){
        }
        else{
            socket.emit('init', docs);
        }
    });
});

/** 
 * 1.5秒おきに最終取得ログの日時以降のログを取得して
 * 全員にブロードキャストし、最終日時を保存します。
 */
var last = new Date();
setInterval(function(){
    Access.find({}).where('time').gt(last).sort({time:1}).exec(function(e, docs){
        if(e){
        }
        else{
            for(var i=0; i<docs.length; i++){
                if(last < docs[i].time){
                    last = docs[i].time;
                }
            }
            io.sockets.emit('log', docs);
        }
    });
}, 1500);


public/assets/js/client.js
$(function(){
  //接続します
  var socket = io.connect('http://' + location.hostname + ':3001/');
  socket.on('connect', function() {
   /**
     * 接続成功時に初期データを時系列降順で表示させます
     */
    socket.on('init', function(data){
        $("#log").empty();
        for(var i=0; i<data.length; i++){
            row = data[i];
            $("#log").prepend(row.host+" "+row.time+" "+row.path+"<br/>");
        }
    });

    /**
     * 随時送られてくるデータは一番上に表示させます
     */
    socket.on('log', function(data){
        for(var i=0; i<data.length; i++){
            row = data[i];
            $("#log").prepend(row.host+" "+row.time+" "+row.path+"<br/>");
        }
    });

  });
});


public/index.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
<script type="text/javascript">
$(function(){
    function load(){
        $.getScript("assets/js/client.js");
    }
    $.getScript("http://" + location.hostname + ":3001/socket.io/socket.io.js", function(){
        load();
    });
});
</script>
<body>
    <div id="content">
        <h1>ログ</h1>
        <div id="log" width="500px" height="100%">
        </div>
    </div>
</body>
</html>


早速動かしてみます。
$ cd ~/nodetest/node
$ forever start server.js

これでnodeサーバーが起動しました
それでは、ブラウザを見てみます。



初期ログが表示されています。うまく動いているようです。
そこで、fluentdのtail対象サーバーであるwebサーバーへアクセスをしてみます。
わかりやすくするために、何度かリロードしておきます。


そして、さきほどのnodeサーバーの画面をみると、、、


おお!ログが追加で現れました!
これであるサーバーのログデータを別のサーバーでリアルタイムに表示させることができました。

以上です。

2011年10月20日木曜日

WebSocketってなんじゃ?(Node編4 MongoDBってなんじゃ?)

今回はMongoDBの話題です。
MongoDBはいわゆるNoSQLで、BSONというJSONのバイナリ版のような形式でデータの入出力を行い、クエリなどのデータ操作をjavascriptで行うため、サーバーサイドJavascriptと非常に相性がよく、node.jsとともによく利用されているようです。

今回はこのMongoDBをnode.jsから操作してみます。

MongoDB公式サイトにあるように、yumでインストールするために、MongoDB用のyumリポジトリを追加します。
以下のファイルを作成します。
/etc/yum.repos.d/10gen.repo
[10gen]
name=10gen Repository
baseurl=http://downloads-distro.mongodb.org/repo/redhat/os/i686
gpgcheck=0


さっそく、yumでインストールします。
yum install mongo-10gen mongo-10gen-server -y


インストールが終了したら、mongodが/etc/init.d/以下にできるので、起動します。
/etc/init.d/mongod start


MongoDBのコンソールを立ち上げてみます。
$ mongo

MongoDB shell version: 2.0.0
connecting to: test
testというdbにつながったようです。
helpと入力すると、いくつかのコマンドが紹介されています。DBの一覧を見てみます。
> show dbs
local (empty)
test (empty)
localとtestというDBがあり、中身はまだ空になっています。

MongoDBは基本的にはスキーマを定義することなく、いきなりデータを保存できます。データが保存された時点でそれがスキーマになります。
以下のように、hogeというデータを保存、その直後にhogeを参照してみます。
> db.hoge.save({id:"1", name:"HOGE"});
>
> db.hoge.find();
{ "_id" : ObjectId("4e9f8ced316d58abc218dc43"), "id" : "1", "name" : "HOGE" }

このように、クエリにあたるものもすべてjavascriptで行い、結果はJSON形式で表示されます。いかにもnodeと相性がよさそうです。


それでは、このMongoDBにnode.jsからアクセスしてみます。
node.jsからMongoDBにアクセスするには、MongoDB用のパッケージモジュールを入れる必要があります。
Mongo用のモジュールはいくつかあるのですが、今回はMongooseを利用してみます。
mongooseはnpmでインストールします。
# npm install -g mongoose

> mongodb@0.9.6-22 install /usr/local/lib/node_modules/mongoose/node_modules/mongodb
> bash ./install.sh

================================================================================
=                                                                              =
=  To install with C++ bson parser do <npm --mongodb:native="" install="" mongodb="">   =
=  the parser only works for node 0.4.X or lower                               =
=                                                                              =
================================================================================
Not building native library for cygwin
Using GNU make
mongoose@2.3.5 /usr/local/lib/node_modules/mongoose
├── colors@0.5.0
├── hooks@0.1.9
└── mongodb@0.9.6-22


以下のようにmongoにデータを投入するコードを書いてみます。
#!/usr/local/bin/node

var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/memorycraft');

var Schema = mongoose.Schema;
var ItemSchema = new Schema({title:String, auther:String, price:Number, date:Date});
var Item = mongoose.model('Item', ItemSchema);

var item = new Item();
item.title = "体脂肪計タニタの社員食堂 ~500kcalのまんぷく定食~";
item.auther = "タニタ";
item.price = 1200;
item.date = new Date(2010, 1, 21);

item.save(function(err){
  if(err){
    console.log(err.message);
  }
  else{
    console.log("保存しました");
  }
});


これを実行します。
$ ./hello_mongo.js
保存しました


ここで、実際にデータが投入されているか再度mongoのコンソールで確認してみます。
$mongo
> show dbs
local (empty)
memorycraft 0.0625GB
test 0.0625GB
>
> use memorycraft
switched to db memorycraft
>
> show collections
items
system.indexes
>
> db.items.find()
{ "date" : ISODate("2010-02-20T15:00:00Z"), "price" : 1200, "auther" : "タニタ", "title" : "体脂肪計タニタの社員食堂 ~500kcalのまんぷく定食~", "_id" : ObjectId("4e9f98394699a3fb40000001") }

memorycraftというdbが増えていて、投入したデータが入っていることが確認できました。

ここで、mongodb+nodeがいいのは、データの保持の時点でJSONなので、取得結果もJSONになっていることです。
そして、上記のコードを書いたようにアクセスの結果がすべてコールバックで記述できるので、とても柔軟にコード記述ができます。
例えば以下のように別途WEBフレームワークなどを使用した場合、取得したデータをそのままレスポンスに返すことで余計な変換処理が必要なくなります。
Item.find({}, function(err, items) {
  if(!err) {
    res.json(items);
  }
});

ほんの少し触れただけですが、とても便利なことがわかりました。
まだまだいろいろいじってみようと思います。
本日は以上です。

2011年10月19日水曜日

WebSocketってなんじゃ?(Node編3 node.jsでサーバーサイドjQuery)

再びnodeの話題です。 前回紹介したSocket.IOのように、node.jsにはnpmで管理される多数のパッケージモジュールがあり、それらをインストールすることでnodeの機能を拡張します。

今回は、jsdomというモジュールを使用してみます。
これはDOMのJS実装を、モジュール化したもので、node上でJSによるDOM解析と操作をサーバーサイドで行うことが可能です。

それでは早速試してみます。
まずnpmでjsdomをインストールしてみます。
$ npm install -g jsdom

> contextify@0.0.6 preinstall /usr/local/lib/node_modules/jsdom/node_modules/contextify
> node-waf clean || true; node-waf configure build

Nothing to clean (project not configured)
Setting srcdir to                        : /usr/local/lib/node_modules/jsdom/node_modules/contextify 
Setting blddir to                        : /usr/local/lib/node_modules/jsdom/node_modules/contextify/build 
Checking for program g++ or c++          : /usr/bin/g++ 
Checking for program cpp                 : /usr/bin/cpp 
Checking for program ar                  : /usr/bin/ar 
Checking for program ranlib              : /usr/bin/ranlib 
Checking for g++                         : ok  
Checking for node path                   : not found 
Checking for node prefix                 : ok /usr/local 
'configure' finished successfully (0.026s)
Waf: Entering directory `/usr/local/lib/node_modules/jsdom/node_modules/contextify/build'
[1/2] cxx: src/contextify.cc -> build/Release/src/contextify_1.o
[2/2] cxx_link: build/Release/src/contextify_1.o -> build/Release/contextify.node
Waf: Leaving directory `/usr/local/lib/node_modules/jsdom/node_modules/contextify/build'
'build' finished successfully (0.789s)
jsdom@0.2.8 /usr/local/lib/node_modules/jsdom 
├── htmlparser@1.7.3
├── cssom@0.2.0
├── request@2.1.1
└── contextify@0.0.6_
インストールが完了しました。


確認のために、自ら作成したDOMの一部を抽出するスクリプトを実行してみます。

hello_dom.js
#!/usr/local/bin/node

var jsdom    = require("jsdom").jsdom;
var document = jsdom("<html><head></head><body><div id='greeting'>hello world</div></body></html>"),
var window   = document.createWindow();

console.log(window.document.getElementById("greeting").innerHTML);
実行
$ chmod 755 hello_dom.js
$ ./hello_dom.js
hello world
うまく出力されました。

様々な言語にDOMを操作するライブラリがありますが、最も有名で最も使いやすいのはやはりjQueryだと思います。
ここではせっかくjavascriptを使っているので、jQueryを利用してみます。

jsdomはjQueryを実行するときに便利なenvメソッドを用意しています。 envメソッドは、ロードしたHTMLソースにscriptタグを追加し、そのscriptがDOMにロードされた後に引数のコールバック関数を呼び出します。
以下のスクリプトは、memorycraftのブログ(このブログです)のトップページから、記事のタイトルをjQueryで抽出してコンソールに出力する、いわゆるスクレイピングを行います。

hello_jquery.js
#!/usr/local/bin/node

var jsdom = require("jsdom");

jsdom.env("http://www.memocra.blogspot.com", [
  'http://code.jquery.com/jquery-1.5.min.js'
],
function(errors, window) {
  var $ = window.$;
  $("h3 a").each(function(){
    console.log($(this).text());
  });
});
実行
$ chmod 755 hello_jquery.js
$ ./hello_jquery.js
EC2でMySQL(世界編2 Spiderとレプリケーションで高速負荷分散)
EC2でMySQL(世界編1 Spiderとレプリケーションで世界進出)
SQSってなんじゃ?(AWSコンソールにSQS登場)
S3ってなんじゃ?(サーバー側でファイルのAES256暗号化)
WebSocketってなんじゃ?(Node編2 Socket.IOでプッシュ通信)

おお!うまく出力できました。

このようにjQueryをサーバーサイドで利用することで、クライアントとサーバーサイド両側で同じ方法でHTMLをコントロールすることができ、とても便利です。

WebSocket関係なくなっちゃいましたが、本日は以上です。

2011年10月5日水曜日

WebSocketってなんじゃ?(Node編2 Socket.IOでプッシュ通信)

前回Node編1で、スタンドアローンのHTTPサーバーをつくり、HTTP通信を行いました。

今回は、WebSocketを使用した通信を行ってみます。 WebSocketとは、ブラウザとサーバーの双方向通信のプロトコル規格で、現在標準化団体が仕様を策定中の技術です。chromeやsafariなど、いくつかのモダンブラウザがサポートしており、これが一般的に使用されると、いままでのサーバーアプリケーションの作り方が大きく変わってくる可能性がある、注目の技術です。

ここで使用するのは、Socket.IO というnode.jsのモジュールです。 node.jsはモジュールという単位でライブラリを管理しており、npmというモジュール管理ツールで簡単にいろいろなモジュールを追加削除できます。これはrubyでいうgemのようなものです。
Socket.IOはただnode.jsでWebSocketを実装しているだけではなく、モダンブラウザ以外のWebSocketを使用できないブラウザでも、AjaxやFlashなど他の代替手段があるかどうかを自動的に検知して通信方式を自動で選択されるので、ブラウザの対応をあまり気にしなくてもよくなっています。
また、Socket.IOは WebSocketとその代替手段において、クロスドメイン通信を可能にしています。

早速実践してみます。 まず最初にnpmをインストールします。
$ curl http://npmjs.org/install.sh | sh
$ npm install -g socket.io


socket.ioのインストールでエラーが出る場合がありますが、その場合はtarが古いので、tarを最新にしておきます。
$ cd /usr/local/src
$ wget http://ftp.gnu.org/gnu/tar/tar-1.26.tar.gz
$ tar xzvf tar-1.26.tar.gz
$ cd tar-1.26
$ ./configure
$ make
# make install


socket.ioをインストールします。
$ curl http://npmjs.org/install.sh | sh
$ npm install -g socket.io

※エラーになる場合は curl -L http://npmjs.org/install.sh | sh とやると良いようです。

これで実装の準備が整いました。
それでは実装を始めます。
今回は、メッセージを送信したら見ている全員にメッセージを表示させる、簡単なチャットの仕組みを作ってみます。

サーバー側

/opt/memorycraft/node/socketsample.js
require.paths.push('/usr/local/lib/node_modules');

var server = require('http').createServer(function(req, res){
  res.writeHead(200, {'Content-Type': 'text/html'});
  res.end('server connected');
});
server.listen(1337);

var io = require('socket.io').listen(server);
io.sockets.on('connection', function (socket) {
  socket.emit('info', { msg: 'welcome' });
  socket.on('msg', function (msg) {
    io.sockets.emit('msg', {msg: msg});
  });
  socket.on('disconnect', function(){
    socket.emit('info', {msg: 'bye'});
  });
});


サーバー側では任意のポート(ここでは1337)を開放したhttpサーバーにsocket.ioをつなげてリスン状態にします。
通信の確立(connection)や切断(disconnect)などのビルトインイベントや、自分のアプリケーションで使用したいオリジナルのイベント(ここではmsg)のイベントハンドラを定義します。
このコードでは接続したときに"welcome"、切断したときに"bye"をそのユーザーに、あるユーザーから"msg"というイベントが送られたら全員に"msg"というイベントを、それぞれプッシュ送信しています。

クライアント側

/var/html/www/test/node/index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js" type="text/javascript" charset="utf-8"></script>
  <script src="http://xxx.xxx.xxx.xxx:1337/socket.io/socket.io.js"></script>
  <script type="text/javascript">
  $(function(){
    var socket = io.connect('http://xxx.xxx.xxx.xxx:1337/');
    socket.on('connect', function() {
      $("#log").html($("#log").html() + "<br />" + 'connected');
      socket.on('info', function (data) {
        $("#log").html($("#log").html() + "<br />" + data.msg);
      });
      socket.on('msg', function(data){
        $("#log").html($("#log").html() + "<br />" + "<b>" + data.msg + "</b>");
      });
      $("#send").click(function(){
        var msg = $("#msg").val();
        if(!msg){
          alert("input your message");
          return;
        }
        socket.emit('msg', msg);
      });
    });
  });
  </script>
  <title>nodetest</title>
</head>
<body>
  <input id="msg" type="text" style="width:400px;"></input>
  <input id="send" type="button" value="send" /></br >
  <div id="log" style="width:400px;height:400px;overflow:auto;border:1px solid #000000;"></div>
</html>

クライアント側からはサーバー側の1337ポートに接続しますが、サーバー側は実際にファイルを自分で配置しなくてもSocket.IOが勝手にクライアント側のライブラリを/socket.io/socket.io.jsとして配信してくれます。
そのファイルをscriptファイルでロードするようにし、あとは接続部分とサーバーからのイベントのハンドラを定義します。また、ボタンを押したらテキストをサーバー側に送信するように記述します。


それでは、サーバー側のコードを実行してみます。
$ node /opt/memorycraft/node/socketsample.js
   info  - socket.io started

ブラウザでhtmlを表示します。

サーバー側からの"welcome"メッセージが表示されていることがわかります。
また、メッセージを入力して送信すると、

メッセージ欄にサーバーから送られてきているのがわかります。

別のブラウザでも開いておき、そちらからメッセージを送信するともう一方のブラウザにメッセージが表示されることが分かります。


このように、socket.ioを利用することで多くのブラウザで簡単にWebSocketを実現することができます。
WebSocketは通信内容が少なく、コネクションやリソースの消費が少ないので、ゲームやソーシャルサイトなどのマルチユーザーアプリや、クライアントのログ集計など、様々な分野での利用が期待できます。
今日はここまで。

2011年9月28日水曜日

WebSocketってなんじゃ?(Node編1 node.jsのインストール)

かなりいまさらですが、nodeについて。
Node.jsは、非同期イベント駆動モデルのサーバーサイドJavaScript環境で、非ブロッキングI/O(入出力が完了しないうちに他の処理を開始できる)という方式でクライアントからの多数のリクエストを短い時間で処理することができます。

Node.jsではWebSocketを利用することで双方向のメッセージ送信を可能にします。
サーバーからクライアントに情報を渡す仕組みとしては、Ajaxがポーリングにより毎回コネクションを生成したり、Cometのロングポーリングによる擬似プッシュを実現しているのに対して、WebSocketは一度コネクションが生成されたらそのコネクション上で何度でも双方向にメッセージの送受信を行えるため、サーバーのリソースもクライアントの待機時間も節約できます。


それでは、さっそくnodeをインストールしてみます。
$ mkdir -p ~/src/git
$ cd ~/src/git/
$ git clone --depth 1 git://github.com/joyent/node.git
$ cd node
$ git checkout v0.4.11
$ ./configure
$ make
# make install


インストールができたら、以下のようなサンプルを作ってみます。
$ vi hello.js
~
var http = require('http');
http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Hello World\n');
}).listen(1337);
console.log('Server is running');
~


実行します。
$ node hello.js
Server is running
※AWS上で構築している場合はSecurityGroupでTCPの1337を開放する必要があります。


ここで、クライアント(Mac)のコンソールで以下のようにアクセスしてみます。
$ curl http://xx.xx.xx.xx:1337/
Hello World


次に、さきほどのhello.jsを以下のように書き換えて実行してみます。
var http = require('http');
http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.write("Hello\n");
  setTimeout(function(){
    res.end('This is the Node World !!!!\n');
  }, 10000);
}).listen(1337);
console.log('Server running');

同じようにアクセスしてみます。
$ curl http://xx.xx.xx.xx:1337/
Hello

すると、一旦ここまでレスポンスが表示され、
10秒後に
$ curl http://xx.xx.xx.xx:1337/
Hello
This is the Node World !!!!
と "This is the node World !!!!" が追加で表示されます。

この時サーバークライアント間では、2つめのワードが返る間もコネクションが維持され、しかもサーバーはその間スループやロックやビジーになることなく他のアクセスの処理などを行うことができます。

見た目にはたったこれだけの違いなのですが、これによりとても多くの可能性をもつことになり様々な応用が期待できます。
今日はここまでです。