IM::EngineとStardustで3分クッキング

YAPC::Asia 2009で(少なくとも個人的に)話題だったセッションから、StardustとIM::Engineを使って3分クッキングしてみる。(実際どのくらいかかるかはよくわからん。)

概要

Stardust(CPAN, github)は簡単に使えるCOMETサーバ。COMETでStardust…ああ、ネーミングセンスが良すぎる…。てのはともかく、COMETのデモはやっぱりインパクトがあって楽しい。起動すると/channel/の下にAPIが出来て、外からそいつに適当にアクセスすれば話は済むようになっている。APIにPOSTしてあげればデータが登録されて、APIからGETすることで引き出せるってわけだ。

IM::Engine(CPAN, github)はAPI Designのセッションで登場するのだけれど、HTTP::Engineのように使える(らしい)Instant Messaging処理エンジン。なんせインターフェイスがさっぱりしていて使いやすそうだ。

となると、当然やるのはIRCのデータをIM::Engineで引っ張って、Stardustでリアルタイム出力するやつ。と思ってやってみたのだけれど、オリジナルなコードはほとんどなしに書けてしまう。なんてラクチンな世の中。

方針は、IM::Engineを仕込んだスクリプトを使って、発言の度に別で立てたStardustにPOSTする。StardustはAPIとして使って、フロントエンドインターフェイスはペラのHTMLを用意する。HTMLはStardustのDEMOでついてくるcurl_commandsの出力を真似ることにしよう。

Stardustを立ち上げる

ではまず、Stardustから行こう。

Stardustをインストールしたら、起動する。

$ stardust.pl
Please contact me at: http://localhost:5742/

終わり。

StardustはSquatting(CPAN, github)というフレームワークをベースに作られている。(どちらも同じ作者。)小さなフレームワークだが、個人的にはこれまであまり見かけない書き方だったので、ちょっとドキッとした。Squatting::Cookbookがあるので、これを見るとざっくり掴めるかもしれない。3分クッキングではそれで良しとしよう。(本体もさほど大きくないので、ざっと読んでもそれほど苦労は無いが、後述するがちょっと特殊な感じがする。)

StardustのAPI

StardustのAPIは、以下のようになっている:

  • GET /channelでチャンネル一覧取得
  • GET /channel/$fooで任意のチャンネルの情報を取得
  • POST /channel/$fooで任意のチャンネルにメッセージを送信
  • GET /channel/$foo/stream/$connection_idがlong-poll用のAPI

つまるところ、Stardustは基本的に、COMETで必要なサーバ側APIをざっくり提供してくれるわけだ。

ついでなので、ここでデモページを見ておこう。デモページは起動時に--demoオプションを指定しておくと見れるようになる。

% stardust.pl --demo
      The demo is at: http://localhost:5742/demo/
Please contact me at: http://localhost:5742/

で、http://localhost:5742/demo/を見るとcurl_commandsというリンクあるので、そこを辿る。ターミナルから指定されているcurlコマンドを投げれば、ページ内に表示されることが分かるだろう。

インターフェイスになるHTMLを作る

今回作るフロントエンドのインターフェイスは、このcurl_commandsページを参考にする(というかコピーする)。以下のようなものにしてみた。ちょっと長いが。

<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
    <meta http-equiv="Content-Script-Type" content="text/javascript" />
    <meta http-equiv="Content-Style-Type" content="text/css" />
    <title>ShootingStar</title>

    <style type="text/css">
div.message {
    border-width: 0px 0px 1px 0px;
    border-style: solid;
    border-color: #999999;
}
span.time {
    font-size:small;
    color: #999999;
}
span.name {
    color: #666699;
    padding: 0em 0.5em;
}
span.message {
    color: #000000;
}
    </style>

    <script type="text/javascript" src="/js/fx.js"></script>
    <script type="text/javascript" src="/js/jquery-1.3.2.js"></script>
    <script type="text/javascript" src="/js/jquery.color.js"></script>
    <script type="text/javascript" src="/js/jquery.ev.js"></script>

    <script type="text/javascript">
