Paradigm Shift Design

ISHITOYA Kentaro's blog.

MojoliciousでPocketIOを使いつつセッションを共有する

さて念願の81忘年会、2次会まではよかったのですが、3次会は飲み過ぎてダウンしてました。 日曜日は久々に二日酔い。グロッキーで記事を書くつもりが…orz

というわけで、Goomerの技術的な話を、備忘録的にまとめて書いておきたいと思います。元記事はイベント内Likeツール、Goomer作りましたです。

Goomerは、HTML5WebSocketを使ったリアルタイムWebアプリケーションです。 WAFはMojoliciousを使っています。Mojoliciousには標準でWebSocket実装が乗っているのですが、今回は、socket.ioperl実装であるPocketIOを使っています。

あまりよく理解していなかったのですが、実際に実装をしてみるまで、WebSocket = socket.ioだと思っている僕がいました。 なので最初はMojoliciousに載っていたWebSocketを使って色々やってました。ただ、socket.ioはWebSocketが利用できないクライアントのときのフォールバック実装が豊富にあり、IE5.5からサポートしているという変態さを発揮しています。ヤバ過ぎる。Mojoliciousの方はそんなのなくて、WebSocket実装なくんばブラウザにあらず的な高慢さです。

いやまぁ、正直IE5.5とか…

とにかく、まぁ、そういうわけでMojoliciousのWebSocket実装から、PocketIOへと移行しました。そして、Mojolicious側とPocketIO側で同じセッション情報を利用したかったので、色々工夫したわけですよ。

yusukebeさんの「PocketIOのイカ娘語echoサンプル」がMojolicious + PocketIOなので参考になりそうです。が、正直perlはじめて1ヶ月経たない僕にはいろいろと理解ができず…。

特に、MojoliciousとPocketIOでどうやったらセッションを共有できるのか、また、よく色々なところに「Mojoliciousのセッション機能は貧弱だからPlackのSession使いましょう」とか書いてあるんだけど、そのPlackのセッションはどうやって使うのかと!

諦めムードですよ。まじで。

そんなわけで、だらだら前置き長いですが、 「MojoliciousとPocketIOを同じアプリケーションで作成しつつ、MojoX::Sessionを使ってセッションを共有する」 サンプルをgithubのリポジトリにおきました。

perl始めて2ヶ月だし、ほぼやっつけなのでソースコードの汚さはどうか!と先に言っておきます。 以下解説です。

Mojoliciousアプリを作る

まずは、普通に

mojo generate app [app_name]

としてアプリを生成します。次に自動生成のscript/app_nameとは別に、PSGIファイルを作ります。

PSGIファイルの書き方

mojolicio.usでPocketIOを使うためにはPSGIファイルを書き換えないといけないのですが、やりかたが分からず…。
CookBookよんでも、正直分からない…
こういうのみなさんどうやって身に付けてるんだろう。

とりあえず、色々なサイトぐぐって片っ端から色々やるに、script/[app_name].psgiファイルは次のようになりました。

script/mojolicious_pocket_io.psgi

MojoX::Sessionを使う

use utf8;
use warnings;
use strict;

use Mojo::Server::PSGI;
use Plack::Builder;
use Plack::App::File;
use PocketIO;

use FindBin;
use lib "$FindBin::Bin/../lib";

use MojoliciousPocketIO;
use MojoliciousPocketIO::WebSocket;

#MojoliciousをPSGIで
my $psgi = Mojo::Server::PSGI->new( app => MojoliciousPocketIO->new );
my $app = sub { $psgi->run(@_) };

#socket.ioのスクリプトとファイルのルートディレクトリ
my $siroot = "$FindBin::Bin/../public/js/";

builder {
  #socket.ioのスクリプトとファイルをmountする
  mount '/socket.io/socket.io.js' =>
    Plack::App::File->new(file => "$siroot/socket.io.js");
  mount '/socket.io/static/flashsocket/WebSocketMain.swf' =>
    Plack::App::File->new(file => "$siroot/WebSocketMain.swf");
  mount '/socket.io/static/flashsocket/WebSocketMainInsecure.swf' =>
    Plack::App::File->new(file => "$siroot/WebSocketMainInsecure.swf");

  #PocketIOをマウント
  mount '/socket.io' =>
    PocketIO->new( class => 'MojoliciousPocketIO::WebSocket', method => 'run' )\
;
  #Mojoliciousをマウント
  mount '/' => $app;
};

