はちゅにっき

こっちのブログはまったり更新

WebSocket を使って Amon2 でチャットプログラムを書いてみる

ずいぶん前ですが Amon2 が WebSocket に対応したということで、今更感のあるチャットプログラムを書いてみることに。
今更とは言いつつも社内では IRC を使うことができないので、ちょっとしたメッセージがやりとりできるものを作ってみようかなと思い、そのベースとなるようなイメージで作ってみました。
オマケで以前から使ってみたいなぁと思っていた Redis や Proclet なんかも (無駄に) 使ってみました。
適当な Heartbeat で WebSocket を維持していたり、エラー処理が適当すぎて Member の一覧が嘘つきだったり *1 と、まだまだイケてない部分ばかりですが、最近時間がなかなか取れないこともあり、とりあえず動くし晒してみようかと思います。
Twincle の名前は昔テストで Twitter Client を作ったときに押さえた ID をそのまま踏襲しただけで、特に意味はないです。
README すらないご覧のザマだよ。

hatyuki / twincle
https://github.com/hatyuki/twincle
デモ
http://twincle.magicalhat.jp/

セッションで Socket を管理しているため、同一ブラウザで2つウィンドウ立ち上げてもうまく動きません。この辺は Hash::MultiValue をうまく使えばいいのかなー。などと検討中。

なんとなく工夫した気がするところ

WebSocket ではバイナリデータを送受信することができるので、今回は Perl (Amon2) ←→ JavaScript 間を MessagePack でバイナリ化してやりとりをしてみました。
Amon2 では下位のレイヤーでは Protocol::WebSocket を利用して WebSocket のやりとりをしていますが、その部分は見事に隠蔽されており、プログラムするときは Protocol::WebSocket の存在を意識せずに、文字列を単に渡すだけで WebSocket を扱うことができます。

  • MyApp::Web::Dispatcher
get '/socket' => sub {
    my $c = shift;

    $c->websocket( sub {
        my $ws = shift;
        $ws->send_message("文字列を渡すだけで OK");
        # ...
    } );
};

が、バイナリデータを送受信するためには Protocol::WebSocket::Frame のコンストラクタで送信したいフレームがバイナリデータであることを指定する必要があるため、ちょっと都合が悪いこととなります。
そこで、Amon2::Plugin::Web::WebSocket をちょっとだけ編集して、文字列だけではなく Protocol::WebSocket::Frame そのものも渡せるようにしました。

  • MyApp::Plugin::Web::WebSocket
# ... (中略)
$ws->{send_message} = sub {
    my $message = shift;

    # Protocol::WebSocket::Frame オブジェクトを受け付けるように
    unless (eval { $message->isa('Protocol::WebSocket::Frame') }) {
        $message = Protocol::WebSocket::Frame->new($message);
    }

    $h->push_write($message->to_bytes);
};
# ... (中略)

これにより以下のように、バイナリフレームを送信することができるようになります。

  • MyApp::Web::Dispatcher
get '/socket' => sub {
    my $c = shift;

    $c->websocket( sub {
        my $ws = shift;

        # バイナリフレームを作成して
        my $frame = Protocol::WebSocket::Frame->new(
            type => 'binary',
            buffer => $binary_strings,
        );

        # 送信
        $ws->send_message($frame);
        # ...
    } );
};

無駄にハマって解決できていないところ

手元の Mac では再現しませんが、Debian 上で実行するとなぜか LWP::UserAgent がすごい時間がかかる。
具体的には不自然に必ず30秒以上かかる。なんかタイムアウトを待っているみたいな。。。
そのせいで Net::Twitter::Lite を使った Twitter の認証に必要以上の時間がかかってしまい、困ったことになりました。
とりあえずの対処として、Net::Twitter::Lite が LWP::UserAgent ではなく Furl を使うように無理矢理パッチをあてました。

package Twincle::Auth::Site::Twitter;
use strict;
use warnings;
use Furl;
use Net::Twitter::Lite;

sub import
{
    no warnings 'redefine';

    # "default_header" メソッドがないとエラーになるので申し訳程度に追加
    *Furl::default_header = sub { };

    my $orig = *Net::Twitter::Lite::new{CODE};
    *Net::Twitter::Lite::new = sub {
        my ($class, %args) = @_;

        # UserAgent を LWP::UserAgent から Furl に差し替え
        $args{ua} = Furl->new;
        $args{legacy_lists_api} = 0;

        $orig->($class, %args);
    };
}

1;

LWP::UserAgent が遅い原因については据え置き中。
時間があるときに調べてみよっと。

まとめ

Amon2 の WebSocket サポートを使うと、すごく簡単に WebSocket を使うアプリケーションが作成できました。
バイナリデータを送受信するために改良した部分は、もう少し確認して本家の方へ pull request してみたいなぁと思います。
まだまだ追加したい機能はたくさんありますが WebSocket や Redis など、使ってみたかったものを使いつつ動くということで
わーい。

*1:きちんとログアウトしないと幽霊部員になる