Cometでブラウザをgkbrさせてみる!

最近久しぶりにCometとかFlashXML-Socketとかを調べています。

Cometといえばチャットに代表されるように「テキストや画像の配信」が頭に思い浮かびますが、
今回はjsonpで任意のイベントを送り込む例として「サーバにアクセスしている人のブラウザを好き勝手に動かしてみる」という、やや怪しげなサンプルを書いてみました。


仕組みはこんな感じ。

  • サーバはPOEで動作します。HTTPの受け口とコマンドラインを受け付ける口を持っています。
  • クライアント側からはjqueryを使ってXHRなlong_pollセッションを張っておきます。
  • サーバからは任意のタイミング(コマンドラインを受け付けたタイミング)でjsonpを送ってやります。
  • クライアントは受け取ったjsonpによってあらかじめロードしておいたいくつかの関数のうち、どれかがキックされる

というような構成です。


実際にどうなるかというと、ユーザにブラウザでアクセスさせてから、こっそりとそのサーバのコンソールで「right, left, up, down」とかコマンドを打つと、ユーザのブラウザが右に左に、上に下にと移動します。

ユーザ「なんじゃこりゃー」と驚いているあいだに、さらにコンソールから「shake」と叩くと、ブラウザがガクガクブルブル震えます。そう、まさに名実ともにgkbr!!

まったく無意味なくだらないイタズラcometですが、実際にやってみると結構面白いです。
perl界隈の玄人さんたちはあまりビビラないかもしれないけど、民間人はイチコロです。せいぜいビックリさせてあげましょう!

以下、おソースでがんす。

use strict;
use POE;
use POE::Component::Server::HTTP;
use POE::Wheel::ReadLine;
use YAML;
use Template;
use HTTP::Status;
use Scalar::Util qw(refaddr);

my %receiver;
my $yaml = YAML::Load( join '', <DATA> );

my $server = POE::Component::Server::HTTP->new(
    Port           => 80,
    ContentHandler => {
        "/"          => \&index,
        "/jsonp.js"  => \&jsonp,
        "/long_poll" => \&long_poll,
    }
);

POE::Session->create(
    inline_states => {
        _start    => \&make_wheel,
        got_input => \&got_input_handler,
    }
);

POE::Kernel->run;

sub make_wheel {
    my ( $kernel, $session, $heap ) = @_[ KERNEL, SESSION, HEAP ];
    $heap->{wheel} = POE::Wheel::ReadLine->new(
        InputEvent => "got_input",
        appname    => 'mycli'
    );
    $heap->{wheel}->get("Prompt: ");
}

sub got_input_handler {
    my ( $heap, $input, $arg ) = @_[ HEAP, ARG0, ARG1 ];
    if ( defined $input ) {
        $heap->{wheel}->addhistory($input);
        if ( $input =~ /^shake|left|right|up|down$/ ) { &release($input); }
        $heap->{wheel}->get("Prompt: ");
    }
    else {
        $heap->{wheel}->put("Exception: $arg");
        if ( $arg eq 'interrupt' ) {
            POE::Kernel->stop;
        }
    }
}

sub index {
    my ( $req, $res ) = @_;

    my $tpl = $yaml->{index};
    my $tt  = Template->new;
    $tt->process( \$tpl, "", \my $content );
    $req->headers->header( Connection => 'close' );
    $res->code(RC_OK);
    &standard_response( $res, "text/html" );
    $res->content($content);
    return RC_OK;
}

sub jsonp {
    my ( $req, $res ) = @_;

    my $tpl = $yaml->{js};
    my $tt  = Template->new;
    $tt->process( \$tpl, "", \my $content );
    $req->headers->header( Connection => 'close' );
    $res->code(RC_OK);
    &standard_response( $res, "text/javascript" );
    $res->content($content);
    return RC_OK;

}

sub long_poll {
    my ( $req, $res ) = @_;

    $receiver{ refaddr $res} = $res;
    $req->headers->header( Connectcion => 'close' );
    &standard_response( $res, "application/json" );

    return RC_WAIT;
}

sub release {
    my $str = shift;

    my $jsonp = 'do_jsonp({data : { name : "' . $str . '"} })';
    foreach my $res ( values %receiver ) {
        $res->code(RC_OK);
        $res->headers->header( "Connection" => 'Keep-Alive' )
          ;    # IE6のバグのため必要
        $res->continue();
        $res->content($jsonp);
        delete $receiver{ refaddr $res};
    }
    return RC_OK;
}

sub standard_response {
    my ( $res, $type ) = @_;

    $type ||= "text/html";
    $res->headers->header( "Cache-Control" => 'no-cache' );
    $res->headers->header( Expires         => '-1' );
    $res->headers->header( Pragma          => 'no-cache' );
    $res->content_type( $type . "; charset=UTF-8" );
}

__DATA__
---
index: |
  <html>
  <head>
  <script type="text/javascript" src="http://cachefile.net/scripts/jquery/1.2.6/jquery-1.2.6.js"></script>
  <script type="text/javascript" src="jsonp.js"></script>
  </head>
  <body>
  あぶない実験
  </body>
  </html>


js: |
  function shake(num) {
      if(num == -1){
          itv = 50;
          cnt = 0;
          x = new Array( 12,-20,  8,-16, 20, -4, 16, -8,  4,-12,0);
          y = new Array(-20,  8,-16, 12,-12, 16, -4, 20, -8,  4,0);
          shake(cnt);
      }
      else if(num == 11){
          cnt = 0;
      }
      else{
          if(x[cnt] != 0) moveBy(x[cnt],y[cnt]);
          cnt++;
          if(cnt < x.length) setTimeout("shake(cnt)",itv);
          else cnt = 0;
      }
  }

  function window_move(direction,num){
        switch (direction){
            case "left" : x=-1; y= 0; break;
            case "right": x= 1; y= 0; break;
            case "up"   : x= 0; y=-1; break;
            case "down" : x= 0; y= 1; break;
        }
        for(cnt=0;cnt<num;cnt++){
            moveBy(x,y);
        }
  }

  function do_jsonp(data){
    switch (data.data.name){
        case "left"  : window_move("left", 100); break;
        case "right" : window_move("right",100); break;
        case "up"    : window_move("up",   100); break;
        case "down"  : window_move("down", 100); break;
        case "shake" : shake(-1); break;
    }
    long_poll();
  }

  function long_poll(){
      $.ajax({
          dataType: "jsonp",
          //data: {},
          url: "/long_poll",
          success: "do_jsonp"
      });
  }

  $(function(){setTimeout(long_poll,"1000");});


1点だけ、がんばったところを忘れないようにメモ。

sub releaseのなかで$res->headers->header( "Connection" => 'Keep-Alive' )としていますが、これはIE6のKeepAliveの仕様があまりにも個性的すぎるため、こんな風になっています。
参考)id:malaさんのブログのコメント欄に答えがあった! → http://la.ma.la/blog/diary_200702101610.htm


余談ですが、ちょっと前にcybozu labsにお邪魔してid:kazuhookuさんとid:ZIGOROuさんとお話してきたんですが、「CometよりもFlashXML-Socket使ったほうがクロスドメイン問題とかクリアするの楽ですよね」みたいな話になりました。実際にCometが商用サービスで使われてるのLingr以外にあまり知らない。。

2年前くらいのブームが落ち着いてきて、cometって技術は実際にどれくらい使われてるんだろうか?
ちょっと気になる今日この頃。