読者です 読者をやめる 読者になる 読者になる

Perl日記

PerlとかRubyとかPHPとかPythonとか

なんでもPSGIで考えるのはやめようという話【5/25追記しました】

昔作ったCGIでこんなのがあった。(あの頃は若かった)

#!/usr/bin/perl
use Furl;
my $furl = Furl->new;
use CGI;
print CGI->header;
print "<html>\n";
print "<head>\n";
print "<title>image</title>\n";
print "</head>\n";
print "<body>\n";
for my $i (1..10) {
  # 画像ファイル名
  $image_file = sprintf('%05d', $i).'.gif';
  # 画像を取得する
  my $res = $furl->get('http://.…/'.$image_file);
  # 画像を保存する
  open my $fh, '>', '/home/rightgo09/'.$image_file or die $!;
  print $fh $res->content;
  close $fh;

  # 結果をブラウザ画面に表示
  print "<div>$image_file saved !</div>\n";
  sleep 1; # 1秒待つ
}
print "</body>\n";
print "</html>\n";

連番の画像をとりつつ、その状況をブラウザで見られるCGIである。
サーバ負荷のためか画像をとってくるのを1秒間隔で行っている。
その都度ブラウザには1行ずつ文字列が現れていく。


これをいまならどう書くかなーと思って、PSGIで考えてたらまあPlackではできないよなと気がついた(遅い)。

# 最低限のPSGI
sub { [ 200, [ 'Content-Type' => 'text/plain' ], [ 'Hello!' ] ] }

HTTPレスポンスの本文できちゃってる状態で渡さないと!
途中の状態をだらだら表示というのに向いてない。


そもそもHTMLとして不完全な状態をだらだら表示してくれるのはブラウザが賢いからであって、途中の状態までを表示(出力)していい道理にはならない


まあだからあれだ、ここは素直にAjaxとかWebSocketとかでクライアントサイドで動的に画面内描画をするのが妥当なのでしょうね。


2012/5/25 追記

教えてもらいました。PSGI/Plackでも上で書いたことができました。

http://search.cpan.org/~miyagawa/PSGI-1.10/PSGI.pod#Delayed_Response_and_Streaming_Body

ちゃんとPODに書いてありました。ほんと、その、失礼しました。
「あれでしょ? PSGIってHTTPレスポンスコードとヘッダとボディが入った配列リファレンスを返せばいいんでしょ?」と覚えていたのが敗着でした。


と、いうわけで、覚えたことを書いておく。

PODのサンプルを拝借する。(日本語コメントを足した)

my $app = sub {
  my $env = shift;

  # Delays response until it fetches content from the network
  # ネットワークからコンテンツを取得するまでレスポンスを遅らせる
  return sub {             # CODEREFを返すときは
    my $responder = shift; # レスポンス用のCODEREFを受け取れる

    # サーバからコンテンツを取得するサブルーチン
    fetch_content_from_server(sub {
      my $content = shift;
      $responder->([ 200, $headers, [ $content ] ]); # レスポンス用のCODEREFを実行する
    });
  };
};

たぶん下にこんな感じで続くのだろう。

sub fetch_content_from_server {
  my $code = shift;
  my $content = Furl->new->get('http://www.google.co.jp/')->content;
  return $code->($content);
}

コールバックでフォールバックな感じと。
そしてこのスタイルが上記エントリで書いてたことを実現させるためには不可欠となるようだ。

もう1つの方のサンプルも拝借させていただく。(日本語コメントを足した)

my $app = sub {
  my $env = shift;

  # immediately starts the response and stream the content
  # レスポンスとコンテンツのストリーミングをすぐに始める
  return sub {             # CODEREFを返すときは
    my $responder = shift; # レスポンス用のCODEREFを受け取れる
    my $writer = $responder->( # さらにオブジェクトが返ってくる
      # 渡すのがHTTPステータスコードとヘッダだけだと↑
      [ 200, [ 'Content-Type', 'application/json' ]]);

    # 待ちが発生するイベント
    wait_for_events(sub {
      my $new_event = shift;
      if ($new_event) {
        # 出力
        $writer->write($new_event->as_json . "\n");
      } else {
        # 通信終了
        $writer->close;
      }
    });
  };
};

そしてたぶんこんな感じで下に続くのだろう。

sub wait_for_events {
  my $code = shift;
  my $pm = Parallel::ForkManager->new(5);
  for my $i (1..100) {
    $pm->start and next;
    my $result = heavy_process($i); # 遅い処理
    $code->($result);               # できたら出力
    $pm->finish;
  }
  $pm->wait_all_children;
  $code->(); # 出力終了
}

テラ素晴ラシス!



「できない」とか言って、もう赤面ですけど、勉強できたので書いてよかったと思います。