のような感じになりました。
知っている人にはなんでもないんでしょうが、mountっていうのでアプリケーションをパスを振り分けることができるんですね。
あと、PocketIOのサイトをいくら探してもsocket.io.jsとswfファイルを見つけることができなかったのですが、githubのsocket.io-clientにあるんですね。ものすごい探した…orz。

まぁ、これでMojolicio.usを使った普通のアプリケーションと、PocketIOを使ったWebSocketアプリケーションが両立できます。

で、問題はここから。
MojoliciousのセッションとPocketIOのセッションを透過的に扱いたい!
というわけで、MojoX::Sessionを使いました。Plack::MiddleWare::Sessionを使ってみたかったのですが、なんかよくわからなかったのでMojoX::Sessionです。一応Mojoliciousのプラグインもあるようなので。

Mojolicious側のセッションは次のように初期化しています。lib/MojoliciousPocketIO.pm

sub setup_app{
  my $self = shift;
  my $config =
    $self->plugin('Config',
                  {file => $self->app->home->rel_file('conf/app.conf')});
  $self->secret($config->{secret});
  $self->controller_class('MojoliciousPocketIO::Controller');
  
  my $handler = DBIx::Handler->new(
    $config->{db_dsn},
    $config->{db_username},
    $config->{db_password},
    +{
      mysql_auto_reconnect => 1,
      mysql_enable_utf8 => 1,
      RaiseError => 1,
      PrintError => 0,
      AutoCommit => 1,
      on_connect_do => [
        "SET NAMES 'utf8'",
        "SET CHARACTER SET 'utf8'",
      ],
    },
  );

  $self->plugin(
    session => {
      stash_key => 'mojox-session',
      store     => [dbi => {dbh => $handler->dbh}],
      transport => 'cookie',
      expires_delta => 1209600, #2 weeks.
      init      => sub{
        my ($self, $session) = @_;
        $session->load;
        if(!$session->sid){
          $session->create;
        }
      },
    }
  );
}

みたくなってます。Handlerの初期化とかは、なんかもっといい方法あると思います。で、WebSocket側は、

WebSocketController.pmソースコードがあります。Mojolicious側と大体同じ方法でMojoX::Sessionを初期化しています。
その際、session_idが必要なのですが、

WebSocket.pmでPocketIOのENVからHTTP_COOKIEを取り出し、SIDを取得しています。

sub session_id{
  my $self = shift;
  my $socket = shift;
  my $cookie = $socket->{conn}->{on_connect_args}[0]->{HTTP_COOKIE};
  if($cookie =~ /sid=([0-9a-f]+)/){
    return $1;
  }
  return undef;
}

SIDさえ取得できれば、

$session->load($self->session_id);

として、Mojolicious側のsessionを取得することができます。
いやしかし、$socket->{conn}->{on_connect_args}[0]->{HTTP_COOKIE};というのを発見するのに1日かかった感じです…orz

ここまでくれば、Mojolicious側のMain.pm

sub set_name{
  my $self = shift;
  my $name = $self->param('name');
  $self->session(name => $name);
  $self->render(json => $name);
}

みたいな感じで、sessionにセットしたnameを、

sub onEcho{
  my ($self, $socket, $message) = @_;
  my $room = $self->session('room_id');
  my $name = $self->session('name');
  $socket->sockets->in($room)->emit('echo', $name . ' says ' . $message);
}

のようにして、使うことができます。

分かっちゃえば楽だし、もう考えなくてすむのでいいんですがね…長かった。

サンプルを動かす

サンプルは

git clone https://github.com/kent013/MojoliciousPoketIO.git
cd MojoliciousPocketIO

リポジトリを取り出して、

cpanm --installdeps .

依存ライブラリをインストールし

twiggy script/mojolicious_pocket_io.psgi -l :3000

として、サーバを立ち上げた後、

http://localhost:3000

にアクセスすれば動作確認ができます。

しょかん

ショージキ、辛かった。
っていうか、この方法があっているのかも、分かりません!
もっとスマートな方法があったら教えてください!