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月11日日曜日

redisってなんじゃ?(pub/sub編)


redisにはpub/subという機能があります。

これはpublish/subscribeパターンという仕組みの実装で、発行者と購読者という役割でメッセージの配信を行うものです。

では、redisのコンソールを使って試してみたいと思います。

redis1とredis2という2つのホストを使用します。
redis1ではローカルホストでredis-cliに接続します。
redis2ではredis1のredis-clに接続します。

redis1
# redis-cliredis 127.0.0.1:6379>

redis2
# redis-cli -h 10.0.0.200redis 10.0.0.200:6379>

メッセージの購読はチャンネル名を指定します。
redis2でgreetingというチャンネル名でsubscribeコマンドを実行します。

redis2
redis 10.0.0.200:6379> subscribe greeting
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "greeting"
3) (integer) 1

こうすることによって、redis2はredis1のgreetingというチャンネルのメッセージの待ち受けが始まります。

続いて、redis1でメッセージを発行します。
greetingというチャンネルで「Hello World!」という内容のメッセージを発行します。

redis1
redis 127.0.0.1:6379> publish greeting "Hello World!"
(integer) 1
redis 127.0.0.1:6379>

すると即座にredis2のクライアントにHello Worldというメッセージが受信されたことがわかります。

redis2
redis 10.0.0.200:6379> subscribe greeting
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "greeting"
3) (integer) 1
1) "message"
2) "greeting"
3) "Hello World!"

このように、redisでは他のサーバーのredisクライアントとメッセージのやりとりが可能です。
クライアントコンソールでは効能がわかりにくいので、次回はアプリケーションレベルで試してみたいと思います。

以上です。

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」が表示されました!
これで台数が増えてもすべてのユーザーが同じ空間でコミュニケーションすることができます。

以上です。