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ボタンを押してみます。



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


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

以上です。