var clientId = Math.random().toString().replace(/\./, '');
jQuery.ev.loop(
    '/stardust/channel/shooting_star/stream/'+clientId,
    {
        incoming_callback: function(ev){
            var dt = new Date();
            jQuery('#messages').prepend(
                '<div class="message">'
                    + '<span class="time">' + dt.getHours() + ':' + dt.getMinutes() + '<' + '/span>'
                    + '<span class="name">(' + ev.incomming.sender.name + ')<' + '/span>'
                    + '<span class="message">' + ev.incomming.message + '<' + '/span>'
                    + '<' + '/div>'
            );
        }
    }
);
</script>
  </head>
  <body>
    <h1>ShootingStar</h1>
    <p>#perl-casual@chat.freenode.net</p>
    <div id="messages">
    </div>
  </body>
</html>

肝になるjQueryの部分は短い。COMET接続部分をjQuery.ev(github)がやってくれるからだ(Stardustと作者は同じ)。あとはちょっと体裁を整えたりとかそういうことだ。$app_root/templates/index.htmlとして保存した。

Stardustのchannel名はshooting_starにした。もちろんこれは任意だが、COMETでStardustときたらShooting Starにせざるを得ない。

アプリケーションのパス構成を変える

で、ここで気づいたのだけれど、このHTMLを表示するにはまた別にhttpdを立てる必要がある(XHRがクロスドメインを越えられないので、同一ドメイン上の必要がある)。まぁ、別に良いんだけど、ちょっと面倒だなぁ…ってことでちょっと脱線して、StardustがベースにしているSquattingを使い、以下のようなパス構成に変更してみよう。

  • / : インターフェイス。上記のHTMLを表示する
  • /js/* : jQueryJavaScript各種が入るパス
  • /stardust/* : Stardustのサーバを立てるパス

なんでこんなことを言い始めたかというと、ちょっと調べた限りだと、Squattingだと特定のパスに別にアプリケーションを走らせるのが簡単そうだったから。まぁ要するに試してみたいってだけなんだけど。

アプリケーションの名前は(もちろん)ShootingStarとして、次のようなアプリケーションを書いた。Stardustをコピーして書き換えただけだ。(主にいらないところを消す作業になる…。)

package ShootingStar;
use 5.008;
use strict;
use warnings;
use base 'Squatting';

package ShootingStar::Controllers;
use strict;
use warnings;
use Squatting ':controllers';
use FindBin;
use Path::Class qw( dir );

our @C = (
    C(
        Home => [ '/' ],
        get => sub {
            my ($self) = @_;
            $self->headers->{'Content-Type'} = 'text/html';
            return dir($FindBin::Bin)->file('../templates/index.html')->slurp;
        },
    ),
);

1;

まぁ、静的なHTMLなので、今回はViewは省略。てかコントローラもいらないんじゃないかって感じだが、気分を出すためにもコントローラは書いてみた。もちろんSquattingはMVCフレームワークなので、Viewに渡したり出来ます。

起動用のスクリプトは、stardust.plを参考に書く。以下のような感じになった。

#!/usr/bin/perl
use strict;
use warnings;
use ShootingStar 'On::Continuity';
use Stardust;
use Getopt::Long;
use FindBin;
use Path::Class qw( dir );

my $config = { port => 5742 };

GetOptions(
    $config,
    "port|p=n",
);

ShootingStar->mount('Stardust' => '/stardust');
ShootingStar->init();
ShootingStar->continue(
    port    => $config->{port},
    docroot => dir($FindBin::Bin)->subdir('../static/'),
);

/stardustにStardustをマウントしている。あとcontinueメソッドにdocrootで渡しているのは、/js/*などの静的ファイル置き場。continueメソッドは、Squatting自体ではなくてSquatting::On::Continuityに書いてあり、これが簡単なサーバを実現してくれる。

なお、$app_root/staticの中にはstardust/shareの中に入っているjsディレクトリをそのままコピーしてきた。

ちなみに、Squattingフレームワークには、アプリケーション起動用のコマンドsquatting(まんま)がついてくる。libパスを外から指定する方法はなさそうだが、スクリプトを読むとuse lib 'lib'としているので、少なくともlibの上のパスからであれば起動可能だ。

$ cd shooting-star
$ squatting -p 5742 ShootingStar
Please contact me at: http://localhost:5742/

今回こうしなかったのは、/stardustにStardustアプリケーションを立てるなど、ちょっとしたカスタマイズのためである。

あとは指定されたURIをブラウザで叩く。きちんと見えていればOK。

IM::Engineを使って発言をStardustにPOSTする

最後に、発言の度にStardustにPOSTするスクリプトを書こう。といっても、IM::EngineのPODに書いてあるサンプルコードをそのままコピペして、ちょっとだけ書き換えれば良い。

#!/usr/bin/perl
use strict;
use warnings;
use IM::Engine;
use AnyEvent::HTTP;
use JSON::Syck;

IM::Engine->new(
    interface => {
        protocol => 'IRC',
        credentials => {
            server   => "chat.freenode.net", # 任意
            port     => 6667,
            channels => ["#perl-casual"],    # 任意
            nick     => "shooting-star",
        },
        incoming_callback => sub {
            my $incoming = shift;
            return unless $incoming->sender->name;
            my $message = sprintf( 'm=%s', JSON::Syck::Dump({
                type      => 'incoming_callback',
                incomming => $incoming,
            }));
            http_post(
                'http://localhost:5742/stardust/channel/shooting_star',
                $message,
                sub { printf "%s\n", $message; }
            );
            return 1;
        },
    },
)->run;

これでIM::Engineも終わり。チャンネルのところは自分で好きなのに変えておいてください。起動しよう。

$ script/im_engine.pl

完成!

さて、何かしら発言があるのを待とう。あるいは自分で発言したりしても良い。問題なくブラウザ側でも見れたら完成。

ちなみに所要時間は、素晴らしくいろいろ脱線してコードを読んでたので、まったく3分どころじゃなかった。

今後の展望としては、まぁログを取っておいて最初の接続時にちょっと流すとかか。頑張ってください。

ここから先はおまけ

以下はおまけ。さくっと見てみた感想。

Squatting

割とコンパクトなので、Catalystのように読むのに苦労することもないと思われる(相対的な話だけど)。あまり見かけない(気がする)書き方なのだけれど、YAPCの懇親会でご本人はプロトタイプベースのオブジェクト指向が好きだと言っていて、そう思って読み返すと多少すっきりするかも。

面白いのは、特定のパスに別のSquattingアプリケーションを割り当てたり出来る点。そういうことをmod_rewriteなどに頼らず出来るのは、ライトユースには便利かもしれない。

例えば、お遊びでCGI書くこととかあるけれど(大きなフレームワーク使うほどじゃないからCGIでいいや、みたいな時だが)、CGIの代わりにSquattingを動かしておいて、その下の特定のパスには必要に応じて別にアプリケーションを差し込むとかが出来そう。

と思って、上記ではSquattingを使ってトップページを作り、適当なパスにStardustを割り当ててみた。もともとStardust::Demoは、--demoのオプション指定時に動的に/demoに差し込まれるようになっている。

Tenjin

「とても速い」テンプレートエンジンらしい。StardustやSquattingが各ページの描画に使っている。

ベンチマークを信じるならTTの5から10倍か。そしてとても対応言語が多い。テンプレート部分の書き方は割合オーソドックスなので、さほど構えずに使えそう。(しかしTTに慣れきった自分には…。まぁ、使い分けなんだろうなぁ。)

とか言いながら今回は省略した。

[perl] TTより5倍速い?テンプレートエンジン"Tenjin"を試す - ありんく tech-logなんていう記事があった。

ちなみにフレームワークとしては、Cookbookにも書いてあるけれど別にテンプレートエンジンに縛りがある訳ではない。TTでももちろんOK。

COMET -> Stardust -> Shooting Star

何の話かってことだけれど、一応。毎年定期的に発生する流星群は、「母彗星」と呼ばれる彗星が吹き出した「ダスト」によって生み出される。なので、「彗星」→「ダスト」となったら次は「流星」なんです。