PSGIアプリでWebSocketを使う場合、responderはどうすべきか?

Plack::App::WebSocketを実装していて、「PSGIのresponderをどう扱うべきか?」に悩みました。

Plack::App::WebSocketは内部で以下のようなことをしています。

my $env = shift;
my $fh = $env->{"psgix.io"};
return sub {
    my $responder = shift;
    ... # WebSocket communication on $fh
};

PSGI拡張仕様で定義されている"psgix.io"から生ソケットを取り出し、あとはモジュール側で勝手にWebSocket通信をする、といった具合です。Amon2::Plugin::Web::WebSocketやPocketIOもこういった実装になっています。

しかしここで、$responderはどうすればいいでしょうか?


$responderをコールしない限り、PSGIサーバはそのセッションが生きているものとみなすはずです。ソースを読む限り、Twiggyではexit_guardというAnyEvent::CondVarオブジェクトのカウンタがリセットされず、CoronaではCoroスレッドが立ちっぱなしになるように見えます。

つまり$responderをコールしないとPSGIサーバ内でなんらかのセッション情報が溜まり続けてしまうことになります。

とりあえず、Plack::App::WebSocketでは、WebSocketコネクションがクローズしてから数秒後に自動的にダミーのレスポンスを渡して$responderを呼ぶようにしてみました。が、あまりスジのいいやり方とも思えません。

もう一つのやり方として、PSGIのストリーミングレスポンス仕様を使う手があります。

my $env = shift;
my $fh = $env->{"psgix.io"};
return sub {
    my $responder = shift;
    validation_of_client_request($env);
    my @handshake_response = create_handshake_response($env);
    my $writer = $responder->(101, \@handshake_response);
    ... # read on $fh, write on $writer
};

サーバからのハンドシェイクレスポンスをPSGI仕様にのっとって送信し、その後サーバからクライアントに送るデータはPSGIストリーミングレスポンスとして送信する、というやり方です。

この方法はread端とwrite端が異なるあたりがちょっと面倒ということを除けば、割とマトモな方法に見えます。実際、Plack::App::Proxy::WebSocketではこういった方法がとられているようです。

しかし、この方法には以下のような不安が残ります。

  • PSGIサーバがHTTP/1.1非対応だったら? WebSocketハンドシェイクレスポンスは一応、HTTP/1.1形式であることが要求されている。
  • PSGIサーバやミドルウェアが$writerで書き出されたデータをバッファリングしないか?
  • PSGIサーバやミドルウェアが$writerで書き出されたデータをchunkedエンコーディングしたりgzip圧縮したりしないか?
  • PSGIサーバは$writer->close()時点でちゃんとTCPコネクションを切断するのか?

これらの挙動はレスポンスのHTTPヘッダをきちんと設定すればPSGIアプリ側から制御できるかもしれません。が、PSGIのコンセプトから言って、PSGIアプリに対してこういった挙動の詳細が隠蔽されていても文句は言えないような気がします。

ここまで考えると、そもそもHTTPとは別の通信仕様であるWebSocketプロトコルを、HTTPの抽象化インタフェースであるPSGIの上に乗っける事自体に無理があるのかもなと思えてきました。まあ本気でWebSocketを使うアプリを作るんだったら、WebSocket処理部とそれ以外を別プロセスに分けて、前段のリバースプロキシか何かでリクエストを振り分けるっていう構成が無難ですかね。